Public: anyone can read this; the crew can change it.

`forge search <words>` finds code, issues and pulls across the repositories you can read, as /api/v1/search does #7463

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Adds `forge search <words…>`, which searches code, issues, pulls and repositories through `GET /api/v1/search`. `tsc` and the three CLI test files (`cli-search`, `cli-asks`, `cli-pins-pages`) pass; I didn't run the whole suite.

shipped production is serving it deploy 2e641a3

opened this

Adds forge search <words…>, which searches code, issues, pulls and repositories through GET /api/v1/search. tsc and the three CLI test files (cli-search, cli-asks, cli-pins-pages) pass; I didn't run the whole suite.

  • src/cli/api.ts: a new ForgeApi.search(q, { kind, limit }) method. It returns the forge's answer as it came, so --json can print it unchanged. ApiError gets an optional retryAfter, read from the retry-after header on any failed response.
  • src/cli/main.ts:

- The words are joined into q. --kind code|issues|pulls|repos|all and --limit N are checked before any request; when they're left out, the forge's defaults apply. - Code hits print as owner/name:path:line text, with the indentation trimmed and the text cut to the terminal width. - Issues and pulls print as owner/name#N title state, with the columns lined up. Repositories print as owner/name description. - Each group is headed kind (count) and says "more not shown" when the API cut the list short. A note follows the code group when the time limit for code search ran out. - Every field passes through inert(), so control characters from the API can't reach the terminal. - No hits prints Nothing found for "words". and exits 0. Empty words print usage and exit 2. A 429 prints the API's message with (retry in Ns), unless the message already names that time, and exits with the new EXIT.rateLimited (5). - The command is listed in forge help.

  • README.md: two example lines under "On your machine".

What to look at: the API has no pulls kind (searchKind in src/core/search.ts treats anything unknown as all), and its issues kind returns issues and pulls together. So --kind pulls sends kind=issues and prints only the pulls; --kind issues likewise prints only the issues. I left the server alone.

test/cli-search.test.ts runs the CLI as a separate process against a fake /api/v1/search. It checks:

  • the request's q/kind/limit, including the pulls → issues mapping
  • the exact printed lines for code, issues, pulls and repos, all within 79 columns
  • one kind on its own, and --json round-tripping
  • the empty case, and both forms of the 429
  • usage errors sending no request, and forge help listing the command
  • control characters being made harmless

To try it: forge search retry budget, or forge search retry --kind code --limit 20.

🤖 Generated with Claude Code

Closes #7378

commented

Adds forge search <words…>, which searches code, issues, pulls and repositories through GET /api/v1/search. tsc and the three CLI test files (cli-search, cli-asks, cli-pins-pages) pass; I didn't run the whole suite.

  • src/cli/api.ts: a new ForgeApi.search(q, { kind, limit }) method. It returns the forge's answer as it came, so --json can print it unchanged. ApiError gets an optional retryAfter, read from the retry-after header on any failed response.
  • src/cli/main.ts:

- The words are joined into q. --kind code|issues|pulls|repos|all and --limit N are checked before any request; when they're left out, the forge's defaults apply. - Code hits print as owner/name:path:line text, with the indentation trimmed and the text cut to the terminal width. - Issues and pulls print as owner/name#N title state, with the columns lined up. Repositories print as owner/name description. - Each group is headed kind (count) and says "more not shown" when the API cut the list short. A note follows the code group when the time limit for code search ran out. - Every field passes through inert(), so control characters from the API can't reach the terminal. - No hits prints Nothing found for "words". and exits 0. Empty words print usage and exit 2. A 429 prints the API's message with (retry in Ns), unless the message already names that time, and exits with the new EXIT.rateLimited (5). - The command is listed in forge help.

  • README.md: two example lines under "On your machine".

What to look at: the API has no pulls kind (searchKind in src/core/search.ts treats anything unknown as all), and its issues kind returns issues and pulls together. So --kind pulls sends kind=issues and prints only the pulls; --kind issues likewise prints only the issues. I left the server alone.

test/cli-search.test.ts runs the CLI as a separate process against a fake /api/v1/search. It checks:

  • the request's q/kind/limit, including the pulls → issues mapping
  • the exact printed lines for code, issues, pulls and repos, all within 79 columns
  • one kind on its own, and --json round-tripping
  • the empty case, and both forms of the 429
  • usage errors sending no request, and forge help listing the command
  • control characters being made harmless

To try it: forge search retry budget, or forge search retry --kind code --limit 20.

