chore: fix mentions of read tool in backticks causing confusing against bash read

This commit is contained in:
can1357
2026-05-13 05:22:41 +02:00
parent b513fee874
commit 724ef3784a
10 changed files with 40 additions and 24 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ Each subagent **MUST** follow this exact workflow:
#### a. Read everything
1. `github issue_view` (with comments) — comments often carry the real repro and fix hints.
1. Read `issue://<N>` (or `issue://<owner>/<repo>/<N>` for cross-repo) — fetches the issue body plus comments; comments often carry the real repro and fix hints. Append `?comments=0` only if you explicitly want to skip them.
2. `gh search prs` for the issue number to see if a fix is already in flight.
- If a PR exists and looks reasonable → switch tracks: review that PR per `.omp/commands/review-prs.md` instead, and report back as `existing-pr`. Do **not** open a competing fix.
+1 -1
View File
@@ -35,7 +35,7 @@ Each subagent **MUST** follow this exact workflow:
#### a. Read & decide
1. `github pr_view` (with comments) and `github pr_diff` for the PR.
1. Read `pr://<N>` (with comments by default; append `?comments=0` to skip) and `pr://<N>/diff` for the changed-files listing — use `pr://<N>/diff/all` when you need the full unified diff, or `pr://<N>/diff/<i>` for a single file slice.
2. Check `git log origin/main` and `gh search prs` for whether the same change already landed.
3. Classify into one of:
- **slop** — AI-generated noise, broken, off-spec, or net-negative. Drop, write a 1–2 line justification, do not check out.
+1 -1
View File
@@ -3,7 +3,7 @@
Skills are file-backed capability packs discovered at startup and exposed to the model as:
- lightweight metadata in the system prompt (name + description)
- on-demand content via `read skill://...`
- on-demand content via the `read` tool against `skill://...`
- optional interactive `/skill:<name>` commands
This document covers current runtime behavior in `src/extensibility/skills.ts`, `src/discovery/builtin.ts`, `src/internal-urls/skill-protocol.ts`, and `src/discovery/agents-md.ts`.
+1 -1
View File
@@ -100,7 +100,7 @@ Stdout and stderr are merged before the model sees them. Non-zero exit codes are
- Uses `session.allocateOutputArtifact()` for spill files.
- User-visible prompts / interactive UI
- PTY mode opens a TUI overlay titled `Console` and forwards input to the PTY.
- Background start messages direct the agent to `job` and `read jobs://<id>`.
- Background start messages direct the agent to `job` and to read `jobs://<id>`.
- Background work / cancellation
- Async and auto-background jobs continue after the initial tool return.
- Cancellation aborts the native run; PTY overlay dismissal also kills the PTY.
+5 -5
View File
@@ -42,8 +42,8 @@ Streaming behavior:
- Final return is single-shot after a completion, timeout, abort, or immediate fast path.
Related read path:
- `read jobs://` lists all current jobs.
- `read jobs://<id>` renders one job with status, label, start time, duration, and stored result/error text.
- Reading `jobs://` lists all current jobs.
- Reading `jobs://<id>` renders one job with status, label, start time, duration, and stored result/error text.
## Flow
1. `JobTool.createIf(...)` in `packages/coding-agent/src/tools/job.ts` only exposes the tool when `isBackgroundJobSupportEnabled(...)` returns true for either `async.enabled` or `bash.autoBackground.enabled`.
@@ -129,15 +129,15 @@ Lifecycle and exact state names:
- Cancelling a non-running job is not an exception; it reports `already_completed` even if the actual status is `completed`, `failed`, or `cancelled`.
- Tool-call abort during polling stops waiting and returns a final snapshot through `#buildResult(...)`; it does not cancel watched jobs.
- Failures inside the underlying async work are stored on the job (`status: "failed"`, `errorText`) and reported in normal tool output, not rethrown by `job`.
- `read jobs://<id>` missing job returns markdown content headed `# Job Not Found` rather than throwing.
- Reading `jobs://<id>` for a missing job returns markdown content headed `# Job Not Found` rather than throwing.
## Notes
- `job` waits for the first watched running job to settle, not for all watched jobs. If others remain `running`, they are reported under `## Still Running`; the caller must invoke `job` again to continue waiting.
- Delivery suppression is the key difference between snapshot and automatic delivery:
- snapshots (`job`, `read jobs://`) read current manager state;
- snapshots (`job`, reads of `jobs://`) read current manager state;
- follow-up delivery comes from `AsyncJobManager.#enqueueDelivery(...)` and `sdk.ts` `onJobComplete`;
- watched or acknowledged ids are suppressed via `isDeliverySuppressed(...)`.
- `manager.cancel(id)` sets `status = "cancelled"` before the underlying promise settles. The job function may later populate `resultText` or `errorText`; `job-manager.ts` preserves that text but does not transition the status away from `cancelled`.
- `jobs://` is implemented by `JobsProtocolHandler` with `immutable = true`, but each resolve call reads live manager state at access time.
- `jobs://<id>` shows a cancellation section only when a cancelled job has `errorText`; cancelled jobs with `resultText` are not rendered with a result section there.
- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both `job` and `read jobs://<id>` behave as if the id never existed.
- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both `job` and reads of `jobs://<id>` behave as if the id never existed.
+2 -2
View File
@@ -4,8 +4,8 @@
### Breaking Changes
- Removed `op: issue_view` and `op: pr_view` from the `github` tool. Read single issues/PRs through `read issue://<N>` / `read pr://<N>` (or the long form `read issue://<owner>/<repo>/<N>` / `read pr://<owner>/<repo>/<N>`); append `?comments=0` to drop the comments section. The `issue` and `comments` parameters were removed from the tool schema since no remaining op consumes them. Mutating ops (`pr_create`, `pr_checkout`, `pr_push`), `repo_view`, `search_*`, and `run_watch` are unchanged.
- Removed `op: pr_diff` (along with the `nameOnly` and `exclude` schema fields) from the `github` tool. Read PR diffs through the new `pr://` URL family: `pr://<N>/diff` for the changed-file listing, `pr://<N>/diff/<i>` for a single file slice (1-indexed), and `pr://<N>/diff/all` for the verbatim unified diff. Long-form `pr://<owner>/<repo>/<N>/diff[/…]` works the same way. All three variants share one `gh pr diff` invocation through a new `pr-diff` cache row, so the listing and per-file slices reconstruct from cached bytes without re-shelling. Diff content is served as `text/plain` so `read`'s line selectors (`pr://<N>/diff/all:200-400`) page the cached output without falsely advertising hashline anchors.
- Removed `op: issue_view` and `op: pr_view` from the `github` tool. Read single issues/PRs via the `read` tool against `issue://<N>` / `pr://<N>` (or the long form `issue://<owner>/<repo>/<N>` / `pr://<owner>/<repo>/<N>`); append `?comments=0` to drop the comments section. The `issue` and `comments` parameters were removed from the tool schema since no remaining op consumes them. Mutating ops (`pr_create`, `pr_checkout`, `pr_push`), `repo_view`, `search_*`, and `run_watch` are unchanged.
- Removed `op: pr_diff` (along with the `nameOnly` and `exclude` schema fields) from the `github` tool. Read PR diffs through the new `pr://` URL family: `pr://<N>/diff` for the changed-file listing, `pr://<N>/diff/<i>` for a single file slice (1-indexed), and `pr://<N>/diff/all` for the verbatim unified diff. Long-form `pr://<owner>/<repo>/<N>/diff[/…]` works the same way. All three variants share one `gh pr diff` invocation through a new `pr-diff` cache row, so the listing and per-file slices reconstruct from cached bytes without re-shelling. Diff content is served as `text/plain` so the `read` tool's line selectors (e.g. `pr://<N>/diff/all:200-400`) page the cached output without falsely advertising hashline anchors.
### Added
@@ -240,7 +240,7 @@ export function getConflictHistory(session: ToolSession): ConflictHistory {
return session.conflictHistory;
}
/** A side of a conflict block that `read conflict://N/<scope>` can render. */
/** A side of a conflict block that the `read` tool can render via `conflict://N/<scope>`. */
export type ConflictScope = "ours" | "theirs" | "base";
const CONFLICT_SCOPES = new Set<ConflictScope>(["ours", "theirs", "base"]);
@@ -440,7 +440,7 @@ function markerLine(prefix: string, label: string | undefined): string {
}
/**
* Materialise a conflict block for `read conflict://<N>` (and its
* Materialise a conflict block for `conflict://<N>` reads (and their
* `/ours` / `/theirs` / `/base` scopes).
*
* Returns:
@@ -534,7 +534,7 @@ export function formatConflictWarning(
if (partial) {
const hintPath = options.displayPath ?? "<file>";
out.push(
`⚠ ${entries.length} of ${total} unresolved ${word} visible in this window (run \`read ${hintPath}:conflicts\` for the full list).`,
`⚠ ${entries.length} of ${total} unresolved ${word} visible in this window (read \`${hintPath}:conflicts\` for the full list).`,
);
} else {
out.push(`⚠ ${total} unresolved ${word} detected`);
@@ -551,7 +551,7 @@ export function formatConflictWarning(
if (theirsLabel) out.push(`- theirs = ${theirsLabel}`);
if (anyBase) out.push(`- base = ${baseLabel ?? "(no label)"}`);
out.push(
'NOTICE: Inspect a block with `read conflict://<N>` (add `/ours` / `/theirs` / `/base` to render a single side). Resolve with `write({ path: "conflict://<N>", content })`, or bulk-resolve every registered conflict with `write({ path: "conflict://*", content })`. Writes replace the whole conflict region (markers + all sides).',
'NOTICE: Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` to render a single side). Resolve with `write({ path: "conflict://<N>", content })`, or bulk-resolve every registered conflict with `write({ path: "conflict://*", content })`. Writes replace the whole conflict region (markers + all sides).',
);
out.push(
'`content` shorthand: a line that is exactly `@ours` / `@theirs` / `@base` / `@both` expands to that recorded section. `@both` is ours-then-theirs with no separator. Lines that are not a token pass through verbatim, so `"// keep both\\n@ours\\n@theirs"` literally writes the comment, then ours, then theirs.',
@@ -592,7 +592,7 @@ export function formatConflictWarning(
/**
* Render a single-line-per-block index of every conflict in a file.
* Used by `read <path>:conflicts` to give the agent a cheap overview
* Used by the `<path>:conflicts` read selector to give the agent a cheap overview
* of a heavily-conflicted file without dumping every body.
*/
export function formatConflictSummary(
@@ -614,7 +614,7 @@ export function formatConflictSummary(
if (theirsLabel) lines.push(`- theirs = ${theirsLabel}`);
if (anyBase) lines.push(`- base = ${baseLabel ?? "(no label)"}`);
lines.push(
'NOTICE: Bulk-resolve with `write({ path: "conflict://*", content })`, or address a single block with `write({ path: "conflict://<N>", content })`. Inspect a block with `read conflict://<N>` (add `/ours` / `/theirs` / `/base` for a single side).',
'NOTICE: Bulk-resolve with `write({ path: "conflict://*", content })`, or address a single block with `write({ path: "conflict://<N>", content })`. Inspect a block by reading `conflict://<N>` (add `/ours` / `/theirs` / `/base` for a single side).',
);
lines.push(
"`content` shorthand: `@ours` / `@theirs` / `@base` / `@both` lines expand to the recorded sections; `@both` = ours-then-theirs. Non-token lines pass through verbatim.",
@@ -198,6 +198,22 @@ describe("pr:// protocol handler", () => {
await expect(router.resolve("pr://owner/example/foo/bar")).rejects.toThrow(/Invalid pr:\/\/ URL/);
await expect(router.resolve("pr://owner/example/abc")).rejects.toThrow(/Invalid pr:\/\/ number/);
});
it("rejects empty / dot / dotdot path segments", async () => {
const router = InternalUrlRouter.instance();
await expect(router.resolve("pr://owner//77")).rejects.toThrow(
/Invalid pr:\/\/ URL: empty or unsafe path segment/,
);
await expect(router.resolve("pr://owner/repo/77/diff//2")).rejects.toThrow(
/Invalid pr:\/\/ URL: empty or unsafe path segment/,
);
await expect(router.resolve("pr://owner/../77/diff")).rejects.toThrow(
/Invalid pr:\/\/ URL: empty or unsafe path segment/,
);
await expect(router.resolve("issue://owner/./repo/1")).rejects.toThrow(
/Invalid issue:\/\/ URL: empty or unsafe path segment/,
);
});
});
describe("pr://.../diff family", () => {
@@ -380,7 +380,7 @@ describe("formatConflictWarning", () => {
expect(text).toContain(">>> theirs");
expect(text).toContain("\nc");
// NOTICE line with shorthand tokens.
expect(text).toContain("NOTICE: Inspect a block with `read conflict://<N>`");
expect(text).toContain("NOTICE: Inspect a block by reading `conflict://<N>`");
expect(text).toContain('`write({ path: "conflict://<N>", content })`');
expect(text).toContain('`write({ path: "conflict://*", content })`');
expect(text).toContain("@ours");
@@ -101,7 +101,7 @@ describe("read surfaces conflicts as a warning footer", () => {
expect(text).toContain("──── #1 L2-6 ────");
expect(text).toContain("<<< ours");
expect(text).toContain(">>> theirs");
expect(text).toContain("NOTICE: Inspect a block with `read conflict://<N>`");
expect(text).toContain("NOTICE: Inspect a block by reading `conflict://<N>`");
expect(text).toContain('`write({ path: "conflict://<N>", content })`');
expect(text).toContain('`write({ path: "conflict://*", content })`');
expect(text).toContain("@ours");
@@ -164,7 +164,7 @@ describe("read surfaces conflicts as a warning footer", () => {
expect(session.conflictHistory?.get(2)).toBeUndefined();
});
it("renders the full conflict block via `read conflict://<N>`", async () => {
it("renders the full conflict block via reads of `conflict://<N>`", async () => {
const filePath = path.join(tempDir, "full.ts");
await Bun.write(filePath, TWO_WAY);
const session = createTestSession(tempDir);
@@ -182,7 +182,7 @@ describe("read surfaces conflicts as a warning footer", () => {
expect(text).not.toContain("⚠");
});
it("renders only the theirs body via `read conflict://<N>/theirs`", async () => {
it("renders only the theirs body via reads of `conflict://<N>/theirs`", async () => {
const filePath = path.join(tempDir, "theirs.ts");
await Bun.write(filePath, TWO_WAY);
const session = createTestSession(tempDir);
@@ -230,14 +230,14 @@ describe("read surfaces conflicts as a warning footer", () => {
await expect(promise).rejects.toThrow(/Conflict #99 not found/);
});
it("rejects `read conflict://*` (wildcard is write-only)", async () => {
it("rejects reads of `conflict://*` (wildcard is write-only)", async () => {
const session = createTestSession(tempDir);
const read = await getTool(session, "read");
const promise = read.execute("read-wildcard", { path: "conflict://*" });
await expect(promise).rejects.toThrow(/wildcards are write-only/);
});
it("`read <path>:conflicts` lists every conflict in the file with stable ids", async () => {
it("the `<path>:conflicts` read selector lists every conflict in the file with stable ids", async () => {
const filePath = path.join(tempDir, "many.ts");
await Bun.write(filePath, TWO_BLOCKS);
const session = createTestSession(tempDir);
@@ -287,7 +287,7 @@ describe("read surfaces conflicts as a warning footer", () => {
const result = await read.execute("read-window", { path: "wide.ts:1-5" });
const text = getText(result);
expect(text).toContain("1 of 2 unresolved");
expect(text).toContain("read wide.ts:conflicts");
expect(text).toContain("read `wide.ts:conflicts`");
});
});