@sjawhar/claude-envoy 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.omp-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -2
- package/dist/dispatch.js +6 -6
- package/dist/envoy-channel.js +8 -8
- package/dist/session-hook.js +171 -17
- package/hooks/session-hook.ts +118 -11
- package/legion/skills/legion-worker/SKILL.md +1 -1
- package/legion/skills/legion-worker/references/pr-body.md +7 -1
- package/package.json +2 -2
package/.omp-plugin/plugin.json
CHANGED
package/AGENTS.md
CHANGED
|
@@ -113,9 +113,9 @@ Dispatch asks stay on Dispatch.
|
|
|
113
113
|
| Oh My Pi plugin manifest | `.omp-plugin/plugin.json` | Empty `mcpServers`, which is how omp is told this plugin has no MCP server: the channel server exits without Claude Code's session identity, and omp gets Envoy and Dispatch from `@sjawhar/pi-envoy`. omp reads this file before `.claude-plugin/plugin.json` and a manifest `mcpServers` replaces `.mcp.json` rather than merging; omitting the key falls through to `.mcp.json` and relaunches the server. Claude Code never reads it. Its `version` equals `package.json`'s (tested); omp reads only the file's `mcpServers` and that it exists, never its `version`. |
|
|
114
114
|
| Skills | `skills/agent-secrets`, `skills/asd-ste100`, `skills/envoy`, `skills/dispatch`, `skills/dispatch-first`, `skills/dispatch-writing-specs`, `skills/outside-message`, `skills/plain-words` | Relative symlinks into the root `skills/`; the plugin ships only these eight, the ones a standalone Claude Code session uses (the rest belong to Legion roles on Oh My Pi; README "Skills"). The pack resolves them, not the install: `bun pm pack` packs a link as nothing at all, so `package.json`'s `prepack` (`scripts/pack-skills.sh`) replaces each link with the skill's own files, which `files`' `skills/**` then packs, and its `postpack` puts the links back. `scripts/check-packed-skills.sh` reads them back out of the tarball for `release.yaml`'s `claude_envoy_build`, and `tests/packed-skills.test.ts` packs a tarball of its own. |
|
|
115
115
|
| Built bundle | `dist/`, `scripts/build.ts`, `scripts/verify-build.sh` | Built, never committed: `release.yaml`'s `claude_envoy_build` builds it and packs it into the tarball it publishes to npm (its `bun run verify-build <tar>` builds it twice in a scratch directory, checks it as the release will and packs it, writing no file of the checkout, and nothing a person runs locally writes `dist/` or `legion/`; the jobs that run `verify-build` are named in its header and the jobs that run `release-push.sh extract-built` in that subcommand's paragraph; convention below), and `bun scripts/build.ts --out <dir>` builds a runnable copy outside the repository. The plugin cache has no `node_modules`, so the four executables (the channel server, the session hook, the title hook and the `dispatch` CLI, `BUNDLE_ENTRYPOINTS`) ship bundled, with `dist/THIRD_PARTY_NOTICES` (the inlined packages' licenses, from Bun's metafile through the root `scripts/third-party-notices.ts`); the build runs only on the pinned Bun (`.bun-version`). A mode folded into an existing bundle is paid for by that bundle's every other mode: an ESM entrypoint evaluates its whole module graph before it reads `argv`, which is why the title hook has an entrypoint of its own and why a hook on a blocking event is weighed by what its bundle imports, not by what its mode runs. |
|
|
116
|
-
| The `dispatch` CLI | `bin/dispatch` (the shim, mode 755), `dist/dispatch.js` (built from `../envoy-client/bin/dispatch.ts`), `hooks/session-hook.ts` | Claude Code puts the plugin's `bin/` on the Bash tool's `PATH`. The `open-asks` hook writes `export DISPATCH_HOST=claude` and `export ENVOY_CONFIG_FILE='<path>'` (the envoy.json Claude Code's rule picks, `$CLAUDE_CONFIG_DIR/envoy.json` when that is set; single-quoted,
|
|
116
|
+
| The `dispatch` CLI | `bin/dispatch` (the shim, mode 755), `dist/dispatch.js` (built from `../envoy-client/bin/dispatch.ts`), `hooks/session-hook.ts` | Claude Code puts the plugin's `bin/` on the Bash tool's `PATH`. The `open-asks` hook writes `export DISPATCH_HOST=claude` and `export ENVOY_CONFIG_FILE='<path>'` (the envoy.json Claude Code's rule picks, `$CLAUDE_CONFIG_DIR/envoy.json` when that is set; single-quoted, which Claude Code's PowerShell bridge reads for a value holding no quote; `singleQuoted` in `hooks/session-hook.ts` gives the rest) to `CLAUDE_ENV_FILE`, each unless the file already holds that line, since every `SessionStart` of a session runs it; with no workable file location (a relative `CLAUDE_CONFIG_DIR`, `ENVOY_CONFIG_FILE` or `XDG_CONFIG_HOME`) it writes no `ENVOY_CONFIG_FILE` line. For a Legion pane (`LEGION_STATE_DIR` set) it writes the pane's jj attribution overlay too, and keeps exactly one `export JJ_CONFIG='<chain>'` line in that file: the line replaces an earlier one in its place rather than joining it, since a resume or a compaction of the same session id runs the hook again over the file it already wrote and the chain it resolves can differ, where two such lines in one sourced file would leave which overlay wins to the order they were written in. When no overlay could be written the file is left as it is, no line written and none removed: `CLAUDE_ENV_FILE` is `<config>/session-env/<session id>/sessionstart-hook-<n>.sh` (read on the pinned 2.1.311), one file per session id, so a `/clear` gets a new and empty one and a line already there names this same session's own overlay, still on disk — removing it would leave the session with neither trailer, where keeping it leaves it the one it had. The command reads the file `ENVOY_CONFIG_FILE` names whenever `DISPATCH_HOST` is set. A Bash that takes nothing from that file, as a subagent's can (anthropics/claude-code#46696), has `DISPATCH_HOST` unset and the `CLAUDE_CODE_CHILD_SESSION=1` Claude Code sets in every Bash, so its `dispatch` command takes host `claude` and reads `CLAUDE_CONFIG_DIR` itself, by the plugin's own rule. The command reads the session id from `CLAUDE_CODE_SESSION_ID`, which Claude Code sets fresh in every Bash command and moves on `/clear`. The command prints the follow notice itself, once per ask. |
|
|
117
117
|
| Channel configuration | `.mcp.json`, `bin/envoy-channel.ts` | Launch is `bun ${CLAUDE_PLUGIN_ROOT}/dist/envoy-channel.js`; stdout remains pure MCP protocol. The `env` block passes through `CLAUDE_PROJECT_DIR` only — an unset `${VAR}` is substituted literally, so optional inputs are inherited, never listed. |
|
|
118
|
-
| Hook command | `hooks/hooks.json`, `hooks/session-hook.ts`, `hooks/session-title-hook.ts` | Two bundles. `dist/session-hook.js` takes its mode as an argument — `open-asks`, `dispatch-first` or `answers`, plus `answers-refresh`, which only the `answers` mode starts and hooks.json never names; an argument naming none of them throws — and each mode is a hook of its own, because Claude Code caps each hook's output at 10,000 characters. `open-asks` (every `SessionStart`) names the `dispatch` command's host and hands on the session's envoy.json in `CLAUDE_ENV_FILE`, records the current session id per Claude process, records the session's chosen title and injects the open-asks summary; always exits 0. `dispatch-first` (every `SessionStart`, and every `SubagentStart`, since a subagent runs the same `dispatch` command) injects the `dispatch-first` skill as `hookSpecificOutput.additionalContext` under the input's `hook_event_name` when Dispatch is configured, reading `${CLAUDE_PLUGIN_ROOT}/skills/dispatch-first/SKILL.md` at run time and exiting non-zero naming it when it is missing. On a resume or fork Claude Code adds a SessionStart hook's context only when the transcript does not already hold the same text (measured on 2.1.283), so a resumed session keeps one copy and one opened without the skill gets it; the hook itself checks nothing. `answers` (every `SessionStart`, and every `SubagentStart`) injects what people wrote to the session in the last 72 hours as `hookSpecificOutput.additionalContext`: `readSessionAnswers` from `@legion/envoy-client/session-answers` for the input's `session_id` (at `SubagentStart` the parent's, which `tests/answers-claude.test.ts` checks on the real binary), the list pi-envoy puts on every model request, or its failure line naming why when a read failed and no list read before has an item to show, and nothing when there is nothing to show, Dispatch answered its own `404 NOT_FOUND` "no route for …" (`isNoAnswersRoute`, which `dispatch answers` cannot read past either; the hook then writes the read's `notice`, naming that Dispatch, to stderr, one line per process), or Dispatch is not configured; a proxy's 404 page or a 200 that is no answers list is a failed read. Its bound is counted from its process's start, Bun's start-up included: it prints by `ANSWERS_BY_MS` (800 ms, `SESSION_ANSWERS_DEADLINE_MS`), or `ANSWERS_LEAST_WAIT_MS` (150 ms) after its code begins when the start-up took longer, carrying the last list read from `answers.json` in the session's Dispatch state directory (`readSessionAnswers` works it out from `env`) when Dispatch has not answered, then waits on `sessionAnswersSettled` for `ANSWERS_SETTLE_MS` (50 ms) more so a read that answered late still writes that file for the next hook, and calls `process.exit(0)` whatever happened, a malformed input included (stderr names why), so nothing still in flight holds it. Dispatch is resolved with `commands: "kept"` first; a fresh process has kept no `!` command's outcome, so when that resolution is off with an error the mode starts the refresher: this bundle again (`process.execPath`, `Bun.main`) in mode `answers-refresh` with the session id and project directory, detached (its own session, so a signal to the hook's process group does not reach it), stdio ignored (so it holds none of the hook's pipes) and `unref()`ed, unless `answers-refresh.lock` in the session's state directory names a live refresher (its pid; a lock older than 30 s, or whose pid is gone, is stale). The refresher takes that lock (`wx`; a second one exits at once, so a burst of `SubagentStart` hooks runs the command once), resolves Dispatch with commands run, so `envoy-config`'s run of the command ends in its own process with its own clean-up (the group killed at the 10 s timeout, the stdout file's directory removed), reads with a 12 s deadline (which writes `answers.json`), writes `{finished_at, dispatch_url, text, notice}` to `answers-refresh.json` and removes the lock. The hook polls that file every 20 ms until its print deadline and carries its `text`, and reports its `notice` on stderr, when it finished after the hook started; otherwise it carries the last list read from the Dispatch URL envoy.json names now (`sessionDispatchUrl`, resolved before the wait without reading the token, passed as `readSessionAnswers`' `dispatchUrl`; a list another Dispatch answered never stands in) or the failure line naming that the command had not finished, and exits, so the next hook carries the list the refresher read. Its `timeout` is 5. Measured with Dispatch hanging: about 870 ms wall from spawn to exit; healthy, about 220 ms. `dist/session-title-hook.js` (every `UserPromptSubmit`, no argument) records the payload's `session_title` — the name a person gave the session, which Claude Code hands a hook and the plugin nowhere else — under `${CLAUDE_PLUGIN_DATA}/titles/`, and removes that file when the field is absent or blank, which is how a rename is undone. It writes nothing to stdout and exits 0 whatever happens, a payload that is not JSON included (`open-asks` and `dispatch-first` still fail loudly on one), because Claude Code adds a `UserPromptSubmit` hook's stdout to the model's context and reads its exit code 2 as a block of the prompt, and a cosmetic title may do neither; a failure goes to stderr. Claude Code blocks the user's prompt on it, so it is a bundle apart from the other two and keeps zod, the Dispatch client and `session-settings` out of its import graph (`src/session-state.ts` holds the file helpers for that reason, `src/session-identity.ts` imports zod and is not reachable from here): 5 kB and ~68 ms against the shared bundle's 811 kB and ~165 ms, the latter all but 3 ms of it bare `bun` start-up. It checks the payload itself — a non-empty string `session_id`, an optional string `session_title` — rather than carry a schema library. Its `timeout` is 5, not the 20 the `open-asks` and `dispatch-first` hooks carry. |
|
|
118
|
+
| Hook command | `hooks/hooks.json`, `hooks/session-hook.ts`, `hooks/session-title-hook.ts` | Two bundles. `dist/session-hook.js` takes its mode as an argument — `open-asks`, `dispatch-first` or `answers`, plus `answers-refresh`, which only the `answers` mode starts and hooks.json never names; an argument naming none of them throws — and each mode is a hook of its own, because Claude Code caps each hook's output at 10,000 characters. `open-asks` (every `SessionStart`) names the `dispatch` command's host and hands on the session's envoy.json in `CLAUDE_ENV_FILE`, records the current session id per Claude process, records the session's chosen title and injects the open-asks summary; always exits 0. For a Legion pane (`LEGION_STATE_DIR` set) it writes the pane's jj attribution overlay under that directory too, and keeps exactly one `export JJ_CONFIG='<chain>'` line in `CLAUDE_ENV_FILE`: the line replaces an earlier one in its place rather than joining it, since a resume or a compaction of the same session id runs the hook again over the file it already wrote and the chain it resolves can differ; when no overlay could be written the file is left as it is, no line written and none removed, since `CLAUDE_ENV_FILE` is one file per session id (`<config>/session-env/<session id>/sessionstart-hook-<n>.sh`, read on the pinned 2.1.311) — a `/clear` gets a new and empty one, and a line already there names this same session's own overlay, still on disk and still correct. `dispatch-first` (every `SessionStart`, and every `SubagentStart`, since a subagent runs the same `dispatch` command) injects the `dispatch-first` skill as `hookSpecificOutput.additionalContext` under the input's `hook_event_name` when Dispatch is configured, reading `${CLAUDE_PLUGIN_ROOT}/skills/dispatch-first/SKILL.md` at run time and exiting non-zero naming it when it is missing. On a resume or fork Claude Code adds a SessionStart hook's context only when the transcript does not already hold the same text (measured on 2.1.283), so a resumed session keeps one copy and one opened without the skill gets it; the hook itself checks nothing. `answers` (every `SessionStart`, and every `SubagentStart`) injects what people wrote to the session in the last 72 hours as `hookSpecificOutput.additionalContext`: `readSessionAnswers` from `@legion/envoy-client/session-answers` for the input's `session_id` (at `SubagentStart` the parent's, which `tests/answers-claude.test.ts` checks on the real binary), the list pi-envoy puts on every model request, or its failure line naming why when a read failed and no list read before has an item to show, and nothing when there is nothing to show, Dispatch answered its own `404 NOT_FOUND` "no route for …" (`isNoAnswersRoute`, which `dispatch answers` cannot read past either; the hook then writes the read's `notice`, naming that Dispatch, to stderr, one line per process), or Dispatch is not configured; a proxy's 404 page or a 200 that is no answers list is a failed read. Its bound is counted from its process's start, Bun's start-up included: it prints by `ANSWERS_BY_MS` (800 ms, `SESSION_ANSWERS_DEADLINE_MS`), or `ANSWERS_LEAST_WAIT_MS` (150 ms) after its code begins when the start-up took longer, carrying the last list read from `answers.json` in the session's Dispatch state directory (`readSessionAnswers` works it out from `env`) when Dispatch has not answered, then waits on `sessionAnswersSettled` for `ANSWERS_SETTLE_MS` (50 ms) more so a read that answered late still writes that file for the next hook, and calls `process.exit(0)` whatever happened, a malformed input included (stderr names why), so nothing still in flight holds it. Dispatch is resolved with `commands: "kept"` first; a fresh process has kept no `!` command's outcome, so when that resolution is off with an error the mode starts the refresher: this bundle again (`process.execPath`, `Bun.main`) in mode `answers-refresh` with the session id and project directory, detached (its own session, so a signal to the hook's process group does not reach it), stdio ignored (so it holds none of the hook's pipes) and `unref()`ed, unless `answers-refresh.lock` in the session's state directory names a live refresher (its pid; a lock older than 30 s, or whose pid is gone, is stale). The refresher takes that lock (`wx`; a second one exits at once, so a burst of `SubagentStart` hooks runs the command once), resolves Dispatch with commands run, so `envoy-config`'s run of the command ends in its own process with its own clean-up (the group killed at the 10 s timeout, the stdout file's directory removed), reads with a 12 s deadline (which writes `answers.json`), writes `{finished_at, dispatch_url, text, notice}` to `answers-refresh.json` and removes the lock. The hook polls that file every 20 ms until its print deadline and carries its `text`, and reports its `notice` on stderr, when it finished after the hook started; otherwise it carries the last list read from the Dispatch URL envoy.json names now (`sessionDispatchUrl`, resolved before the wait without reading the token, passed as `readSessionAnswers`' `dispatchUrl`; a list another Dispatch answered never stands in) or the failure line naming that the command had not finished, and exits, so the next hook carries the list the refresher read. Its `timeout` is 5. Measured with Dispatch hanging: about 870 ms wall from spawn to exit; healthy, about 220 ms. `dist/session-title-hook.js` (every `UserPromptSubmit`, no argument) records the payload's `session_title` — the name a person gave the session, which Claude Code hands a hook and the plugin nowhere else — under `${CLAUDE_PLUGIN_DATA}/titles/`, and removes that file when the field is absent or blank, which is how a rename is undone. It writes nothing to stdout and exits 0 whatever happens, a payload that is not JSON included (`open-asks` and `dispatch-first` still fail loudly on one), because Claude Code adds a `UserPromptSubmit` hook's stdout to the model's context and reads its exit code 2 as a block of the prompt, and a cosmetic title may do neither; a failure goes to stderr. Claude Code blocks the user's prompt on it, so it is a bundle apart from the other two and keeps zod, the Dispatch client and `session-settings` out of its import graph (`src/session-state.ts` holds the file helpers for that reason, `src/session-identity.ts` imports zod and is not reachable from here): 5 kB and ~68 ms against the shared bundle's 811 kB and ~165 ms, the latter all but 3 ms of it bare `bun` start-up. It checks the payload itself — a non-empty string `session_id`, an optional string `session_title` — rather than carry a schema library. Its `timeout` is 5, not the 20 the `open-asks` and `dispatch-first` hooks carry. |
|
|
119
119
|
| Real Claude Code test | `tests/answers-claude.test.ts`, `/.claude-code-pin`, `/.github/actions/install-claude-code` | The `answers` hook on the real binary: Claude Code runs with this package as `--plugin-dir`, an isolated HOME and `CLAUDE_CONFIG_DIR`, and one stand-in as its Messages endpoint (`ANTHROPIC_BASE_URL`, server-sent events as the API streams them) and Dispatch (`DISPATCH_URL`); each case reads the list off the Messages request after the moment: startup (`claude -p --session-id`), `--resume <id>`, `/compact` sent in stream-json as `legion claude-bridge` sends turns, and a subagent (the stand-in answers with an `Agent` tool call, the API name of Claude Code's Task tool). `LEGION_TEST_CLAUDE` names the binary; the test skips without it, except on GitHub Actions, where the claude-envoy job installs the release `.claude-code-pin` pins (`bun add` into the runner's temp directory). A pin bump that changes a request shape is fixed in that file. |
|
|
120
120
|
| Inbound channel server | `src/envoy-channel-server.ts`, `src/envoy-off.ts` | MCP capability, notification queue, NATS registration, session-id handoff, role persistence, rejected-frame replies, and the Envoy tools, on the envoy.json Claude Code's rule picks (`@legion/envoy-client/session-settings`); why a section is off, and that inbound messages are off, goes to stderr once as `envoy-channel: <notice>`. `runEnvoyChannelServer` chooses between that server and the two reduced modes of `src/envoy-off.ts`. With Envoy off it runs `serveEnvoyOff` instead: an MCP server that lists no tool, refuses every call with `Envoy is off for this session: <why>` (the section's notice or error), opens no NATS connection and calls no listener, and exits 0 when stdin ends or on SIGTERM, SIGINT or SIGHUP. With Envoy on and inbound messages off (`inbound.off`: no NATS server, from `envoy.natsUrls` or `ENVOY_NATS_URL`, or its seed refused), it serves the tools on `inboundOffChannelSession`: no NATS connection, nothing registered with the listener, an empty inbox, `envoy_subscribe` refused naming why, and a role `envoy_role_set` claims still persisted. |
|
|
121
121
|
| Channel gate | `src/claude-channel.ts` | Whether Claude Code loaded this server as a channel, which it never tells the server: the command line of the nearest ancestor whose program is `claude` (`claudeCommandLine`; Claude Code may run the server through a shell prefix or a `bun` shim), read from `/proc` (exact, with its stdout's terminal and its directory) or `ps` (split on whitespace), against Claude Code's own gate as of 2.1.301: a `plugin:claude-envoy@…` entry when `CLAUDE_PLUGIN_ROOT` says it runs as a plugin, any `server:` entry from a bare config; `--channels` entries before development ones; `--dangerously-load-development-channels` only in an interactive session (`asksForPrintMode`, the arguments skipped as Claude Code's scanner skips them, and a terminal on stdout where `/proc` shows it); the marketplace it is loaded from (`loadedMarketplace`: `inline` for `--plugin-dir`, else the cache's, compared after Claude Code's cache-name sanitizing); for `--channels`, `allowedChannelPlugins` (object or `plugin@marketplace` entries, each invalid one dropped alone) from the managed source that owns it (`remote-settings.json`, else `managed-settings.json` with its non-dot drop-ins; under `managedSourcesBehavior: "merge"` the highest that sets it). Not loaded, the session registers `capabilities: []`. `ENVOY_CHANNEL_LOADED=1`/`0` overrides it. A command line it cannot read, or no `claude` ancestor, counts as loaded, and so does one Node overwrote with `process.title = "claude"` (on Linux, the title alone followed by NUL padding, whatever the executable's name; elsewhere one word from a program named `node…`). |
|
package/dist/dispatch.js
CHANGED
|
@@ -15531,7 +15531,7 @@ var dispatchToolSpecs = [
|
|
|
15531
15531
|
verdict: "pass",
|
|
15532
15532
|
commit: "1111111111111111111111111111111111111111"
|
|
15533
15533
|
},
|
|
15534
|
-
description: "Record one result of one check of an issue's Verification document, and print the " + "pull-request body line it implies. The check is named by its block id, or, when two " + "Verification documents in reach share that id, by its canonical reference " + "(dispatch://KEY/artifact/verification-md#b-<id>); 409 CHECK_AMBIGUOUS names them, and an " + "id no Verification document of the issue or of an issue above it holds is 400 " + "UNKNOWN_CHECK. A pass counting any failed, skipped or excluded case contradicts itself " + "and is 400 RESULT_CONTRADICTS. The result is recorded against the check's own issue, so " + "a child's run of a parent's check appears under that check. A closed issue takes a " + "result only for a check whose when is after_merge or default_branch \u2014 the production " + "check after the merge \u2014 and refuses every other one with 409 ISSUE_CLOSED, as it refuses " + "every other write. Write the printed line into the pull-request body unchanged: it is " + "the one grammar every writer uses, and it carries no link to Dispatch. " + ISSUE_REFERENCE,
|
|
15534
|
+
description: "Record one result of one check of an issue's Verification document, and print the " + "pull-request body line it implies. The check is named by its block id, or, when two " + "Verification documents in reach share that id, by its canonical reference " + "(dispatch://KEY/artifact/verification-md#b-<id>); 409 CHECK_AMBIGUOUS names them, and an " + "id no Verification document of the issue or of an issue above it holds is 400 " + "UNKNOWN_CHECK. A pass counting any failed, skipped or excluded case contradicts itself " + "and is 400 RESULT_CONTRADICTS. Dispatch asks GitHub whether the repository of the " + "check's issue holds the commit: one it does not hold is 400 RESULT_COMMIT_UNKNOWN, " + "naming the commit and that owner/repo, so record the result after the commit is pushed. " + "A repository Dispatch cannot read records the result as before. " + "The result is recorded against the check's own issue, so " + "a child's run of a parent's check appears under that check. A closed issue takes a " + "result only for a check whose when is after_merge or default_branch \u2014 the production " + "check after the merge \u2014 and refuses every other one with 409 ISSUE_CLOSED, as it refuses " + "every other write. Write the printed line into the pull-request body unchanged: it is " + "the one grammar every writer uses, and it carries no link to Dispatch. " + ISSUE_REFERENCE,
|
|
15535
15535
|
arguments: (z) => ({
|
|
15536
15536
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
15537
15537
|
check: z.string().describe("The check block's id, or its canonical reference dispatch://KEY/artifact/verification-md#b-<id> when the id is not unique among the issue's Verification document and those of the issues above it."),
|
|
@@ -17536,7 +17536,7 @@ import { promisify } from "util";
|
|
|
17536
17536
|
// ../envoy-client/src/machine.ts
|
|
17537
17537
|
import { hostname as hostname3 } from "os";
|
|
17538
17538
|
function machineID() {
|
|
17539
|
-
return process.env["ENVOY_MACHINE_ID"] || hostname3();
|
|
17539
|
+
return process.env["ENVOY_MACHINE_ID"] || process.env["AGENTBOX_HOST"] || hostname3();
|
|
17540
17540
|
}
|
|
17541
17541
|
|
|
17542
17542
|
// ../envoy-client/src/dispatch-cwd.ts
|
|
@@ -20629,7 +20629,7 @@ import { randomUUID } from "crypto";
|
|
|
20629
20629
|
import { renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
20630
20630
|
import { homedir } from "os";
|
|
20631
20631
|
import { join as join2 } from "path";
|
|
20632
|
-
var
|
|
20632
|
+
var SESSION_ID_PATH_SEGMENT = /^[A-Za-z0-9._-]{1,128}$/;
|
|
20633
20633
|
var PRIVATE_FILE = 384;
|
|
20634
20634
|
function sessionStateRoot(env) {
|
|
20635
20635
|
const stateDir = env["DISPATCH_STATE_DIR"];
|
|
@@ -20638,7 +20638,7 @@ function sessionStateRoot(env) {
|
|
|
20638
20638
|
return join2(env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state"), "dispatch");
|
|
20639
20639
|
}
|
|
20640
20640
|
function sessionDirectory(env, sessionId) {
|
|
20641
|
-
if (!
|
|
20641
|
+
if (!SESSION_ID_PATH_SEGMENT.test(sessionId) || sessionId === "." || sessionId === "..") {
|
|
20642
20642
|
throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
|
|
20643
20643
|
}
|
|
20644
20644
|
return join2(sessionStateRoot(env), "sessions", sessionId);
|
|
@@ -22198,7 +22198,7 @@ function formatCheckResults(results) {
|
|
|
22198
22198
|
if (latest === undefined) {
|
|
22199
22199
|
return `${head}no result yet. ${check.text}`;
|
|
22200
22200
|
}
|
|
22201
|
-
return `${head}${latest.verdict} at ${latest.commit_sha
|
|
22201
|
+
return `${head}${latest.verdict} at ${latest.commit_sha} by ${latest.role}, ${latest.currency ?? "unknown"}. ${check.text}`;
|
|
22202
22202
|
});
|
|
22203
22203
|
return [`Checks of ${results.issue}:`, ...lines].join(`
|
|
22204
22204
|
`);
|
|
@@ -23945,7 +23945,7 @@ Document token: ${listed.token}`,
|
|
|
23945
23945
|
...optional
|
|
23946
23946
|
});
|
|
23947
23947
|
return {
|
|
23948
|
-
text: `Recorded ${recorded.result.verdict} for check ${recorded.result.check_block_id} of ${recorded.result.check_issue_key} at ${recorded.result.commit_sha
|
|
23948
|
+
text: `Recorded ${recorded.result.verdict} for check ${recorded.result.check_block_id} of ${recorded.result.check_issue_key} at ${recorded.result.commit_sha}, as ${recorded.result.role}.
|
|
23949
23949
|
|
|
23950
23950
|
Pull request body line:
|
|
23951
23951
|
${recorded.line}`,
|
package/dist/envoy-channel.js
CHANGED
|
@@ -37180,7 +37180,7 @@ var dispatchToolSpecs = [
|
|
|
37180
37180
|
verdict: "pass",
|
|
37181
37181
|
commit: "1111111111111111111111111111111111111111"
|
|
37182
37182
|
},
|
|
37183
|
-
description: "Record one result of one check of an issue's Verification document, and print the " + "pull-request body line it implies. The check is named by its block id, or, when two " + "Verification documents in reach share that id, by its canonical reference " + "(dispatch://KEY/artifact/verification-md#b-<id>); 409 CHECK_AMBIGUOUS names them, and an " + "id no Verification document of the issue or of an issue above it holds is 400 " + "UNKNOWN_CHECK. A pass counting any failed, skipped or excluded case contradicts itself " + "and is 400 RESULT_CONTRADICTS. The result is recorded against the check's own issue, so " + "a child's run of a parent's check appears under that check. A closed issue takes a " + "result only for a check whose when is after_merge or default_branch \u2014 the production " + "check after the merge \u2014 and refuses every other one with 409 ISSUE_CLOSED, as it refuses " + "every other write. Write the printed line into the pull-request body unchanged: it is " + "the one grammar every writer uses, and it carries no link to Dispatch. " + ISSUE_REFERENCE,
|
|
37183
|
+
description: "Record one result of one check of an issue's Verification document, and print the " + "pull-request body line it implies. The check is named by its block id, or, when two " + "Verification documents in reach share that id, by its canonical reference " + "(dispatch://KEY/artifact/verification-md#b-<id>); 409 CHECK_AMBIGUOUS names them, and an " + "id no Verification document of the issue or of an issue above it holds is 400 " + "UNKNOWN_CHECK. A pass counting any failed, skipped or excluded case contradicts itself " + "and is 400 RESULT_CONTRADICTS. Dispatch asks GitHub whether the repository of the " + "check's issue holds the commit: one it does not hold is 400 RESULT_COMMIT_UNKNOWN, " + "naming the commit and that owner/repo, so record the result after the commit is pushed. " + "A repository Dispatch cannot read records the result as before. " + "The result is recorded against the check's own issue, so " + "a child's run of a parent's check appears under that check. A closed issue takes a " + "result only for a check whose when is after_merge or default_branch \u2014 the production " + "check after the merge \u2014 and refuses every other one with 409 ISSUE_CLOSED, as it refuses " + "every other write. Write the printed line into the pull-request body unchanged: it is " + "the one grammar every writer uses, and it carries no link to Dispatch. " + ISSUE_REFERENCE,
|
|
37184
37184
|
arguments: (z) => ({
|
|
37185
37185
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
37186
37186
|
check: z.string().describe("The check block's id, or its canonical reference dispatch://KEY/artifact/verification-md#b-<id> when the id is not unique among the issue's Verification document and those of the issues above it."),
|
|
@@ -44745,7 +44745,7 @@ function normalizeEnvoyUrl(value) {
|
|
|
44745
44745
|
// ../envoy-client/src/machine.ts
|
|
44746
44746
|
import { hostname as hostname3 } from "os";
|
|
44747
44747
|
function machineID() {
|
|
44748
|
-
return process.env["ENVOY_MACHINE_ID"] || hostname3();
|
|
44748
|
+
return process.env["ENVOY_MACHINE_ID"] || process.env["AGENTBOX_HOST"] || hostname3();
|
|
44749
44749
|
}
|
|
44750
44750
|
|
|
44751
44751
|
// ../envoy-client/src/transport.ts
|
|
@@ -46450,7 +46450,7 @@ import { randomUUID } from "crypto";
|
|
|
46450
46450
|
import { renameSync, rmSync as rmSync2, writeFileSync } from "fs";
|
|
46451
46451
|
import { homedir as homedir2 } from "os";
|
|
46452
46452
|
import { join as join2 } from "path";
|
|
46453
|
-
var
|
|
46453
|
+
var SESSION_ID_PATH_SEGMENT = /^[A-Za-z0-9._-]{1,128}$/;
|
|
46454
46454
|
var PRIVATE_FILE = 384;
|
|
46455
46455
|
function sessionStateRoot(env) {
|
|
46456
46456
|
const stateDir = env["DISPATCH_STATE_DIR"];
|
|
@@ -46459,7 +46459,7 @@ function sessionStateRoot(env) {
|
|
|
46459
46459
|
return join2(env["XDG_STATE_HOME"] || join2(homedir2(), ".local", "state"), "dispatch");
|
|
46460
46460
|
}
|
|
46461
46461
|
function sessionDirectory(env, sessionId) {
|
|
46462
|
-
if (!
|
|
46462
|
+
if (!SESSION_ID_PATH_SEGMENT.test(sessionId) || sessionId === "." || sessionId === "..") {
|
|
46463
46463
|
throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
|
|
46464
46464
|
}
|
|
46465
46465
|
return join2(sessionStateRoot(env), "sessions", sessionId);
|
|
@@ -48941,7 +48941,7 @@ function formatCheckResults(results) {
|
|
|
48941
48941
|
if (latest === undefined) {
|
|
48942
48942
|
return `${head}no result yet. ${check.text}`;
|
|
48943
48943
|
}
|
|
48944
|
-
return `${head}${latest.verdict} at ${latest.commit_sha
|
|
48944
|
+
return `${head}${latest.verdict} at ${latest.commit_sha} by ${latest.role}, ${latest.currency ?? "unknown"}. ${check.text}`;
|
|
48945
48945
|
});
|
|
48946
48946
|
return [`Checks of ${results.issue}:`, ...lines].join(`
|
|
48947
48947
|
`);
|
|
@@ -50688,7 +50688,7 @@ Document token: ${listed.token}`,
|
|
|
50688
50688
|
...optional
|
|
50689
50689
|
});
|
|
50690
50690
|
return {
|
|
50691
|
-
text: `Recorded ${recorded.result.verdict} for check ${recorded.result.check_block_id} of ${recorded.result.check_issue_key} at ${recorded.result.commit_sha
|
|
50691
|
+
text: `Recorded ${recorded.result.verdict} for check ${recorded.result.check_block_id} of ${recorded.result.check_issue_key} at ${recorded.result.commit_sha}, as ${recorded.result.role}.
|
|
50692
50692
|
|
|
50693
50693
|
Pull request body line:
|
|
50694
50694
|
${recorded.line}`,
|
|
@@ -55082,9 +55082,9 @@ var import_nats2 = __toESM(require_mod4(), 1);
|
|
|
55082
55082
|
|
|
55083
55083
|
// package.json
|
|
55084
55084
|
var name = "@sjawhar/claude-envoy";
|
|
55085
|
-
var version2 = "1.
|
|
55085
|
+
var version2 = "1.7.1";
|
|
55086
55086
|
var legion = {
|
|
55087
|
-
daemonApiVersion:
|
|
55087
|
+
daemonApiVersion: 52
|
|
55088
55088
|
};
|
|
55089
55089
|
|
|
55090
55090
|
// src/channel-forwarder.ts
|
package/dist/session-hook.js
CHANGED
|
@@ -14530,7 +14530,7 @@ var dispatchToolSpecs = [
|
|
|
14530
14530
|
verdict: "pass",
|
|
14531
14531
|
commit: "1111111111111111111111111111111111111111"
|
|
14532
14532
|
},
|
|
14533
|
-
description: "Record one result of one check of an issue's Verification document, and print the " + "pull-request body line it implies. The check is named by its block id, or, when two " + "Verification documents in reach share that id, by its canonical reference " + "(dispatch://KEY/artifact/verification-md#b-<id>); 409 CHECK_AMBIGUOUS names them, and an " + "id no Verification document of the issue or of an issue above it holds is 400 " + "UNKNOWN_CHECK. A pass counting any failed, skipped or excluded case contradicts itself " + "and is 400 RESULT_CONTRADICTS. The result is recorded against the check's own issue, so " + "a child's run of a parent's check appears under that check. A closed issue takes a " + "result only for a check whose when is after_merge or default_branch \u2014 the production " + "check after the merge \u2014 and refuses every other one with 409 ISSUE_CLOSED, as it refuses " + "every other write. Write the printed line into the pull-request body unchanged: it is " + "the one grammar every writer uses, and it carries no link to Dispatch. " + ISSUE_REFERENCE,
|
|
14533
|
+
description: "Record one result of one check of an issue's Verification document, and print the " + "pull-request body line it implies. The check is named by its block id, or, when two " + "Verification documents in reach share that id, by its canonical reference " + "(dispatch://KEY/artifact/verification-md#b-<id>); 409 CHECK_AMBIGUOUS names them, and an " + "id no Verification document of the issue or of an issue above it holds is 400 " + "UNKNOWN_CHECK. A pass counting any failed, skipped or excluded case contradicts itself " + "and is 400 RESULT_CONTRADICTS. Dispatch asks GitHub whether the repository of the " + "check's issue holds the commit: one it does not hold is 400 RESULT_COMMIT_UNKNOWN, " + "naming the commit and that owner/repo, so record the result after the commit is pushed. " + "A repository Dispatch cannot read records the result as before. " + "The result is recorded against the check's own issue, so " + "a child's run of a parent's check appears under that check. A closed issue takes a " + "result only for a check whose when is after_merge or default_branch \u2014 the production " + "check after the merge \u2014 and refuses every other one with 409 ISSUE_CLOSED, as it refuses " + "every other write. Write the printed line into the pull-request body unchanged: it is " + "the one grammar every writer uses, and it carries no link to Dispatch. " + ISSUE_REFERENCE,
|
|
14534
14534
|
arguments: (z) => ({
|
|
14535
14535
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
14536
14536
|
check: z.string().describe("The check block's id, or its canonical reference dispatch://KEY/artifact/verification-md#b-<id> when the id is not unique among the issue's Verification document and those of the issues above it."),
|
|
@@ -16540,7 +16540,7 @@ import { promisify } from "util";
|
|
|
16540
16540
|
// ../envoy-client/src/machine.ts
|
|
16541
16541
|
import { hostname as hostname3 } from "os";
|
|
16542
16542
|
function machineID() {
|
|
16543
|
-
return process.env["ENVOY_MACHINE_ID"] || hostname3();
|
|
16543
|
+
return process.env["ENVOY_MACHINE_ID"] || process.env["AGENTBOX_HOST"] || hostname3();
|
|
16544
16544
|
}
|
|
16545
16545
|
|
|
16546
16546
|
// ../envoy-client/src/dispatch-cwd.ts
|
|
@@ -19633,7 +19633,7 @@ import { randomUUID } from "crypto";
|
|
|
19633
19633
|
import { renameSync, rmSync, writeFileSync } from "fs";
|
|
19634
19634
|
import { homedir } from "os";
|
|
19635
19635
|
import { join } from "path";
|
|
19636
|
-
var
|
|
19636
|
+
var SESSION_ID_PATH_SEGMENT = /^[A-Za-z0-9._-]{1,128}$/;
|
|
19637
19637
|
var PRIVATE_FILE = 384;
|
|
19638
19638
|
function sessionStateRoot(env) {
|
|
19639
19639
|
const stateDir = env["DISPATCH_STATE_DIR"];
|
|
@@ -19642,7 +19642,7 @@ function sessionStateRoot(env) {
|
|
|
19642
19642
|
return join(env["XDG_STATE_HOME"] || join(homedir(), ".local", "state"), "dispatch");
|
|
19643
19643
|
}
|
|
19644
19644
|
function sessionDirectory(env, sessionId) {
|
|
19645
|
-
if (!
|
|
19645
|
+
if (!SESSION_ID_PATH_SEGMENT.test(sessionId) || sessionId === "." || sessionId === "..") {
|
|
19646
19646
|
throw new Error(`dispatch: the session id ${JSON.stringify(sessionId)} is not 1-128 letters, digits, ".", "_" or "-"`);
|
|
19647
19647
|
}
|
|
19648
19648
|
return join(sessionStateRoot(env), "sessions", sessionId);
|
|
@@ -21202,7 +21202,7 @@ function formatCheckResults(results) {
|
|
|
21202
21202
|
if (latest === undefined) {
|
|
21203
21203
|
return `${head}no result yet. ${check.text}`;
|
|
21204
21204
|
}
|
|
21205
|
-
return `${head}${latest.verdict} at ${latest.commit_sha
|
|
21205
|
+
return `${head}${latest.verdict} at ${latest.commit_sha} by ${latest.role}, ${latest.currency ?? "unknown"}. ${check.text}`;
|
|
21206
21206
|
});
|
|
21207
21207
|
return [`Checks of ${results.issue}:`, ...lines].join(`
|
|
21208
21208
|
`);
|
|
@@ -22949,7 +22949,7 @@ Document token: ${listed.token}`,
|
|
|
22949
22949
|
...optional
|
|
22950
22950
|
});
|
|
22951
22951
|
return {
|
|
22952
|
-
text: `Recorded ${recorded.result.verdict} for check ${recorded.result.check_block_id} of ${recorded.result.check_issue_key} at ${recorded.result.commit_sha
|
|
22952
|
+
text: `Recorded ${recorded.result.verdict} for check ${recorded.result.check_block_id} of ${recorded.result.check_issue_key} at ${recorded.result.commit_sha}, as ${recorded.result.role}.
|
|
22953
22953
|
|
|
22954
22954
|
Pull request body line:
|
|
22955
22955
|
${recorded.line}`,
|
|
@@ -26589,6 +26589,119 @@ function sessionEnvoySettings(file, env, options) {
|
|
|
26589
26589
|
});
|
|
26590
26590
|
}
|
|
26591
26591
|
|
|
26592
|
+
// ../pi-shared/src/jj-attribution.ts
|
|
26593
|
+
import { realpathSync } from "fs";
|
|
26594
|
+
import { mkdir, rename, writeFile } from "fs/promises";
|
|
26595
|
+
import path2 from "path";
|
|
26596
|
+
var UUID = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
|
|
26597
|
+
var JJ_CONFIG_PATH_TIMEOUT_MS = 1e4;
|
|
26598
|
+
var TOP_LEVEL_TRANSCRIPT = new RegExp(`^\\d{4}-\\d{2}-\\d{2}T[\\d-]+Z_(${UUID})\\.jsonl$`);
|
|
26599
|
+
var ATTRIBUTION_OVERLAY = /^omp-attribution-.*\.toml$/;
|
|
26600
|
+
var ISSUE_KEY3 = /^[A-Z][A-Z0-9]{1,9}-[0-9]+$/;
|
|
26601
|
+
var FIRST_RAW_CODE = 32;
|
|
26602
|
+
var DEL_CODE = 127;
|
|
26603
|
+
function resolvedWorkspace(workspace) {
|
|
26604
|
+
if (!workspace)
|
|
26605
|
+
return;
|
|
26606
|
+
try {
|
|
26607
|
+
return realpathSync(workspace);
|
|
26608
|
+
} catch {
|
|
26609
|
+
return;
|
|
26610
|
+
}
|
|
26611
|
+
}
|
|
26612
|
+
function tomlBasicString(workspace) {
|
|
26613
|
+
for (let at = 0;at < workspace.length; at += 1) {
|
|
26614
|
+
const code = workspace.charCodeAt(at);
|
|
26615
|
+
if (code < FIRST_RAW_CODE || code === DEL_CODE)
|
|
26616
|
+
return;
|
|
26617
|
+
}
|
|
26618
|
+
return `"${workspace.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
26619
|
+
}
|
|
26620
|
+
function withoutOverlays(chain) {
|
|
26621
|
+
return chain.split(":").filter((entry) => entry && !ATTRIBUTION_OVERLAY.test(path2.basename(entry))).join(":");
|
|
26622
|
+
}
|
|
26623
|
+
async function userConfigChain(timeoutMs = JJ_CONFIG_PATH_TIMEOUT_MS, env = process.env) {
|
|
26624
|
+
try {
|
|
26625
|
+
const child = Bun.spawn(["jj", "config", "path", "--user"], {
|
|
26626
|
+
detached: true,
|
|
26627
|
+
env,
|
|
26628
|
+
stdin: "ignore",
|
|
26629
|
+
stdout: "pipe",
|
|
26630
|
+
stderr: "pipe"
|
|
26631
|
+
});
|
|
26632
|
+
const killGroup = () => {
|
|
26633
|
+
try {
|
|
26634
|
+
process.kill(-child.pid, "SIGKILL");
|
|
26635
|
+
} catch {}
|
|
26636
|
+
};
|
|
26637
|
+
const exited = child.exited.finally(killGroup);
|
|
26638
|
+
const completed = Promise.all([
|
|
26639
|
+
exited,
|
|
26640
|
+
new Response(child.stdout).text(),
|
|
26641
|
+
new Response(child.stderr).text()
|
|
26642
|
+
]);
|
|
26643
|
+
const stdout = await new Promise((resolve) => {
|
|
26644
|
+
const timer = setTimeout(() => {
|
|
26645
|
+
killGroup();
|
|
26646
|
+
resolve(undefined);
|
|
26647
|
+
}, timeoutMs);
|
|
26648
|
+
completed.then(([code, output]) => {
|
|
26649
|
+
clearTimeout(timer);
|
|
26650
|
+
resolve(code === 0 ? output : undefined);
|
|
26651
|
+
}, () => {
|
|
26652
|
+
clearTimeout(timer);
|
|
26653
|
+
resolve(undefined);
|
|
26654
|
+
});
|
|
26655
|
+
});
|
|
26656
|
+
if (stdout === undefined)
|
|
26657
|
+
return;
|
|
26658
|
+
const paths = stdout.split(`
|
|
26659
|
+
`).filter(Boolean);
|
|
26660
|
+
return paths.length > 0 ? paths.join(":") : undefined;
|
|
26661
|
+
} catch {
|
|
26662
|
+
return;
|
|
26663
|
+
}
|
|
26664
|
+
}
|
|
26665
|
+
async function exportJjSessionAttribution(sessionId, stateDir, options = {}) {
|
|
26666
|
+
const { issueKey, workspace, env = process.env, resolveBase = userConfigChain } = options;
|
|
26667
|
+
if (!SESSION_ID_PATH_SEGMENT.test(sessionId))
|
|
26668
|
+
return;
|
|
26669
|
+
const overlay = path2.join(stateDir, `omp-attribution-${sessionId}.toml`);
|
|
26670
|
+
const active = env["JJ_CONFIG"];
|
|
26671
|
+
if (active?.split(":").includes(overlay))
|
|
26672
|
+
return active;
|
|
26673
|
+
const base = (active ? withoutOverlays(active) : "") || await resolveBase();
|
|
26674
|
+
if (!base)
|
|
26675
|
+
return;
|
|
26676
|
+
const sessionTrailer = `Omp-Session: ${sessionId}`;
|
|
26677
|
+
const scopedTo = issueKey !== undefined && ISSUE_KEY3.test(issueKey) && workspace ? tomlBasicString(workspace) : undefined;
|
|
26678
|
+
const contents = `[templates]
|
|
26679
|
+
commit_trailers = '"${sessionTrailer}"'
|
|
26680
|
+
` + (scopedTo === undefined ? "" : `
|
|
26681
|
+
[[--scope]]
|
|
26682
|
+
--when.workspaces = [${scopedTo}]
|
|
26683
|
+
[--scope.templates]
|
|
26684
|
+
` + `commit_trailers = '"${sessionTrailer}\\nDispatch: ${issueKey}"'
|
|
26685
|
+
`);
|
|
26686
|
+
try {
|
|
26687
|
+
await mkdir(stateDir, { recursive: true });
|
|
26688
|
+
const tmp = `${overlay}.${process.pid}.tmp`;
|
|
26689
|
+
await writeFile(tmp, contents);
|
|
26690
|
+
await rename(tmp, overlay);
|
|
26691
|
+
const chain = `${base}:${overlay}`;
|
|
26692
|
+
env["JJ_CONFIG"] = chain;
|
|
26693
|
+
return chain;
|
|
26694
|
+
} catch {
|
|
26695
|
+
env["JJ_CONFIG"] = base;
|
|
26696
|
+
return;
|
|
26697
|
+
}
|
|
26698
|
+
}
|
|
26699
|
+
function paneAttributionOptions(env) {
|
|
26700
|
+
const issueKey = env["LEGION_ISSUE_TRAILER"];
|
|
26701
|
+
const workspace = resolvedWorkspace(env["LEGION_WORKSPACE"]);
|
|
26702
|
+
return { ...issueKey ? { issueKey } : {}, ...workspace ? { workspace } : {} };
|
|
26703
|
+
}
|
|
26704
|
+
|
|
26592
26705
|
// src/claude-session.ts
|
|
26593
26706
|
function configuredValue(value) {
|
|
26594
26707
|
return value !== undefined && value.trim().length > 0 ? value : undefined;
|
|
@@ -26612,13 +26725,13 @@ import { basename as basename2, join as join6 } from "path";
|
|
|
26612
26725
|
|
|
26613
26726
|
// src/session-state.ts
|
|
26614
26727
|
import { readFileSync as readFileSync5 } from "fs";
|
|
26615
|
-
import { mkdir, readdir, rename, rm, stat, utimes, writeFile } from "fs/promises";
|
|
26728
|
+
import { mkdir as mkdir2, readdir, rename as rename2, rm, stat, utimes, writeFile as writeFile2 } from "fs/promises";
|
|
26616
26729
|
import { dirname as dirname2, join as join5 } from "path";
|
|
26617
26730
|
async function writeAtomicStateFile(file, contents) {
|
|
26618
|
-
await
|
|
26731
|
+
await mkdir2(dirname2(file), { recursive: true, mode: 448 });
|
|
26619
26732
|
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
26620
|
-
await
|
|
26621
|
-
await
|
|
26733
|
+
await writeFile2(temporary, contents, { mode: 384 });
|
|
26734
|
+
await rename2(temporary, file);
|
|
26622
26735
|
}
|
|
26623
26736
|
async function touchStateFile(file) {
|
|
26624
26737
|
const now = new Date;
|
|
@@ -26791,23 +26904,49 @@ var RefreshOutcome = object({
|
|
|
26791
26904
|
});
|
|
26792
26905
|
var ANSWERS_TIMING_FILE = "ENVOY_ANSWERS_TIMING_FILE";
|
|
26793
26906
|
var DISPATCH_HOST_LINE = "export DISPATCH_HOST=claude";
|
|
26794
|
-
|
|
26795
|
-
|
|
26907
|
+
var JJ_CONFIG_EXPORT = "export JJ_CONFIG=";
|
|
26908
|
+
var JJ_BASE_CHAIN_TIMEOUT_MS = 3000;
|
|
26909
|
+
function singleQuoted(value) {
|
|
26910
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
26911
|
+
}
|
|
26912
|
+
function envFileText(envFile) {
|
|
26796
26913
|
try {
|
|
26797
|
-
|
|
26914
|
+
return readFileSync6(envFile, "utf8");
|
|
26798
26915
|
} catch (error) {
|
|
26799
26916
|
if (!hasErrnoCode(error, "ENOENT"))
|
|
26800
26917
|
throw error;
|
|
26918
|
+
return "";
|
|
26801
26919
|
}
|
|
26802
|
-
|
|
26803
|
-
|
|
26804
|
-
return;
|
|
26920
|
+
}
|
|
26921
|
+
function appendLine(envFile, written, line) {
|
|
26805
26922
|
const separator = written === "" || written.endsWith(`
|
|
26806
26923
|
`) ? "" : `
|
|
26807
26924
|
`;
|
|
26808
26925
|
appendFileSync(envFile, `${separator}${line}
|
|
26809
26926
|
`);
|
|
26810
26927
|
}
|
|
26928
|
+
function appendOnce(envFile, line) {
|
|
26929
|
+
const written = envFileText(envFile);
|
|
26930
|
+
if (written.split(`
|
|
26931
|
+
`).includes(line))
|
|
26932
|
+
return;
|
|
26933
|
+
appendLine(envFile, written, line);
|
|
26934
|
+
}
|
|
26935
|
+
function replaceOnce(envFile, prefix, line) {
|
|
26936
|
+
const written = envFileText(envFile);
|
|
26937
|
+
const lines = written.split(`
|
|
26938
|
+
`);
|
|
26939
|
+
const at = lines.findIndex((existing) => existing.startsWith(prefix));
|
|
26940
|
+
if (at === -1) {
|
|
26941
|
+
appendLine(envFile, written, line);
|
|
26942
|
+
return;
|
|
26943
|
+
}
|
|
26944
|
+
if (lines[at] === line)
|
|
26945
|
+
return;
|
|
26946
|
+
lines[at] = line;
|
|
26947
|
+
writeAtomically(envFile, lines.join(`
|
|
26948
|
+
`));
|
|
26949
|
+
}
|
|
26811
26950
|
async function openAsks(raw) {
|
|
26812
26951
|
const input = OpenAsksInput.parse(raw);
|
|
26813
26952
|
const envFile = process.env["CLAUDE_ENV_FILE"];
|
|
@@ -26815,7 +26954,22 @@ async function openAsks(raw) {
|
|
|
26815
26954
|
if (envFile) {
|
|
26816
26955
|
appendOnce(envFile, DISPATCH_HOST_LINE);
|
|
26817
26956
|
if (chosen.path !== null) {
|
|
26818
|
-
appendOnce(envFile, `export ENVOY_CONFIG_FILE
|
|
26957
|
+
appendOnce(envFile, `export ENVOY_CONFIG_FILE=${singleQuoted(chosen.path)}`);
|
|
26958
|
+
}
|
|
26959
|
+
const stateDir = process.env["LEGION_STATE_DIR"];
|
|
26960
|
+
if (stateDir) {
|
|
26961
|
+
try {
|
|
26962
|
+
const chain = await exportJjSessionAttribution(input.session_id, stateDir, {
|
|
26963
|
+
...paneAttributionOptions(process.env),
|
|
26964
|
+
resolveBase: () => userConfigChain(JJ_BASE_CHAIN_TIMEOUT_MS)
|
|
26965
|
+
});
|
|
26966
|
+
if (chain !== undefined) {
|
|
26967
|
+
replaceOnce(envFile, JJ_CONFIG_EXPORT, `${JJ_CONFIG_EXPORT}${singleQuoted(chain)}`);
|
|
26968
|
+
}
|
|
26969
|
+
} catch (error) {
|
|
26970
|
+
process.stderr.write(`envoy: could not export the jj attribution \u2014 ${messageFor(error)}
|
|
26971
|
+
`);
|
|
26972
|
+
}
|
|
26819
26973
|
}
|
|
26820
26974
|
}
|
|
26821
26975
|
const pluginData = process.env["CLAUDE_PLUGIN_DATA"];
|
package/hooks/session-hook.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// characters, so a long open-asks summary or answers list sharing a command with the skill could
|
|
4
4
|
// push it out.
|
|
5
5
|
//
|
|
6
|
-
// open-asks (every SessionStart: startup, resume, clear, compact, fork).
|
|
6
|
+
// open-asks (every SessionStart: startup, resume, clear, compact, fork). Four jobs:
|
|
7
7
|
// 1. Name Claude Code as the `dispatch` command's host and hand on the session's envoy.json:
|
|
8
8
|
// append `export DISPATCH_HOST=claude` and `export ENVOY_CONFIG_FILE='<path>'` (the file
|
|
9
9
|
// Claude Code's rule picks, `$CLAUDE_CONFIG_DIR/envoy.json` when that is set) to
|
|
@@ -12,12 +12,32 @@
|
|
|
12
12
|
// reads the file ENVOY_CONFIG_FILE names whenever DISPATCH_HOST is set. A subagent's Bash can
|
|
13
13
|
// lack what that file holds (anthropics/claude-code#46696); the `dispatch` command it runs then
|
|
14
14
|
// takes host `claude` from `CLAUDE_CODE_CHILD_SESSION=1` and reads `CLAUDE_CONFIG_DIR` itself.
|
|
15
|
-
// The path is single-quoted
|
|
16
|
-
// bridge
|
|
15
|
+
// The path is single-quoted (`singleQuoted`, whose doc gives what Claude Code's
|
|
16
|
+
// PowerShell bridge reads of that form). The command reads the session id from `CLAUDE_CODE_SESSION_ID`, which
|
|
17
17
|
// Claude Code sets fresh in each Bash command, so a `/clear` needs nothing more here.
|
|
18
|
-
// 2.
|
|
18
|
+
// 2. For a Legion pane (`LEGION_STATE_DIR` set), write the pane's jj attribution overlay under
|
|
19
|
+
// that directory and hand on the jj config chain it layers over: `export JJ_CONFIG='<chain>'`
|
|
20
|
+
// in the same file, so every jj commit a Bash command of the session makes carries an
|
|
21
|
+
// `Omp-Session: <id>` trailer, and a `Dispatch: <KEY>` line too for a pane whose environment
|
|
22
|
+
// names an issue key, scoped to that pane's own workspace — which of the pane's variables say
|
|
23
|
+
// so, and how, is `paneAttributionOptions`
|
|
24
|
+
// (`exportJjSessionAttribution`, `@legion/pi-shared/jj-attribution`). The
|
|
25
|
+
// hook writes it, not the claim boot: that boot is the MCP server Claude Code spawns as a
|
|
26
|
+
// child, and a child cannot change its parent's environment. This line replaces an earlier one
|
|
27
|
+
// rather than joining it: exactly one `export JJ_CONFIG=` stands however many times the hook
|
|
28
|
+
// runs, since a resume or a compaction of the same session id runs it again over the file it
|
|
29
|
+
// already wrote and the chain it resolves can differ, and two such lines in one sourced file
|
|
30
|
+
// would leave which overlay wins to the order they were written in. A session that is no
|
|
31
|
+
// Legion pane gets nothing, and an overlay that could not be written (no jj, or a jj that
|
|
32
|
+
// never answers inside its 3 s) costs the session that one export and nothing else: a line an
|
|
33
|
+
// earlier run of this same id wrote is left as it stands, since `CLAUDE_ENV_FILE` is
|
|
34
|
+
// `<config>/session-env/<session id>/sessionstart-hook-<n>.sh` on the pinned Claude Code
|
|
35
|
+
// 2.1.311 — one file per session id, so a `/clear`, which gives the session a new id, gets a
|
|
36
|
+
// new and empty one — and a line already there names this same session's own overlay, still on
|
|
37
|
+
// disk and still correct.
|
|
38
|
+
// 3. Record the CURRENT session id for this Claude process so the channel
|
|
19
39
|
// server, which keeps the id it was spawned with, can follow a `/clear`.
|
|
20
|
-
//
|
|
40
|
+
// 4. Put the session's open Dispatch asks into the model's context, after one `envoy: <warning>`
|
|
21
41
|
// line for each file the read found and did not read (a checkout's `.opencode/envoy.json`,
|
|
22
42
|
// naming its rename), as the other plugins' start notices and the `dispatch` command name it.
|
|
23
43
|
// Plain stdout becomes context; it always exits 0 so a Dispatch outage never
|
|
@@ -87,6 +107,11 @@ import {
|
|
|
87
107
|
sessionDispatchSettings,
|
|
88
108
|
sessionDispatchUrl,
|
|
89
109
|
} from "@legion/envoy-client/session-settings"
|
|
110
|
+
import {
|
|
111
|
+
exportJjSessionAttribution,
|
|
112
|
+
paneAttributionOptions,
|
|
113
|
+
userConfigChain,
|
|
114
|
+
} from "@legion/pi-shared/jj-attribution"
|
|
90
115
|
import { z } from "zod"
|
|
91
116
|
import { claudeProjectDirectory, configuredValue } from "../src/claude-session"
|
|
92
117
|
import { sessionHandoffFile, writeSessionHandoff } from "../src/session-identity"
|
|
@@ -149,20 +174,75 @@ const RefreshOutcome = z.object({
|
|
|
149
174
|
const ANSWERS_TIMING_FILE = "ENVOY_ANSWERS_TIMING_FILE"
|
|
150
175
|
|
|
151
176
|
const DISPATCH_HOST_LINE = "export DISPATCH_HOST=claude"
|
|
177
|
+
/** What an overlay export line starts with: the one line of the env file this hook replaces
|
|
178
|
+
* rather than adds to. */
|
|
179
|
+
const JJ_CONFIG_EXPORT = "export JJ_CONFIG="
|
|
180
|
+
/** The bound on resolving jj's own user config chain for the overlay, inside the 20 s hooks.json
|
|
181
|
+
* gives this hook: 3 s of jj and then 3 s of the Dispatch read (`OPEN_ASKS_TIMEOUT_MS`) still
|
|
182
|
+
* leave the session its summary. Not `JJ_CONFIG_PATH_TIMEOUT_MS`, which is the Oh My Pi lane's
|
|
183
|
+
* bound inside the 30 s that harness gives its session-start handler. A jj that never answers
|
|
184
|
+
* costs the session the export and nothing else. */
|
|
185
|
+
const JJ_BASE_CHAIN_TIMEOUT_MS = 3_000
|
|
152
186
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
187
|
+
/** `value` as a single-quoted shell word: Claude Code sources this file before every bash command
|
|
188
|
+
* of the session, so a path or a config chain holding a space, a quote or a `$` must reach the
|
|
189
|
+
* shell as one word. A single quote inside the value ends the quoting, is passed as an escaped
|
|
190
|
+
* quote of its own, and opens it again. Claude Code's PowerShell bridge reads the single-quoted
|
|
191
|
+
* form of a value holding no quote; the `'\''` escape is POSIX `sh`'s, and that bridge's value
|
|
192
|
+
* grammar, read on the pinned 2.1.311, does not take it, so a line carrying one is no line the
|
|
193
|
+
* bridge can read — which costs the whole file there, and costs a Legion pane nothing, since
|
|
194
|
+
* Legion panes are not on Windows. */
|
|
195
|
+
function singleQuoted(value: string): string {
|
|
196
|
+
return `'${value.replaceAll("'", "'\\''")}'`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** What the env file holds, and "" when it does not exist yet: Claude Code names the file before
|
|
200
|
+
* any hook has written it. */
|
|
201
|
+
function envFileText(envFile: string): string {
|
|
156
202
|
try {
|
|
157
|
-
|
|
203
|
+
return readFileSync(envFile, "utf8")
|
|
158
204
|
} catch (error) {
|
|
159
205
|
if (!hasErrnoCode(error, "ENOENT")) throw error
|
|
206
|
+
return ""
|
|
160
207
|
}
|
|
161
|
-
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Appends `line` to the env file as a line of its own, `written` being what `envFileText` read:
|
|
211
|
+
* a file not ending in a newline gets one first, so the line never joins the last one. */
|
|
212
|
+
function appendLine(envFile: string, written: string, line: string): void {
|
|
162
213
|
const separator = written === "" || written.endsWith("\n") ? "" : "\n"
|
|
163
214
|
appendFileSync(envFile, `${separator}${line}\n`)
|
|
164
215
|
}
|
|
165
216
|
|
|
217
|
+
/** Appends `line` to the env file, once: every SessionStart of the session runs this. */
|
|
218
|
+
function appendOnce(envFile: string, line: string): void {
|
|
219
|
+
const written = envFileText(envFile)
|
|
220
|
+
if (written.split("\n").includes(line)) return
|
|
221
|
+
appendLine(envFile, written, line)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Writes `line` to the env file as the only line starting with `prefix`, replacing an earlier
|
|
225
|
+
* one in its place and appending when there is none: a resume or a compaction of the same
|
|
226
|
+
* session id runs the hook again over the file it already wrote, and the chain it resolves can
|
|
227
|
+
* differ, where two `export JJ_CONFIG=` lines in one sourced file would leave which overlay wins
|
|
228
|
+
* to the order they were written in. Every other line keeps its text and its place.
|
|
229
|
+
* A replacement is written through a temporary file and a rename, so a process that dies
|
|
230
|
+
* mid-write leaves the shell the whole old file, never a truncated one; with no earlier line it
|
|
231
|
+
* appends, as `appendOnce` does (`appendLine`), rather than rewriting what another hook may be
|
|
232
|
+
* appending to. */
|
|
233
|
+
function replaceOnce(envFile: string, prefix: string, line: string): void {
|
|
234
|
+
const written = envFileText(envFile)
|
|
235
|
+
const lines = written.split("\n")
|
|
236
|
+
const at = lines.findIndex((existing) => existing.startsWith(prefix))
|
|
237
|
+
if (at === -1) {
|
|
238
|
+
appendLine(envFile, written, line)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (lines[at] === line) return
|
|
242
|
+
lines[at] = line
|
|
243
|
+
writeAtomically(envFile, lines.join("\n"))
|
|
244
|
+
}
|
|
245
|
+
|
|
166
246
|
async function openAsks(raw: unknown): Promise<void> {
|
|
167
247
|
const input = OpenAsksInput.parse(raw)
|
|
168
248
|
const envFile = process.env["CLAUDE_ENV_FILE"]
|
|
@@ -172,7 +252,34 @@ async function openAsks(raw: unknown): Promise<void> {
|
|
|
172
252
|
if (envFile) {
|
|
173
253
|
appendOnce(envFile, DISPATCH_HOST_LINE)
|
|
174
254
|
if (chosen.path !== null) {
|
|
175
|
-
appendOnce(envFile, `export ENVOY_CONFIG_FILE
|
|
255
|
+
appendOnce(envFile, `export ENVOY_CONFIG_FILE=${singleQuoted(chosen.path)}`)
|
|
256
|
+
}
|
|
257
|
+
const stateDir = process.env["LEGION_STATE_DIR"]
|
|
258
|
+
if (stateDir) {
|
|
259
|
+
try {
|
|
260
|
+
// The pane's own key and workspace, by the rule both harnesses read those two variables
|
|
261
|
+
// by (`paneAttributionOptions`).
|
|
262
|
+
const chain = await exportJjSessionAttribution(input.session_id, stateDir, {
|
|
263
|
+
...paneAttributionOptions(process.env),
|
|
264
|
+
resolveBase: () => userConfigChain(JJ_BASE_CHAIN_TIMEOUT_MS),
|
|
265
|
+
})
|
|
266
|
+
// An overlay that could not be written leaves the file as it is: no line written, and
|
|
267
|
+
// none removed. `CLAUDE_ENV_FILE` is
|
|
268
|
+
// `<config>/session-env/<session id>/sessionstart-hook-<n>.sh` on the pinned Claude Code
|
|
269
|
+
// 2.1.311, so the file belongs to this session id alone: an `export JJ_CONFIG=` line
|
|
270
|
+
// already in it was written by an earlier run of this same id — a resume or a compaction
|
|
271
|
+
// — and names this same session's own overlay, still on disk and still correct. Removing
|
|
272
|
+
// it after a jj that did not answer would leave the session with neither trailer, where
|
|
273
|
+
// keeping it leaves the session the trailer it already had.
|
|
274
|
+
if (chain !== undefined) {
|
|
275
|
+
replaceOnce(envFile, JJ_CONFIG_EXPORT, `${JJ_CONFIG_EXPORT}${singleQuoted(chain)}`)
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
// The overlay writer swallows its own failures, but replacing a line writes a temporary
|
|
279
|
+
// file beside the env file, which needs its directory writable where appending
|
|
280
|
+
// above did not: attribution must cost the session neither its exports nor its summary.
|
|
281
|
+
process.stderr.write(`envoy: could not export the jj attribution — ${messageFor(error)}\n`)
|
|
282
|
+
}
|
|
176
283
|
}
|
|
177
284
|
}
|
|
178
285
|
const pluginData = process.env["CLAUDE_PLUGIN_DATA"]
|
|
@@ -48,7 +48,7 @@ Read each by its `skill://` link: a read by path stops at 300 lines.
|
|
|
48
48
|
|
|
49
49
|
## Every role pushes its own commits
|
|
50
50
|
|
|
51
|
-
- Before a push, read `jj -R "$LEGION_WORKSPACE" log -r '
|
|
51
|
+
- Before a push, run the identity check (`skill://legion-worker/references/default-branch-check.md`) and then, with the `$default` it set, read your branch's own commit messages: `jj -R "$LEGION_WORKSPACE" log -r "${default}@origin..@" -T 'description ++ "\n"'` — earlier phases' commits in it are expected; never `ancestors(@, 5)`, which stops at five where one measurement found branches of 12 and 28 commits, and never `description.first_line()`, which shows no trailer. Invent no trailer of your own: the pane's jj appends `Omp-Session:` to every commit the pane's shell makes, in any jj repository, and, for a repository the deployment names, the issue line `Dispatch: <KEY>` beside it, that one scoped to the pane's own workspace. A jj repository you clone or create under that workspace is stamped like the workspace, so a commit you make in one carries this deployment's issue key: clone a repository you will push elsewhere outside `$LEGION_WORKSPACE`, and push no commit made under it to a repository that takes no issue key. Three commits carry neither — one `git` made in the colocated workspace, one another process made outside the pane's shell, and, on the Claude harness, one a subagent's bash made, which may not carry the session's environment file (anthropics/claude-code#46696, which `packages/claude-envoy/hooks/session-hook.ts` records and ships a `dispatch` fallback for, though one measurement on the pinned release found those exports present there). A commit of yours that shows a line short is cured by describing it again, since the trailer is appended when a description is set. A re-describe appends the issue line only where the shell it runs in has the pane's jj config overlay, so it adds nothing in a shell that carries none — a subagent's bash, or a pane whose overlay the writer could not make. Where it adds nothing, write the line yourself: put `Dispatch: $LEGION_ISSUE_TRAILER` into the description as a trailer line of its own, and only when `LEGION_ISSUE_TRAILER` is set, since that variable is the deployment's own name for the key this repository takes, and an unset one means the repository takes no key and the line belongs in no commit of it. Look because a base branch can require the issue line in the commit that lands: a squash builds that message from the branch's commit messages, and it leaves a merge commit's message out, so the line belongs in an ordinary commit.
|
|
52
52
|
- Only work is pushed: code, red tests, a forward merge, a rewrite, and the empty commit that carries the pull request onto the commit you pushed (the bullet below). No commit message carries a line or bracket keyword that stops GitHub's workflows (`[skip ci]` and its kin), since every head needs its own checks and a base that requires a check will not merge a head whose checks never ran. Retro pushes nothing: it records what the work taught with `dispatch comment` on the issue.
|
|
53
53
|
- **After a push, read the pull request's head and check that it is your commit.** A branch and a pull request are two objects on GitHub: the push moves the branch, and GitHub then moves the pull request onto the new commit, which is what starts its checks. Twice on 2026-11-05 it did not, and the newer commit got no checks while Legion read, tested and would have merged the older one. The step applies once the branch has an open pull request, whose number your task's `Pull request:` line names (`gh pr list --repo <the issue's repository> --head legion/<KEY> --state open --json number --jq '.[0].number'` when it names none): run `gh pr view <n> --repo <the issue's repository> --json headRefOid --jq .headRefOid` about ten seconds after your push, again about ten seconds later, and a last time about thirty seconds after that, with a read that gives the pushed commit ending the step there and only the last read deciding a mismatch, and compare each answer with the commit you pushed, which `jj -R "$LEGION_WORKSPACE" log -r legion/<KEY>@origin --no-graph -T commit_id` prints in the 40 characters `headRefOid` answers in. This read is of the pull request's own head and of no check, so the wait it makes is not a wait on CI (`skill://legion-worker/references/pr-body.md#never-waiting-on-ci`). When the last read still is not your commit, carry the pull request with a commit of its own, an empty one when your work is complete and an empty one just as much when `@` holds work of yours, since a push of a commit the branch already holds moves no ref; the push section of your role prompt gives the one `jj` recipe, which leaves `@` and everything in it where it is. A new head makes every result recorded at the old head older, so record each `before_merge` check again at the new head. When that second push does not carry the pull request either, tell your architect the commit you pushed, the head each read gave and the time of each, and end the turn with a WAITING line naming that answer rather than completing: a phase completed here hands the next role a commit the pull request does not name. The push you make to answer an update task is the exception: that task has you answer the daemon and end the turn at once, so make this read in your next turn, before your phase completes. The merger pushes nothing, so this step never falls to it; it reports a mismatch it sees to the architect.
|
|
54
54
|
- A planner whose issue moved back from implementing starts on the remote tip, `jj -R "$LEGION_WORKSPACE" new legion/<KEY>@origin`, so a commit it makes never sits on the implementer's unpushed commits.
|
|
@@ -35,7 +35,7 @@ later phase keeps it current rather than replacing it:
|
|
|
35
35
|
|
|
36
36
|
**Checks:** one line per `check` block of the issue's Verification document, in the document's
|
|
37
37
|
order, each the line `dispatch result` printed when the result was recorded:
|
|
38
|
-
- Check <block id> (<when>): <pass|fail> at <the commit
|
|
38
|
+
- Check <block id> (<when>): <pass|fail> at <the commit>, recorded by <role>. <the check's first words>
|
|
39
39
|
- Check <block id> (after_merge): not yet run
|
|
40
40
|
|
|
41
41
|
**Fast-follow:** "none", or one named cleanup item in a file this pull request does not touch, and where it will land.
|
|
@@ -103,6 +103,12 @@ unit suite is not it. A unit or integration test is a regression lock, never the
|
|
|
103
103
|
check. The agent that develops the change records its results this way before the merge, and
|
|
104
104
|
whatever blocks a check is fixed, not skipped (*When no surface reaches the changed path*, below).
|
|
105
105
|
|
|
106
|
+
**Record a result after the commit is pushed.** Dispatch asks GitHub whether the repository of
|
|
107
|
+
the check's issue holds the commit, and refuses a result at one it does not hold with `400
|
|
108
|
+
RESULT_COMMIT_UNKNOWN`, naming the commit and the repository; a commit that is still only local
|
|
109
|
+
is such a commit. Where Dispatch cannot read the repository at all it stores the result as
|
|
110
|
+
before, and the result reads `unknown`.
|
|
111
|
+
|
|
106
112
|
**A check that cannot fail is not a result.** Before a check counts as evidence, show it
|
|
107
113
|
failing: the negative control is that demonstration, run on the same command with a deliberately
|
|
108
114
|
wrong input or call. A check whose pass survives empty output, a missing file or the wrong head
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/claude-envoy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Claude Code channel plugin for Legion's Envoy bus",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"AGENTS.md"
|
|
20
20
|
],
|
|
21
21
|
"legion": {
|
|
22
|
-
"daemonApiVersion":
|
|
22
|
+
"daemonApiVersion": 52
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|