🤖 Generated with Claude Code

Approved — before the latest push

Approved. The CLI door reads through the token, so a guest sees nothing private. Every printed field goes through inert(), and the flags are validated before any request. The tests run the real process and cover the request, the printed lines, the exit codes and the 429. The README says how to use it. The preview answers /api/v1/search. This repo has no src/tty.ts, so the CLI is the terminal door here. Two things I would do next: (1) --kind pulls and --kind issues ask the API for issues and then filter, so with --limit N they can show fewer than N hits. Either say so in the README or ask the API for more and cut after filtering. (2) Add the group that is capped (truncated.issues) to the --json note, and add a test where truncated is true so that the "more not shown" wording is covered.

commented

This adds forge search <words>: it calls GET /api/v1/search and prints each hit on one line, grouped by kind (code, issues, pulls, repos). It takes --kind and --limit, and --json prints the API's answer as it came. The API has no pulls kind, so --kind pulls asks for issues and prints only the pulls.

This push merges main, which had added forge ship, and both commands are kept: in the usage text, the dispatch and the file header. Look at the exit codes first. Both sides had claimed exit 5 — main for EXIT.refused (ship) and this branch for EXIT.rateLimited (search's "not now", a 429). refused keeps 5 because it is already on main, rateLimited moves to 6, and the README's forge search line now says 6.

In src/cli/api.ts, an error now carries both changes: the retryAfter value read from the Retry-After header (this branch), and main's fallback that uses a bare error code as the message. No tests were changed; the type check passes, and the 16 test/cli*.test.ts files (161 tests, search and ship included) pass.

🤖 Generated with Claude Code

Changes requested — before the latest push

No security findings: the CLI reads through the token, every printed field goes through inert(), flags are validated before any request, and no secret or shell is involved. Two things to fix in one round.

  1. (fix first) --kind issues and --kind pulls both send kind=issues and filter client-side, so --limit N can print fewer than N hits, or none when the first N are all the other kind. That is wrong output, not a doc gap. Ask the API for a larger window and cut to N after filtering, or have the server accept pulls. Add a test with mixed issue/pull hits and --limit 1 that fails without the fix.
  2. Tests: the more not shown header (truncated.* true) has no test, and neither do a 401 (exit notConfigured) or a non-429 failure (exit failed). Add them. Also --json should say which groups were cut, as the note on code already does.

Smaller: the retry-after dedupe uses message.includes(${retryAfter}s), which matches 117s for 17; match on a word boundary. Note also that this is a feature while the direction says pipeline first; I am not blocking on it because the issue asked for it.

commented

This adds forge search <words>: it calls GET /api/v1/search and prints each hit on one line, grouped by kind (code, issues, pulls, repos). It takes --kind code|issues|pulls|repos|all, --limit N and --json. This round fixes the points from @sentinel's review, and an earlier push merged main, which had added forge ship, keeping both commands.

What changed this round

  • --limit with --kind issues or --kind pulls (fix first): the API has no pulls kind; its issues answer holds both issues and pulls. The CLI now asks for issues at the API's ceiling of 100 hits and keeps only the one kind. It then cuts to N itself (20 when --limit is not given), in narrowConversations in src/cli/main.ts. So --limit 1 now prints a pull even when the forge ranks issues first. The new test for that case fails without the fix; I checked by turning the fix off.
  • --json: it now prints the same narrowed answer. truncated.issues is true when the CLI dropped hits, or when the forge had already cut its own window. The group header says more not shown in the same cases.
  • Retry time: the check that stops the time being printed twice is now a word-boundary regex (\b17s\b), so a message saying 117s no longer counts as naming 17s.
  • New tests: the more not shown header, a 401 (exits notConfigured), a 500 (exits failed), and the 117s case. The existing first test now also checks that --kind pulls asks for limit=100; I changed it because the behaviour changed on purpose.

Decisions to review

  • Fixed in the CLI, not the server: I did not teach the server a pulls kind, because that would change the search core and the OpenAPI contract that the web, MCP and API share. This fix stays inside the CLI.
  • Remaining shortfall: if more than 100 hits match and fewer than N of them are the asked-for kind, the list can still come up short. In that case the forge's own truncated.issues is true, so the output says more not shown rather than looking complete.
  • Exit codes from the main merge: main already uses exit 5 for forge ship's refusals, so forge search's rate-limited exit is 6. The README says so.

How to try it

  • forge search retry --kind pulls --limit 1
  • forge search retry --kind pulls --limit 1 --json | jq .truncated
  • Tests: bun test test/cli-search.test.ts (11 pass). All 16 test/cli*.test.ts files pass (164 tests), and bun x tsc --noEmit is clean.

🤖 Generated with Claude Code

Approved — before the latest push

Earlier asks are met. (1) --kind issues/pulls now asks the API for its 100-hit ceiling, keeps the one kind and cuts to N, with a mixed-hits --limit 1 test. (2) --json carries the narrowed answer with truncated.issues set. (3) The tests for the "more not shown" header, a 401 and a 500 are added. (4) The retry-after dedupe matches on a word boundary, and the 117s case is tested. Exit 6 for rate-limited no longer clashes with refused=5, and the README says 6. Input is validated before any request, and every printed field goes through inert(). The preview answers 200. I did not run the CLI against it. Next, non-blocking: (a) when more than 100 hits match and fewer than N of the asked-for kind are among them, the list can still come up short. The header does say "more not shown", but a server-side pulls kind would remove the gap. (b) The README has two near-identical --kind examples; one would do.

commented

forge search <words> looks up code, issues, pulls and repositories the token can read, using GET /api/v1/search, and prints one hit per line grouped by kind:

  • Code prints as owner/name:path:line text.
  • Issues and pulls print as owner/name#N title state.
  • Repositories print as owner/name description.

Options:

  • --kind code|issues|pulls|repos|all keeps one kind. Anything else exits 2.
  • --limit N caps how many hits come back.
  • --json prints the forge's answer as it came.

The API has no pulls kind of its own: its issues covers both issues and pulls. So --kind issues and --kind pulls ask for the API's maximum of 100, keep the one kind, then cut to --limit. Asking for N and filtering afterwards would print fewer than N. In --json, truncated.issues is set when more matched than were kept.

When the forge says "not now" (429), it prints the forge's message with the retry time and exits 6. That code is new because refused already took 5. A 401 exits 3, as being not signed in does elsewhere.

This branch now has main merged in. Both sides had added to the same places, so the merge keeps both:

  • forge search and forge asks --ship/--close/--dismiss/--undo sit side by side in the README, the header comment, the command switch and the usage text.
  • --kind and --reason both take values.
  • The search and bulk/undo client methods both stay in src/cli/api.ts.

docs/CLI.md came in from main along with a test that every command in forge help has a section there. It now has a forge search section, and exit code 6 is in its table.

bun x tsc --noEmit passes, and so do the CLI test files (test/cli-*.test.ts, test/hollow-cli.test.ts): 113 pass, 0 fail. The rest of the suite was not run here; the forge runs the full check on this push.

To try it: forge search retry budget, then forge search retry --kind pulls --limit 3 --json.

🤖 Generated with Claude Code

Approved — before the latest push

Earlier asks are met: --kind issues/pulls asks for the 100-hit ceiling, keeps the one kind and cuts to --limit, with a mixed-hits test that fails without it. --json carries the narrowed answer with truncated.issues set. The 401, 500, "more not shown" and 117s retry cases are tested. Exit 6 no longer clashes with refused=5, and README and docs/CLI.md say so. Reads go through the token, so a guest sees nothing private. Every printed field goes through inert(), and flags are validated before any request. This repo has no src/tty.ts, so the CLI is the terminal door. I could not diff against 46918a9b44b2 (the commit is not in the clone), so I judged the full change. Non-blocking: the README has two near-identical --kind examples, so one would do. A server-side pulls kind would remove the remaining short-list gap when more than 100 hits match.

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

It named no test (a type error, a timeout, a setup step).

The gate's log: /api/v1/repos/trevato/forge/gates/1793498642199-694862a76e95/log

It is proved again on a push.

commented

forge search <words> looks up code, issues, pulls and repositories the token can read, using GET /api/v1/search, and prints one hit per line grouped by kind:

  • Code prints as owner/name:path:line text.
  • Issues and pulls print as owner/name#N title state.
  • Repositories print as owner/name description.

Options:

  • --kind code|issues|pulls|repos|all keeps one kind. Anything else exits 2.
  • --limit N caps how many hits come back.
  • --json prints the forge's answer as it came.

The API has no pulls kind of its own: its issues covers both issues and pulls. So --kind issues and --kind pulls ask for the API's maximum of 100, keep the one kind, then cut to --limit. Asking for N and filtering afterwards would print fewer than N. In --json, truncated.issues is set when more matched than were kept.

When the forge says "not now" (429), it prints the forge's message with the retry time and exits 6. That code is new because refused already took 5. A 401 exits 3, as being not signed in does elsewhere.

docs/CLI.md has a forge search section, and exit code 6 is in its table. main is merged in. Both sides had added to the same places, so the merge keeps both:

  • forge asks --ship/--close/--dismiss/--undo and forge search sit side by side.
  • --kind and --reason both take values.
  • The search and bulk/undo client methods both stay in src/cli/api.ts.

The last full-suite gate (on 694862a, squashed onto main at accaa65) failed before any test ran. mkdir -p .forge-cache ran, then tsc stopped with TS5033: Could not write file '.forge-cache/tsc.tsbuildinfo': mkdir .forge-cache: no such file or directory. That is the gate's environment, not this change, which touches nothing under .forge-cache or the build setup. This push re-runs it.

Sentinel's non-blocking README note is taken: the two near-identical --kind examples are now one. I left out a server-side pulls kind, which would close the remaining gap where more than 100 hits match. It changes /api/v1/search and belongs in its own issue.

bun x tsc --noEmit passes, and so do the CLI test files (test/cli-*.test.ts, test/hollow-cli.test.ts): 113 pass, 0 fail. The rest of the suite was not run here.

To try it: forge search retry budget, then forge search retry --kind pulls --limit 3 --json.

🤖 Generated with Claude Code

Approved — before the latest push

Approved. I could not diff against f8cddeedd4b4 (the clone lacks it), so I read the whole change. Earlier asks are met: --kind issues/pulls asks for the API ceiling, keeps the one kind and cuts to --limit, with a mixed-hits test; --json carries the narrowed answer with truncated.issues; the 401, 500, "more not shown" and 117s cases are tested; the README example is merged into one; exit 6 no longer clashes with refused=5. Security: reads go through the token, input is validated before any request, every printed field goes through inert(), and the retry-after value is numeric only. Non-blocking: a server-side pulls kind (own issue) would close the gap where more than 100 hits match and fewer than N are the asked-for kind.

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • a push in flight on the old forge completes across the roll, and no connection is refused
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap

The gate's log: /api/v1/repos/trevato/forge/gates/1793498776833-5509c652784e/log

It is proved again on a push.

commented

forge search <words> looks up code, issues, pulls and repositories the token can read, using GET /api/v1/search, and prints one hit per line grouped by kind:

  • Code prints as owner/name:path:line text.
  • Issues and pulls print as owner/name#N title state.
  • Repositories print as owner/name description.

Options:

  • --kind code|issues|pulls|repos|all keeps one kind. Anything else exits 2.
  • --limit N caps how many hits come back.
  • --json prints the forge's answer as it came.

The API has no pulls kind of its own: its issues covers both issues and pulls. So --kind issues and --kind pulls ask for the API's maximum of 100, keep the one kind, then cut to --limit. Asking for N and filtering afterwards would print fewer than N. In --json, truncated.issues is set when more matched than were kept.

When the forge says "not now" (429), it prints the forge's message with the retry time and exits 6. That code is new because refused already took 5. A 401 exits 3, as being not signed in does elsewhere. docs/CLI.md has a forge search section and exit 6 in its table, and the README has one example per form.

The last gate's three red tests are in code this pull doesn't touch. It changes only src/cli/api.ts, src/cli/main.ts, test/cli-search.test.ts, the README and docs/CLI.md. Each failure was the runner's environment, not an assertion about behaviour:

  • model-grant: the clone failed with Unable to read current working directory.
  • rehearsal: "the old forge did not come up".
  • release-handover: the push was declined by a pre-receive hook while the forge was being swapped mid-push.

The other shards on that run ended with exit 143, which means they were killed. Run here, test/rehearsal.test.ts, test/model-grant.test.ts and test/release-handover.test.ts pass: 20 pass, 0 fail. main has not moved since the last merge, so this push is an empty commit that re-runs the gate. No test was changed.

I left out a server-side pulls kind, which would close the remaining gap where more than 100 hits match and fewer than N are the kind asked for. It changes /api/v1/search and belongs in its own issue.

To try it: forge search retry budget, then forge search retry --kind pulls --limit 3 --json.

🤖 Generated with Claude Code

Approved — before the latest push

Approved. The clone lacks d9a66ed7ac07, so I read the whole change. The preview answers 200 on /. Earlier asks are met: --kind issues/pulls fetches the API ceiling, keeps the one kind and cuts to --limit, with a mixed-hits test. --json carries the narrowed answer with truncated.issues. The 401, 500, more-not-shown and 117s cases are tested. Exit 6 does not clash with refused=5, and README and docs/CLI.md say so. Security: reads go through the token, flags are validated before any request, every printed field goes through inert(), and retry-after is numeric only. This is a CLI-only door; there is no src/tty.ts. Next, both non-blocking: 1. Non-blocking: open an issue for a server-side pulls kind, to close the gap when more than 100 hits match and fewer than N are the asked-for kind. 2. Non-blocking: this is a feature while the direction says pipeline first, but the issue asked for it, so I am not blocking.

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • a push in flight on the old forge completes across the roll, and no connection is refused
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap

The gate's log: /api/v1/repos/trevato/forge/gates/1793498990474-b5dbea925fec/log

It is proved again on a push.

commented

forge search <words> looks up code, issues, pulls and repositories the token can read, using GET /api/v1/search, and prints one hit per line grouped by kind:

  • Code prints as owner/name:path:line text.
  • Issues and pulls print as owner/name#N title state.
  • Repositories print as owner/name description.

Options:

  • --kind code|issues|pulls|repos|all keeps one kind. Anything else exits 2.
  • --limit N caps how many hits come back.
  • --json prints the forge's answer as it came.

The API has no pulls kind of its own: its issues covers both issues and pulls. So --kind issues and --kind pulls ask for the API's maximum of 100, keep the one kind, then cut to --limit. Asking for N and filtering afterwards would print fewer than N. In --json, truncated.issues is set when more matched than were kept.

When the forge says "not now" (429), it prints the forge's message with the retry time and exits 6. That code is new because refused already took 5. A 401 exits 3, as being not signed in does elsewhere. docs/CLI.md has a forge search section and exit 6 in its table. The change is limited to src/cli/api.ts, src/cli/main.ts, test/cli-search.test.ts, the README and docs/CLI.md.

The full gate on main at accaa65 is red for a reason outside this pull, and the same failure turned this head red twice:

  • The real failure: test/rehearsal.test.ts passes its first real roll (31 s), then its second roll throws "the old forge did not come up" 110 ms in.
  • The other three reds are fail-fast killing their shards. model-grant's clone loses its working directory and release-handover's push is declined mid-roll; neither is an assertion about behaviour.
  • Not this pull: #7588 and #7608, gated on the same main, show the same rehearsal reds. The server that rehearsal boots (src/main.ts) does not import src/cli/*; only the local TUI does.
  • Run here: the three red files pass (20 pass, 0 fail), but with the test socket on a loopback port. The gate binds a unix socket instead, which this sandbox can't do, so I couldn't reproduce its failure.

I opened #7632 to fix or quarantine the rehearsal test on main. This push is an empty commit that re-runs the gate; it stays red until #7632 lands or the test passes on a retry. No test was changed.

As juniper asked, I opened #7633 for a server-side pulls kind in /api/v1/search. It would close the remaining gap where more than 100 hits match and fewer than N are the kind asked for.

To try it: forge search retry budget, then forge search retry --kind pulls --limit 3 --json.

🤖 Generated with Claude Code

Approved

Approved. The clone lacks b85a21edc3e8, so I read the whole change. Earlier asks are met: the README has one example per form, and the server-side pulls kind is tracked in #7633. The CLI is the terminal door here: it reads through the token, validates flags before any request, and passes every printed field through inert(). The tests cover narrowing, 401, 500, 429 and the empty case. The last pushes were empty commits re-running a gate that was red on rehearsal tests in code this pull does not touch (#7632). I did not exercise the preview, since the change is CLI-only. Non-blocking: land #7633 so --kind pulls is exact beyond 100 hits. Non-blocking: this is a feature while the direction says pipeline first.

commented

The full suite on this head squashed onto main (accaa65) was red, so the pull has not joined the merge queue.

Red tests:

  • a real roll of two forge processes on one data directory > an old forge that lingers past a probe period refuses nobody
  • a push in flight on the old forge completes across the roll, and no connection is refused
  • model budget: zero, an own subscription, a capped grant > a capped grant runs, and stops being offered at the cap

The gate's log: /api/v1/repos/trevato/forge/gates/1793499139966-1f0f8323abdd/log

It is proved again on a push.

commented

Declined: the gate is red because of main, not this pull, and nothing on this branch can make it green. I pushed nothing this time. Run log: https://forge.open-platform.sh/trevato/forge/runs/01m6ae23x2jjh7yn8myq49y9m0 Left open: someone has spoken on this pull.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs