@mrciphersmith/keryx 0.2.14 → 0.2.16

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.
Files changed (3) hide show
  1. package/README.md +58 -42
  2. package/dist/cli.js +486 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -93,15 +93,13 @@ it by reading files at random.
93
93
  ### What it looks like on a real repository
94
94
 
95
95
  Real output from a fresh clone of
96
- [express](https://github.com/expressjs/express) — four commands, nothing edited:
96
+ [express](https://github.com/expressjs/express) — three commands after `init`,
97
+ nothing edited:
97
98
 
98
99
  ```console
99
- $ keryx init --yes
100
- ✓ gdgraph ✓ gdctx ✓ gdwiki ✓ gdskills
101
- ✓ health ✓ testing ✓ memory ✓ tasks ✓ security
102
-
103
100
  $ keryx gdgraph build
104
101
  gdgraph build complete: 139 nodes, 153 edges
102
+ summary: .metaproject/data/gdgraph/artifacts/summary.md
105
103
 
106
104
  $ keryx gdgraph query cycles
107
105
  No cycles found.
@@ -127,6 +125,7 @@ affected graph supplies deterministically, in one command.
127
125
 
128
126
  ```text
129
127
  .metaproject/
128
+ ├── metaproject.json # the module manifest
130
129
  ├── index.md # the routing index every agent reads first
131
130
  ├── wiki/ # architecture, domain models, decisions, flows
132
131
  ├── memory/ # lessons, decisions, constraints, known mistakes
@@ -136,7 +135,8 @@ affected graph supplies deterministically, in one command.
136
135
  ├── data/gdgraph/ # graph artifacts, module map, query results
137
136
  ├── data/testing/ # test context, related tests, normalized reports
138
137
  ├── data/health/ # normalized health artifacts and trends
139
- └── flows/ # task flows with frozen acceptance criteria
138
+ ├── flows/ # task flows with frozen acceptance criteria
139
+ └── … # per-module config, hooks, templates, dashboard
140
140
  ```
141
141
 
142
142
  All Markdown and JSON. All diffable. All yours. And readable as a dashboard when
@@ -182,31 +182,40 @@ of guessing — the same `ask` the policy engine raises for a guarded action:
182
182
 
183
183
  What is in it today:
184
184
 
185
- - **Provider-neutral loop.** Anthropic, Ollama, OpenRouter and Grok, plus an
185
+ - **Provider-neutral loop.** Anthropic, Ollama, and any OpenAI-compatible
186
+ gateway — OpenRouter, DeepSeek, Z.AI, Cerebras, Groq, Moonshot, Grok — plus an
186
187
  offline fake provider for deterministic runs. Swapping the model does not
187
188
  change the loop, the tools or the policy.
188
- - **Durable sessions, per project.** Append-only event log on disk, resume across
189
- a process restart, branching, and context compaction that keeps the archive.
190
- `/resume`, `/compact`, `/new` and `keryx sessions list|export`.
189
+ - **Durable sessions, per project.** JSONL transcripts on disk, resume across a
190
+ process restart, and context compaction that keeps the full archive.
191
+ `/resume`, `/compact`, `/new`, and `keryx sessions list|fork|export` — `fork`
192
+ branches a conversation into a new session that keeps its ancestry, without
193
+ editing a transcript by hand.
191
194
  - **A policy engine with three answers, not two.** `allow`, `ask`, `deny` over
192
- paths, commands, tools, network and resources. Filesystem mutation is
193
- path-checked, security-scanned, approval-bound and recorded as evidence.
195
+ seven risk classes — read, write, shell, network, credential, delegate,
196
+ destructive with path and command rules underneath. Shell and destructive
197
+ actions are default-deny and need an explicit approval before they run.
194
198
  - **Kernel-enforced containment underneath.** The OS sandbox sits *below* the
195
199
  policy engine — Seatbelt on macOS, bubblewrap on Linux — with network off/on,
196
200
  and on macOS a loopback domain allowlist, credential masking behind a per-run
197
- sentinel, and opt-in TLS termination. It fails closed when a launcher or a
198
- posture is missing rather than quietly doing less.
201
+ sentinel, and TLS termination where masking requires it. It fails closed when a
202
+ launcher or a posture is missing rather than quietly doing less.
199
203
  - **Child agents with budgets.** Dispatch over the canonical
200
204
  `subagent-dispatch`/`subagent-result` contracts, token budgets per child,
201
- bounded parallel scheduling, and a fleet monitor (`keryx agents monitor`).
202
- - **Completion you can audit.** An evidence ledger backs the completion gate: a
205
+ bounded parallel scheduling, and an offline fleet report over a recorded event
206
+ log (`keryx agents monitor <events-file>`).
207
+ - **Completion you can audit.** The completion gate blocks on missing evidence: a
203
208
  run that cannot produce the evidence its flow requires does not get to claim
204
209
  it finished.
205
- - **Deterministic replay.** Recorded provider and tool fixtures replay a run with
206
- no network and no mutation, and report where the state transitions diverge.
207
- - **Four doors, one loop.** The CLI (`keryx harness run|exec|extension|wave`),
208
- JSONL/RPC, the TUI, and the loopback HTTP entry (`keryx serve`) all drive the
209
- same execution loop and the same session state.
210
+ - **Four doors.** The CLI (`keryx harness run|exec|extension|wave|replay`), JSONL/RPC
211
+ and the loopback HTTP entry (`keryx serve`) share one execution loop; the
212
+ interactive TUI runs its own on the same tool registry and the same policy.
213
+ - **A record you can check.** `keryx harness run --record` writes a run's
214
+ recomputable hash surface and `keryx harness replay` validates a fixture
215
+ against it, naming the diverging field when one moves.
216
+
217
+ The full tour — including what the harness does *not* do yet — is in
218
+ [the harness page](docs/docs/harness.md).
210
219
 
211
220
  Provider-neutral means what it says — the same loop, the same tool registry and
212
221
  the same policy, with the model swapped out from under it:
@@ -226,7 +235,7 @@ Grouped by what you are trying to do, not by internal module layout.
226
235
  **Understand the codebase**
227
236
 
228
237
  - **gdgraph** — language-aware dependency graph for TypeScript/JavaScript, Java
229
- (Maven/Gradle) and Python: cycle and orphan queries, concept and symbol lookup,
238
+ (Maven/Gradle) and Python: cycle and orphan queries, file and symbol search,
230
239
  shortest paths, affected-set blast radius, PageRank repo map, and an optional
231
240
  tree-sitter symbol/call graph.
232
241
  - **gdwiki** — a Markdown architecture wiki with hierarchical indexes, link
@@ -237,7 +246,7 @@ Grouped by what you are trying to do, not by internal module layout.
237
246
  **Preserve knowledge**
238
247
 
239
248
  - **memory** — long-term project memory with indexing, lexical search, dedup and
240
- bitemporal validity, so a lesson learned once stays learned.
249
+ as-of validity queries, so a lesson learned once stays learned.
241
250
  - **gdskills** — bundled and project-generated agent skills with routing,
242
251
  verification, learning from reviews, and export to different agent runtimes.
243
252
 
@@ -246,9 +255,11 @@ Grouped by what you are trying to do, not by internal module layout.
246
255
  - **testing** — testing context, related-test selection, changed-scope runs, and
247
256
  an opt-in coverage-map Test Impact Analysis.
248
257
  - **health** — normalized reports from TypeScript, tests, audit, complexity,
249
- coverage and lint (optional SonarQube), plus a quality gate and trends.
250
- - **review** — managed review packages under `.metaproject/reviews/`, standalone
251
- or attached to a flow, so review findings become durable project artifacts.
258
+ coverage and lint (optional SonarQube issue import), plus a quality gate and
259
+ trends.
260
+ - **review** managed review packages, standalone under `.metaproject/reviews/`
261
+ or inside the flow package when attached to a flow, so review findings become
262
+ durable project artifacts.
252
263
 
253
264
  **Operate agents**
254
265
 
@@ -258,13 +269,13 @@ Grouped by what you are trying to do, not by internal module layout.
258
269
  scanning, redaction, and a policy gate at agent write seams, with a committed
259
270
  evaluation corpus.
260
271
  - **mcp** — an opt-in [Model Context Protocol](https://modelcontextprotocol.io)
261
- server exposing read-only module services to agents.
272
+ server exposing read-only module services to agents, plus one report-writing
273
+ security scan.
262
274
 
263
275
  **Run agents inside boundaries**
264
276
 
265
277
  - **harness** — the first-party agent runtime described above: provider-neutral
266
- loop, durable sessions, policy engine, child agents, evidence-gated completion,
267
- deterministic replay.
278
+ loop, durable sessions, policy engine, child agents, evidence-gated completion.
268
279
  - **sandbox** — kernel-enforced containment under the policy engine
269
280
  (`keryx harness exec`), with filesystem boundaries, network posture and, on
270
281
  macOS, a domain allowlist with credential masking.
@@ -301,7 +312,7 @@ Alternative install paths — the managed installer (`~/.keryx` with a wrapper i
301
312
  `~/.local/bin`), project-local installs, and running from source — are in the
302
313
  [onboarding guide](docs/docs/onboarding.md).
303
314
 
304
- Bare `keryx` prints the CLI surface; `keryx shell` starts the agent harness
315
+ Bare `keryx` prints the main commands; `keryx shell` starts the agent harness
305
316
  described [above](#the-agent-harness).
306
317
 
307
318
  ## Agent integrations
@@ -323,18 +334,21 @@ keryx agents bootstrap install --runtime claude
323
334
  keryx mcp install --runtime cursor # opt-in read-only MCP server
324
335
  ```
325
336
 
337
+ Each of those commands has its own `--runtime` vocabulary — run
338
+ `keryx <command> --help` for the values it accepts.
339
+
326
340
  ## Requirements and compatibility
327
341
 
328
342
  | Requirement | Status |
329
343
  |-------------|--------|
330
344
  | Bun | >= 1.1.0 |
331
- | Git | Required |
345
+ | Git | Required for hooks, `--changed` scopes and the managed installer; the core runs without it |
332
346
  | ripgrep | Required only for `keryx ctx rg` and the agent's `search_code` tool |
333
347
  | Model provider credential | Required only for the optional AI commands below |
334
348
  | macOS | Full support, including the complete policy sandbox |
335
349
  | Linux | Full core support; filesystem containment and network on/off (needs `bubblewrap`) |
336
350
  | Windows | Core CLI is not verified in CI; the OS sandbox is macOS/Linux only |
337
- | CI | Ubuntu and macOS runners on every push |
351
+ | CI | Ubuntu and macOS runners on every pull request and every push to `main` |
338
352
 
339
353
  ## Optional AI features
340
354
 
@@ -365,7 +379,7 @@ graph falls back to its deterministic resolver when a grammar is absent.
365
379
  | Domain allowlist is macOS-only | Domain-level egress policy, credential masking and TLS termination refuse to run on Linux rather than silently doing less | Filesystem containment and network on/off work on both |
366
380
  | No bundled embedding runtime | No semantic ranking in memory search | Lexical memory search remains fully available |
367
381
  | ripgrep is external | `keryx ctx rg` needs `rg` on `PATH` | Install ripgrep, or let the agent read files directly |
368
- | Model commands need a credential | The five commands above exit non-zero without one | Everything else runs deterministically offline |
382
+ | Model commands need a credential | Four of the five commands above exit non-zero without one; `wiki enrich` exits `0` and marks the affected pages skipped | Everything else runs deterministically offline |
369
383
 
370
384
  Full detail, including known defects and platform caveats:
371
385
  [limitations](docs/docs/limitations.md).
@@ -383,11 +397,12 @@ keryx serve # bind 127.0.0.1 and listen
383
397
  keryx serve status --json # configuration state
384
398
  ```
385
399
 
386
- It is off unless you configure it, binds loopback unless you pass
387
- `--acknowledge-non-loopback`, and authenticates *before* routing, so an
388
- unauthenticated caller cannot tell a known path from an unknown one. The remote
389
- policy profile may never be weaker than the local one it is compared on every
390
- turn and a weaker profile is refused. See
400
+ It is off unless you configure it, and moving it off loopback takes a `--bind`
401
+ address plus an explicit acknowledgement in *both* the stored config and the
402
+ command line either one alone refuses to start. It authenticates *before*
403
+ routing, so an unauthenticated caller cannot tell a known path from an unknown
404
+ one. The remote policy profile may never be weaker than the local one — it is
405
+ compared at startup, and a weaker profile refuses to bind at all. See
391
406
  [drive keryx remotely](docs/docs/guides/drive-keryx-remotely.md) for routes and
392
407
  setup.
393
408
 
@@ -405,7 +420,9 @@ keryx dashboard build
405
420
 
406
421
  `keryx health gate --strict-warn` fails a job on the normalized health gate
407
422
  instead of parsing raw linter/test logs, and `keryx security eval --corpus all`
408
- fails on any detector breaching its committed false-negative threshold. See
423
+ fails on any detector breaching its committed false-negative threshold — from a
424
+ repository checkout, since the evaluation corpus is not shipped in the npm
425
+ package. See
409
426
  [run keryx in CI](docs/docs/guides/run-in-ci.md).
410
427
 
411
428
  ## Documentation
@@ -415,13 +432,12 @@ Full documentation site: **<https://mrciphersmith.github.io/keryx/>**
415
432
  - **[Onboarding](docs/docs/onboarding.md)** — install paths, first-run walkthrough, the build loop.
416
433
  - **[Architecture](docs/docs/architecture.md)** — the four-layer pattern, invariants, cross-module data flows.
417
434
  - **[Module reference](docs/docs/modules.md)** — one section per module: purpose, CLI surface, mechanics, data paths.
418
- - **[CLI reference](docs/docs/cli-reference.md)** — every command, subcommand, flag and exit code.
435
+ - **[CLI reference](docs/docs/cli-reference.md)** — the command surface: subcommands, flags and exit codes.
419
436
  - **[Workspace & lifecycle](docs/docs/workspace-and-lifecycle.md)** — the `.metaproject/` contract and `init`/`update` lifecycle.
420
437
  - **[Limitations](docs/docs/limitations.md)** — known gaps, platform caveats, and what to do instead.
421
438
  - **[Changelog](CHANGELOG.md)** — what has landed since `v0.1.0`.
422
439
 
423
- Run `keryx <command> --help` (or `keryx` with no arguments) for the live command
424
- surface.
440
+ Run `keryx <command> --help` for the live flag surface of any command.
425
441
 
426
442
  ## Local development
427
443
 
package/dist/cli.js CHANGED
@@ -20056,6 +20056,7 @@ async function initCommand(args) {
20056
20056
  if (enableMcp) {
20057
20057
  statusLine("mcp", true, "Model Context Protocol server (opt-in)");
20058
20058
  }
20059
+ const gitHooksRoot = await resolveGitHooksRoot(projectRoot);
20059
20060
  const hookLines = [];
20060
20061
  if (enableGdgraph) {
20061
20062
  hookLines.push(["gdgraph post-commit", enableGdgraphHook]);
@@ -20072,13 +20073,23 @@ async function initCommand(args) {
20072
20073
  }
20073
20074
  if (enableSecurity) {
20074
20075
  hookLines.push(["security pre-push", enableSecurityPrePushHook]);
20075
- hookLines.push(["security agent (.claude)", enableSecurityAgentHook]);
20076
20076
  }
20077
- if (hookLines.length > 0) {
20077
+ const agentHookLines = enableSecurity ? [["security agent (.claude)", enableSecurityAgentHook]] : [];
20078
+ if (hookLines.length > 0 || agentHookLines.length > 0) {
20078
20079
  heading("Git hooks");
20079
20080
  for (const [label, on] of hookLines) {
20081
+ if (gitHooksRoot) {
20082
+ statusLine(label, on);
20083
+ } else {
20084
+ statusLine(label, false, "skipped - not a git repository");
20085
+ }
20086
+ }
20087
+ for (const [label, on] of agentHookLines) {
20080
20088
  statusLine(label, on);
20081
20089
  }
20090
+ if (!gitHooksRoot && hookLines.length > 0) {
20091
+ console.log(` ${style.dim("Run")} ${style.cyan("git init")} ${style.dim("and then")} ${style.cyan("keryx init")} ${style.dim("again to install them.")}`);
20092
+ }
20082
20093
  }
20083
20094
  const wroteSandboxPolicy = writeProjectSandboxPolicySkeletonIfMissing(projectRoot);
20084
20095
  if (wroteSandboxPolicy) {
@@ -22490,6 +22501,9 @@ function parseRuntimeArg2(args) {
22490
22501
  const value = optionValue(args, "--runtime") ?? "claude";
22491
22502
  return value.split(",").map((s) => s.trim()).filter(Boolean);
22492
22503
  }
22504
+ function isDryRun(args) {
22505
+ return args.includes("--dry-run");
22506
+ }
22493
22507
  async function readSettings4(file) {
22494
22508
  if (!await pathExists(file))
22495
22509
  return {};
@@ -22526,6 +22540,7 @@ function reportUnsupported2(ids) {
22526
22540
  }
22527
22541
  async function handleInstall(args) {
22528
22542
  const cwd = process.cwd();
22543
+ const dryRun = isDryRun(args);
22529
22544
  const { runtimes, unknown, unsupported } = resolveOrientRuntimes(parseRuntimeArg2(args));
22530
22545
  if (unknown.length > 0) {
22531
22546
  console.error(`Unknown runtime(s): ${unknown.join(", ")}`);
@@ -22533,35 +22548,47 @@ async function handleInstall(args) {
22533
22548
  process.exitCode = 1;
22534
22549
  return;
22535
22550
  }
22536
- console.log("# keryx orientation injector installed");
22551
+ console.log(`# keryx orientation injector ${dryRun ? "install \u2014 dry run, nothing written" : "installed"}`);
22537
22552
  console.log("");
22538
22553
  console.log("injects: compact code-graph map + wiki index + freshness at turn start");
22539
22554
  console.log("");
22540
22555
  for (const runtime of runtimes) {
22556
+ const target = path65.relative(cwd, runtime.locate(cwd));
22557
+ if (dryRun) {
22558
+ console.log(` \xB7 ${runtime.id} -> would write ${target}`);
22559
+ continue;
22560
+ }
22541
22561
  const errors = await installOne(cwd, runtime);
22542
22562
  if (errors.length > 0) {
22543
22563
  for (const e of errors)
22544
22564
  console.error(` \u2717 ${e}`);
22545
22565
  process.exitCode = 1;
22546
22566
  } else {
22547
- console.log(` \u2713 ${runtime.id} -> ${path65.relative(cwd, runtime.locate(cwd))}`);
22567
+ console.log(` \u2713 ${runtime.id} -> ${target}`);
22548
22568
  }
22549
22569
  }
22550
22570
  reportUnsupported2(unsupported);
22551
22571
  }
22552
22572
  async function handleUninstall(args) {
22553
22573
  const cwd = process.cwd();
22574
+ const dryRun = isDryRun(args);
22554
22575
  const { runtimes, unknown, unsupported } = resolveOrientRuntimes(parseRuntimeArg2(args));
22555
22576
  if (unknown.length > 0) {
22556
22577
  console.error(`Unknown runtime(s): ${unknown.join(", ")}`);
22557
22578
  process.exitCode = 1;
22558
22579
  return;
22559
22580
  }
22560
- console.log("# keryx orientation injector uninstall");
22581
+ console.log(`# keryx orientation injector uninstall${dryRun ? " \u2014 dry run, nothing written" : ""}`);
22561
22582
  console.log("");
22562
22583
  for (const runtime of runtimes) {
22584
+ const target = path65.relative(cwd, runtime.locate(cwd));
22585
+ if (dryRun) {
22586
+ const present = await pathExists(runtime.locate(cwd));
22587
+ console.log(` \xB7 ${runtime.id} ${present ? `-> would strip ${target}` : "nothing to remove"}`);
22588
+ continue;
22589
+ }
22563
22590
  const removed = await uninstallOne(cwd, runtime);
22564
- console.log(` ${removed ? "\u2713" : "\xB7"} ${runtime.id} ${removed ? `-> ${path65.relative(cwd, runtime.locate(cwd))}` : "nothing to remove"}`);
22591
+ console.log(` ${removed ? "\u2713" : "\xB7"} ${runtime.id} ${removed ? `-> ${target}` : "nothing to remove"}`);
22565
22592
  }
22566
22593
  reportUnsupported2(unsupported);
22567
22594
  }
@@ -22570,8 +22597,11 @@ function printHelp5() {
22570
22597
 
22571
22598
  Usage:
22572
22599
  keryx orient [<runtime>] emit the orientation block
22573
- keryx orient install-hook [--runtime <id|all>]
22574
- keryx orient uninstall-hook [--runtime <id|all>]
22600
+ keryx orient install-hook [--runtime <id|all>] [--dry-run]
22601
+ keryx orient uninstall-hook [--runtime <id|all>] [--dry-run]
22602
+
22603
+ Options:
22604
+ --dry-run report what would be written or stripped; change nothing
22575
22605
 
22576
22606
  Runtimes with a context-injection hook: ${orientRuntimeIds().join(", ")}
22577
22607
  (Windsurf/Zed have no context-injection hook \u2014 use their rules/memories.)
@@ -33807,7 +33837,11 @@ function printMcpHelp() {
33807
33837
  init_fs();
33808
33838
  init_json();
33809
33839
  import path111 from "path";
33810
- async function statusCommand() {
33840
+ async function statusCommand(args = []) {
33841
+ if (args.includes("--help") || args.includes("-h")) {
33842
+ printHelp14();
33843
+ return;
33844
+ }
33811
33845
  const root = path111.join(process.cwd(), ".metaproject");
33812
33846
  const manifestPath = path111.join(root, "metaproject.json");
33813
33847
  if (!await pathExists(root)) {
@@ -33836,13 +33870,73 @@ async function statusCommand() {
33836
33870
  console.log(` ${name}: ${moduleConfig.enabled ? "enabled" : "disabled"}`);
33837
33871
  }
33838
33872
  }
33873
+ function printHelp14() {
33874
+ console.log(`keryx status \u2014 whether this project has a .metaproject workspace, and which modules are on
33875
+
33876
+ Usage:
33877
+ keryx status
33878
+
33879
+ Reports the workspace root and one enabled/disabled line per module. Use
33880
+ \`keryx modules\` to toggle a module, and \`keryx init\` to create the workspace.
33881
+ `);
33882
+ }
33839
33883
 
33840
33884
  // src/commands/harness.ts
33841
- init_make_provider();
33842
- import { readFileSync as readFileSync7 } from "fs";
33885
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
33843
33886
  import path115 from "path";
33844
33887
  import { randomUUID as randomUUID5 } from "crypto";
33845
33888
 
33889
+ // src/security/harness-scan.ts
33890
+ init_guard();
33891
+ init_config();
33892
+ init_detect();
33893
+ var PERMISSIVE = {
33894
+ scan: () => ({ hasSecret: false }),
33895
+ available: false
33896
+ };
33897
+ var CATEGORY_PRECEDENCE = [
33898
+ "secret",
33899
+ "pii",
33900
+ "prompt-injection",
33901
+ "egress",
33902
+ "artifact-safety",
33903
+ "raw-retention"
33904
+ ];
33905
+ async function buildHarnessScanner(cwd) {
33906
+ let config;
33907
+ try {
33908
+ if (!await isSecurityEnabled(cwd)) {
33909
+ return PERMISSIVE;
33910
+ }
33911
+ config = await loadSecurityConfig(cwd);
33912
+ } catch {
33913
+ return PERMISSIVE;
33914
+ }
33915
+ return {
33916
+ available: true,
33917
+ scan: (content) => {
33918
+ if (content.length === 0) {
33919
+ return { hasSecret: false };
33920
+ }
33921
+ let matches2;
33922
+ try {
33923
+ matches2 = runDetectors(content, config);
33924
+ } catch {
33925
+ return { hasSecret: false, scanFailed: true };
33926
+ }
33927
+ if (matches2.length === 0) {
33928
+ return { hasSecret: false };
33929
+ }
33930
+ const present = new Set(matches2.map((m) => m.category));
33931
+ const category = CATEGORY_PRECEDENCE.find((c) => present.has(c)) ?? matches2[0]?.category ?? "unknown";
33932
+ return { hasSecret: true, category };
33933
+ }
33934
+ };
33935
+ }
33936
+
33937
+ // src/commands/harness.ts
33938
+ init_make_provider();
33939
+
33846
33940
  // src/harness/policy/profiles.ts
33847
33941
  import { createHash as createHash8 } from "crypto";
33848
33942
 
@@ -34406,7 +34500,7 @@ async function runOffline(input2, config, deps) {
34406
34500
  const executed = [];
34407
34501
  const blockerIds = [];
34408
34502
  const unresolvedRisks = [];
34409
- const scan = () => ({ hasSecret: false });
34503
+ const scan = deps.scan ?? (() => ({ hasSecret: false }));
34410
34504
  const toolNameByCall = new Map;
34411
34505
  const actionCounts = new Map;
34412
34506
  let executedToolCalls = 0;
@@ -34538,11 +34632,14 @@ async function runOffline(input2, config, deps) {
34538
34632
  blockerIds.push(`blocker:tool-${result.errorCode ?? "failed"}:${toolCallId}`);
34539
34633
  }
34540
34634
  }
34541
- const requiredGates = [];
34635
+ const requiredGates = [...deps.completionRequirements?.requiredGates ?? []];
34636
+ const requiredEvidenceRefs = [
34637
+ ...deps.completionRequirements?.requiredEvidenceRefs ?? []
34638
+ ];
34542
34639
  const gate = evaluateCompletion({
34543
34640
  runId,
34544
34641
  requiredGates,
34545
- requiredEvidenceRefs: [],
34642
+ requiredEvidenceRefs,
34546
34643
  presentEvidenceIds: uniqueInOrder2(presentEvidenceIds),
34547
34644
  undisposedBlockerIds: uniqueInOrder2(blockerIds),
34548
34645
  finalMessageEmitted
@@ -34676,6 +34773,129 @@ function earlyTermination(input2, config, deps, startedAt, reason) {
34676
34773
  };
34677
34774
  }
34678
34775
 
34776
+ // src/harness/replay/replay.ts
34777
+ var SCHEMA_VERSION5 = 1;
34778
+ function recomputeHashes(run) {
34779
+ return {
34780
+ sessionManifestHash: run.sessionManifestHash,
34781
+ eventLogHash: run.eventLogHash,
34782
+ toolRegistryHash: run.toolRegistryHash,
34783
+ transcriptHash: run.transcriptHash,
34784
+ expectedStateHash: run.expectedStateHash
34785
+ };
34786
+ }
34787
+ function buildReplayFixture(run, deps) {
34788
+ const hashes = recomputeHashes(run);
34789
+ return {
34790
+ schemaVersion: SCHEMA_VERSION5,
34791
+ fixtureId: deps.idSeq(),
34792
+ mode: "validate-log",
34793
+ sessionManifestHash: hashes.sessionManifestHash,
34794
+ eventLogHash: hashes.eventLogHash,
34795
+ toolRegistryHash: hashes.toolRegistryHash,
34796
+ transcriptHash: hashes.transcriptHash,
34797
+ expectedStateHash: hashes.expectedStateHash,
34798
+ noSideEffects: true
34799
+ };
34800
+ }
34801
+ function toRunRecord(run, meta) {
34802
+ return {
34803
+ schemaVersion: SCHEMA_VERSION5,
34804
+ runId: meta.runId,
34805
+ status: meta.status,
34806
+ recordedAt: meta.recordedAt,
34807
+ sessionManifestHash: run.sessionManifestHash,
34808
+ eventLogHash: run.eventLogHash,
34809
+ toolRegistryHash: run.toolRegistryHash,
34810
+ transcriptHash: run.transcriptHash,
34811
+ expectedStateHash: run.expectedStateHash
34812
+ };
34813
+ }
34814
+ var HASH_FIELDS = [
34815
+ "sessionManifestHash",
34816
+ "eventLogHash",
34817
+ "toolRegistryHash",
34818
+ "transcriptHash",
34819
+ "expectedStateHash"
34820
+ ];
34821
+ function nonEmptyString(value) {
34822
+ return typeof value === "string" && value.length > 0;
34823
+ }
34824
+ function hasEveryHash(record) {
34825
+ return HASH_FIELDS.every((field3) => nonEmptyString(record[field3]));
34826
+ }
34827
+ function parseRunRecord(value) {
34828
+ if (typeof value !== "object" || value === null || Array.isArray(value))
34829
+ return;
34830
+ const record = value;
34831
+ if (!hasEveryHash(record) || !nonEmptyString(record.runId))
34832
+ return;
34833
+ return {
34834
+ schemaVersion: typeof record.schemaVersion === "number" ? record.schemaVersion : SCHEMA_VERSION5,
34835
+ runId: record.runId,
34836
+ status: nonEmptyString(record.status) ? record.status : "unknown",
34837
+ recordedAt: nonEmptyString(record.recordedAt) ? record.recordedAt : "",
34838
+ sessionManifestHash: record.sessionManifestHash,
34839
+ eventLogHash: record.eventLogHash,
34840
+ toolRegistryHash: record.toolRegistryHash,
34841
+ transcriptHash: record.transcriptHash,
34842
+ expectedStateHash: record.expectedStateHash
34843
+ };
34844
+ }
34845
+ function parseReplayFixture(value) {
34846
+ if (typeof value !== "object" || value === null || Array.isArray(value))
34847
+ return;
34848
+ const record = value;
34849
+ if (!hasEveryHash(record) || !nonEmptyString(record.fixtureId))
34850
+ return;
34851
+ const mode = record.mode;
34852
+ if (mode !== "validate-log" && mode !== "simulate-recorded-results" && mode !== "isolated-re-execute") {
34853
+ return;
34854
+ }
34855
+ return {
34856
+ schemaVersion: typeof record.schemaVersion === "number" ? record.schemaVersion : SCHEMA_VERSION5,
34857
+ fixtureId: record.fixtureId,
34858
+ mode,
34859
+ sessionManifestHash: record.sessionManifestHash,
34860
+ eventLogHash: record.eventLogHash,
34861
+ toolRegistryHash: record.toolRegistryHash,
34862
+ transcriptHash: record.transcriptHash,
34863
+ expectedStateHash: record.expectedStateHash,
34864
+ noSideEffects: record.noSideEffects !== false,
34865
+ ...nonEmptyString(record.isolationProfile) ? { isolationProfile: record.isolationProfile } : {}
34866
+ };
34867
+ }
34868
+ var HASH_CHECKS = [
34869
+ { field: "sessionManifestHash", kind: "state", detail: "sessionManifestHash diverged on replay (session manifest)" },
34870
+ { field: "eventLogHash", kind: "event-order", detail: "eventLogHash diverged on replay (event order)" },
34871
+ { field: "toolRegistryHash", kind: "tool-result", detail: "toolRegistryHash diverged on replay (tool registry)" },
34872
+ { field: "transcriptHash", kind: "provider-transcript", detail: "transcriptHash diverged on replay (provider transcript)" },
34873
+ { field: "expectedStateHash", kind: "state", detail: "expectedStateHash diverged on replay (terminal state)" }
34874
+ ];
34875
+ function replayOffline(fixture, run, deps) {
34876
+ const actual = recomputeHashes(run);
34877
+ for (const check of HASH_CHECKS) {
34878
+ const expectedHash = fixture[check.field];
34879
+ const actualHash = actual[check.field];
34880
+ if (expectedHash !== actualHash) {
34881
+ return {
34882
+ ok: false,
34883
+ mismatch: {
34884
+ schemaVersion: SCHEMA_VERSION5,
34885
+ mismatchId: deps.idSeq(),
34886
+ fixtureId: fixture.fixtureId,
34887
+ kind: check.kind,
34888
+ expectedHash,
34889
+ actualHash,
34890
+ detectedAt: deps.clock(),
34891
+ detail: check.detail
34892
+ }
34893
+ };
34894
+ }
34895
+ }
34896
+ return { ok: true };
34897
+ }
34898
+
34679
34899
  // src/harness/tool/registry.ts
34680
34900
  import { createHash as createHash13 } from "crypto";
34681
34901
  function canonicalize3(value) {
@@ -35208,7 +35428,7 @@ class SandboxedProcessAdapter {
35208
35428
  }
35209
35429
  spawn(command) {
35210
35430
  const { profile, inner, platform, launcherAvailable, bwrapPath } = this.opts;
35211
- const failClosed = profile.required || (this.opts.failIfUnavailable ?? true);
35431
+ const failClosed = profile.required || profile.network === "restricted" || (this.opts.failIfUnavailable ?? true);
35212
35432
  if (profile.mode === "danger-full-access") {
35213
35433
  return inner.spawn(command);
35214
35434
  }
@@ -36026,7 +36246,8 @@ var USAGE = [
36026
36246
  " [--allowed-domains a,b] [--mask-env NAME@host] [--tls-terminate] [--mask-mode auto|manual|off] [--auto-mask]",
36027
36247
  " -- <path> [args...]",
36028
36248
  " keryx harness extension --spec <path>",
36029
- " keryx harness wave --spec <path>"
36249
+ " keryx harness wave --spec <path>",
36250
+ " keryx harness replay --record <path> [--fixture <path>] [--write-fixture <path>] [--json]"
36030
36251
  ].join(`
36031
36252
  `);
36032
36253
  function readOnlyProfile() {
@@ -36041,6 +36262,7 @@ function parseArgs2(args) {
36041
36262
  let provider = "";
36042
36263
  let model = "";
36043
36264
  let baseUrl;
36265
+ let record;
36044
36266
  const positional = [];
36045
36267
  for (let i = 1;i < args.length; i++) {
36046
36268
  const arg = args[i];
@@ -36050,6 +36272,8 @@ function parseArgs2(args) {
36050
36272
  model = args[++i] ?? "";
36051
36273
  } else if (arg === "--base-url") {
36052
36274
  baseUrl = args[++i];
36275
+ } else if (arg === "--record") {
36276
+ record = args[++i];
36053
36277
  } else if (arg !== undefined) {
36054
36278
  positional.push(arg);
36055
36279
  }
@@ -36057,6 +36281,8 @@ function parseArgs2(args) {
36057
36281
  const parsed = { provider, model, prompt: positional.join(" ") };
36058
36282
  if (baseUrl !== undefined)
36059
36283
  parsed.baseUrl = baseUrl;
36284
+ if (record !== undefined)
36285
+ parsed.record = record;
36060
36286
  return parsed;
36061
36287
  }
36062
36288
  function toStructured(result) {
@@ -36082,11 +36308,15 @@ async function harnessCommand(args, deps) {
36082
36308
  harnessWave(args, deps);
36083
36309
  return;
36084
36310
  }
36311
+ if (subcommand === "replay") {
36312
+ harnessReplay(args, deps);
36313
+ return;
36314
+ }
36085
36315
  if (subcommand !== "run") {
36086
36316
  console.log(USAGE);
36087
36317
  return;
36088
36318
  }
36089
- const { provider, model, baseUrl, prompt } = parseArgs2(args);
36319
+ const { provider, model, baseUrl, prompt, record } = parseArgs2(args);
36090
36320
  const validProviders = new Set(["fake", "anthropic", "ollama"]);
36091
36321
  if (!validProviders.has(provider) || prompt.length === 0) {
36092
36322
  console.log(USAGE);
@@ -36129,6 +36359,7 @@ async function harnessCommand(args, deps) {
36129
36359
  policyProfile: "read-only-review",
36130
36360
  limits: { maxRunSeconds: 300, maxConcurrentChildren: 1, maxToolOutputBytes: 65536, maxRetries: 1 }
36131
36361
  };
36362
+ const { scan } = await buildHarnessScanner(process.cwd());
36132
36363
  const runDeps = {
36133
36364
  provider: providerPort,
36134
36365
  toolRegistry: new ToolRegistry,
@@ -36136,12 +36367,21 @@ async function harnessCommand(args, deps) {
36136
36367
  policyProfile: readOnlyProfile(),
36137
36368
  clock,
36138
36369
  idSeq,
36139
- interactive: false
36370
+ interactive: false,
36371
+ scan
36140
36372
  };
36141
36373
  let structured;
36142
36374
  try {
36143
36375
  const result = await runOffline(input2, config, runDeps);
36144
36376
  structured = toStructured(result);
36377
+ if (record !== undefined && record.length > 0) {
36378
+ writeFileSync6(record, `${JSON.stringify(toRunRecord(result, {
36379
+ runId: result.output.runId,
36380
+ status: result.output.status,
36381
+ recordedAt: result.output.startedAt
36382
+ }), null, 2)}
36383
+ `, { encoding: "utf8", mode: 384 });
36384
+ }
36145
36385
  } catch (error) {
36146
36386
  structured = {
36147
36387
  events: [],
@@ -36152,6 +36392,112 @@ async function harnessCommand(args, deps) {
36152
36392
  }
36153
36393
  console.log(JSON.stringify(structured));
36154
36394
  }
36395
+ function parseReplayArgs(args) {
36396
+ const parsed = { json: false };
36397
+ for (let i = 1;i < args.length; i++) {
36398
+ const arg = args[i];
36399
+ if (arg === "--record") {
36400
+ parsed.record = args[++i] ?? "";
36401
+ } else if (arg === "--fixture") {
36402
+ parsed.fixture = args[++i] ?? "";
36403
+ } else if (arg === "--write-fixture") {
36404
+ parsed.writeFixture = args[++i] ?? "";
36405
+ } else if (arg === "--json") {
36406
+ parsed.json = true;
36407
+ }
36408
+ }
36409
+ return parsed;
36410
+ }
36411
+ function readJsonFile2(file) {
36412
+ let text;
36413
+ try {
36414
+ text = readFileSync7(file, "utf8");
36415
+ } catch (error) {
36416
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
36417
+ }
36418
+ try {
36419
+ return { ok: true, value: JSON.parse(text) };
36420
+ } catch (error) {
36421
+ return { ok: false, reason: `not valid JSON (${error instanceof Error ? error.message : String(error)})` };
36422
+ }
36423
+ }
36424
+ function harnessReplay(args, deps) {
36425
+ const parsed = parseReplayArgs(args);
36426
+ if (parsed.record === undefined || parsed.record.length === 0) {
36427
+ console.log(USAGE);
36428
+ process.exitCode = 1;
36429
+ return;
36430
+ }
36431
+ const recordRead = readJsonFile2(parsed.record);
36432
+ if (!recordRead.ok) {
36433
+ console.error(`Cannot read run record ${parsed.record}: ${recordRead.reason}`);
36434
+ process.exitCode = 1;
36435
+ return;
36436
+ }
36437
+ const run = parseRunRecord(recordRead.value);
36438
+ if (run === undefined) {
36439
+ console.error(`${parsed.record} is not a harness run record (expected runId plus the five recorded hashes; write one with \`keryx harness run --record\`).`);
36440
+ process.exitCode = 1;
36441
+ return;
36442
+ }
36443
+ const clock = deps?.clock ?? (() => new Date().toISOString());
36444
+ let idCounter = 0;
36445
+ const idSeq = deps?.idSeq ?? (() => `${randomUUID5()}-${idCounter++}`);
36446
+ const fixture = (() => {
36447
+ if (parsed.fixture === undefined || parsed.fixture.length === 0) {
36448
+ return { ok: true, value: buildReplayFixture(run, { idSeq }), built: true };
36449
+ }
36450
+ const read = readJsonFile2(parsed.fixture);
36451
+ if (!read.ok) {
36452
+ return { ok: false, reason: `Cannot read fixture ${parsed.fixture}: ${read.reason}` };
36453
+ }
36454
+ const parsedFixture = parseReplayFixture(read.value);
36455
+ if (parsedFixture === undefined) {
36456
+ return {
36457
+ ok: false,
36458
+ reason: `${parsed.fixture} is not a replay fixture (expected fixtureId, mode and the five hashes).`
36459
+ };
36460
+ }
36461
+ return { ok: true, value: parsedFixture, built: false };
36462
+ })();
36463
+ if (!fixture.ok) {
36464
+ console.error(fixture.reason);
36465
+ process.exitCode = 1;
36466
+ return;
36467
+ }
36468
+ if (parsed.writeFixture !== undefined && parsed.writeFixture.length > 0) {
36469
+ writeFileSync6(parsed.writeFixture, `${JSON.stringify(fixture.value, null, 2)}
36470
+ `, {
36471
+ encoding: "utf8",
36472
+ mode: 384
36473
+ });
36474
+ }
36475
+ const outcome = replayOffline(fixture.value, run, { clock, idSeq });
36476
+ if (parsed.json) {
36477
+ console.log(JSON.stringify({
36478
+ schemaVersion: 1,
36479
+ mode: fixture.value.mode,
36480
+ runId: run.runId,
36481
+ fixtureId: fixture.value.fixtureId,
36482
+ fixtureSource: fixture.built ? "built-from-record" : parsed.fixture,
36483
+ ok: outcome.ok,
36484
+ ...outcome.ok ? {} : { mismatch: outcome.mismatch }
36485
+ }));
36486
+ } else if (outcome.ok) {
36487
+ console.log(`Replay OK (${fixture.value.mode}): fixture ${fixture.value.fixtureId} matches run ${run.runId}.`);
36488
+ if (fixture.built) {
36489
+ console.log("Fixture was built from this record, so a match is expected; keep it with --write-fixture.");
36490
+ }
36491
+ } else {
36492
+ console.error(`Replay MISMATCH (${outcome.mismatch.kind}) on run ${run.runId}:`);
36493
+ console.error(` ${outcome.mismatch.detail ?? "hash diverged"}`);
36494
+ console.error(` expected ${outcome.mismatch.expectedHash}`);
36495
+ console.error(` actual ${outcome.mismatch.actualHash}`);
36496
+ }
36497
+ if (!outcome.ok) {
36498
+ process.exitCode = 1;
36499
+ }
36500
+ }
36155
36501
  var EXEC_PARENT_REMAINING_MS = 60000;
36156
36502
  var EXEC_DEFAULT_RUNTIME_MS = 30000;
36157
36503
  var EXEC_OUTPUT_LIMIT_BYTES = 1e6;
@@ -36340,6 +36686,7 @@ async function harnessExec(args, deps) {
36340
36686
  }
36341
36687
  effectiveAllowEnvKeys = [...allowEnvKeys, ...Object.keys(net.envAdditions)];
36342
36688
  }
36689
+ const scanner = await buildHarnessScanner(cwd);
36343
36690
  const command = {
36344
36691
  path: commandPath,
36345
36692
  argv: [commandPath, ...commandArgs],
@@ -36362,7 +36709,7 @@ async function harnessExec(args, deps) {
36362
36709
  envAllowlist: effectiveAllowEnvKeys,
36363
36710
  profile: shellAllowProfile(),
36364
36711
  interactive: true,
36365
- scanAvailable: true,
36712
+ scanAvailable: scanner.available,
36366
36713
  risk: "shell"
36367
36714
  },
36368
36715
  budget,
@@ -39096,7 +39443,7 @@ import {
39096
39443
  mkdirSync as mkdirSync6,
39097
39444
  readdirSync,
39098
39445
  renameSync as renameSync2,
39099
- writeFileSync as writeFileSync6
39446
+ writeFileSync as writeFileSync7
39100
39447
  } from "fs";
39101
39448
  import path117 from "path";
39102
39449
  import { randomUUID as randomUUID7 } from "crypto";
@@ -39137,7 +39484,7 @@ function tighten(target) {
39137
39484
  }
39138
39485
  function atomicWriteText(file, body) {
39139
39486
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
39140
- writeFileSync6(tmp, body, { encoding: "utf8", mode: 384 });
39487
+ writeFileSync7(tmp, body, { encoding: "utf8", mode: 384 });
39141
39488
  renameSync2(tmp, file);
39142
39489
  }
39143
39490
  function atomicWriteJson(file, value) {
@@ -39395,6 +39742,39 @@ function compactSession(handle, context, archive, opts) {
39395
39742
  atomicWriteJson(path117.join(withCount.dir, "summary.json"), withCount.summary);
39396
39743
  return { handle: withCount, context: result.context, result };
39397
39744
  }
39745
+
39746
+ class UnknownSessionError extends Error {
39747
+ idOrPrefix;
39748
+ constructor(idOrPrefix) {
39749
+ super(`no session matching "${idOrPrefix}" in this project`);
39750
+ this.idOrPrefix = idOrPrefix;
39751
+ this.name = "UnknownSessionError";
39752
+ }
39753
+ }
39754
+ function forkSession(opts) {
39755
+ const source = findSession(opts.cwd, opts.sourceIdOrPrefix, opts.dataDir);
39756
+ if (source === undefined) {
39757
+ throw new UnknownSessionError(opts.sourceIdOrPrefix);
39758
+ }
39759
+ const context = loadContext(opts.cwd, source.id, opts.dataDir);
39760
+ const archive = loadArchive(opts.cwd, source.id, opts.dataDir);
39761
+ const title = opts.title !== undefined && opts.title.trim().length > 0 ? opts.title.trim() : `${source.title} (fork)`;
39762
+ const created = createSession({
39763
+ cwd: opts.cwd,
39764
+ parentSessionId: source.id,
39765
+ title,
39766
+ ...opts.dataDir !== undefined ? { dataDir: opts.dataDir } : {},
39767
+ ...source.provider !== undefined ? { provider: source.provider } : {},
39768
+ ...source.model !== undefined ? { model: source.model } : {}
39769
+ });
39770
+ const handle = persistHistory(created, context, {
39771
+ archive,
39772
+ title,
39773
+ ...source.provider !== undefined ? { provider: source.provider } : {},
39774
+ ...source.model !== undefined ? { model: source.model } : {}
39775
+ });
39776
+ return { handle, source, messageCount: context.length, archiveCount: archive.length };
39777
+ }
39398
39778
  function openSession(opts) {
39399
39779
  const cwd = opts.cwd;
39400
39780
  const dataDir = opts.dataDir;
@@ -44039,7 +44419,7 @@ async function shellCommand(args2) {
44039
44419
  async function sessionsCommand(args2) {
44040
44420
  const sub = args2[0] ?? "list";
44041
44421
  if (sub === "--help" || sub === "-h" || sub === "help") {
44042
- printHelp14();
44422
+ printHelp15();
44043
44423
  return;
44044
44424
  }
44045
44425
  const cwd = process.cwd();
@@ -44061,7 +44441,7 @@ async function sessionsCommand(args2) {
44061
44441
  console.log(pad("ID", 10) + pad("UPDATED", 22) + pad("MSGS", 6) + pad("MODEL", 24) + "TITLE");
44062
44442
  for (const s of rows) {
44063
44443
  const model = s.provider !== undefined && s.model !== undefined ? `${s.provider}/${s.model}` : s.model ?? "-";
44064
- console.log(pad(shortSessionId(s.id), 10) + pad(s.updatedAt.slice(0, 19).replace("T", " "), 22) + pad(String(s.messageCount), 6) + pad(clip4(model, 22), 24) + s.title);
44444
+ console.log(pad(shortSessionId(s.id), 10) + pad(s.updatedAt.slice(0, 19).replace("T", " "), 22) + pad(String(s.messageCount), 6) + pad(clip4(model, 22), 24) + (s.parentSessionId !== undefined ? `\u21B3 ${s.title}` : s.title));
44065
44445
  }
44066
44446
  console.log("");
44067
44447
  console.log("Resume: keryx shell -r <id> Continue last: keryx shell -c");
@@ -44091,12 +44471,61 @@ async function sessionsCommand(args2) {
44091
44471
  }
44092
44472
  return;
44093
44473
  }
44474
+ if (sub === "fork") {
44475
+ const id = args2[1];
44476
+ if (id === undefined || id.length === 0 || id.startsWith("-")) {
44477
+ console.error('Usage: keryx sessions fork <id> [--title "<t>"]');
44478
+ process.exitCode = 1;
44479
+ return;
44480
+ }
44481
+ const titleIndex = args2.indexOf("--title");
44482
+ const title = titleIndex >= 0 ? args2[titleIndex + 1] : undefined;
44483
+ if (titleIndex >= 0 && (title === undefined || title.length === 0)) {
44484
+ console.error('Usage: keryx sessions fork <id> [--title "<t>"]');
44485
+ process.exitCode = 1;
44486
+ return;
44487
+ }
44488
+ try {
44489
+ const forked = forkSession({ cwd, sourceIdOrPrefix: id, ...title !== undefined ? { title } : {} });
44490
+ if (args2.includes("--json")) {
44491
+ console.log(JSON.stringify({
44492
+ schemaVersion: 1,
44493
+ id: forked.handle.summary.id,
44494
+ parentSessionId: forked.source.id,
44495
+ title: forked.handle.summary.title,
44496
+ messageCount: forked.messageCount,
44497
+ archiveMessageCount: forked.archiveCount,
44498
+ dir: forked.handle.dir
44499
+ }, null, 2));
44500
+ return;
44501
+ }
44502
+ console.log(`Forked ${shortSessionId(forked.source.id)} -> ${shortSessionId(forked.handle.summary.id)}`);
44503
+ console.log(` title: ${forked.handle.summary.title}`);
44504
+ console.log(` parent: ${forked.source.id}`);
44505
+ console.log(` history: ${forked.messageCount} context / ${forked.archiveCount} archive`);
44506
+ console.log("");
44507
+ console.log(`Resume: keryx shell -r ${shortSessionId(forked.handle.summary.id)}`);
44508
+ } catch (cause) {
44509
+ if (cause instanceof UnknownSessionError) {
44510
+ console.error(`No session "${id}" in this project. Use \`keryx sessions list\`.`);
44511
+ process.exitCode = 1;
44512
+ return;
44513
+ }
44514
+ if (cause instanceof TranscriptUnreadableError) {
44515
+ console.error(`Cannot fork session "${id}": ${cause.message}`);
44516
+ process.exitCode = 1;
44517
+ return;
44518
+ }
44519
+ throw cause;
44520
+ }
44521
+ return;
44522
+ }
44094
44523
  if (sub === "path") {
44095
44524
  console.log(projectSessionsDir(cwd));
44096
44525
  return;
44097
44526
  }
44098
44527
  console.error(`Unknown sessions subcommand: ${sub}`);
44099
- printHelp14();
44528
+ printHelp15();
44100
44529
  process.exitCode = 1;
44101
44530
  }
44102
44531
  function pad(s, n) {
@@ -44105,13 +44534,14 @@ function pad(s, n) {
44105
44534
  function clip4(s, n) {
44106
44535
  return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
44107
44536
  }
44108
- function printHelp14() {
44537
+ function printHelp15() {
44109
44538
  console.log(`keryx sessions
44110
44539
 
44111
44540
  Per-project interactive shell sessions (isolated by git root / cwd).
44112
44541
 
44113
44542
  Usage:
44114
44543
  keryx sessions list [--json] List sessions for the current project
44544
+ keryx sessions fork <id> Branch a session into a new one (--title, --json)
44115
44545
  keryx sessions export <id> Export transcript as Markdown
44116
44546
  keryx sessions path Print the on-disk sessions directory
44117
44547
 
@@ -44160,7 +44590,7 @@ function buildInitFlags(next, profile) {
44160
44590
  async function modulesCommand(args2 = []) {
44161
44591
  const sub = args2[0];
44162
44592
  if (sub === "--help" || sub === "-h" || sub === "help") {
44163
- printHelp15();
44593
+ printHelp16();
44164
44594
  return;
44165
44595
  }
44166
44596
  const wantsJson = args2.includes("--json") && (sub === undefined || sub === "status" || sub === "list" || sub === "--json");
@@ -44217,7 +44647,7 @@ async function modulesCommand(args2 = []) {
44217
44647
  }
44218
44648
  }
44219
44649
  } else {
44220
- printHelp15();
44650
+ printHelp16();
44221
44651
  process.exitCode = 1;
44222
44652
  return;
44223
44653
  }
@@ -44260,7 +44690,7 @@ function setsEqual(a, b) {
44260
44690
  }
44261
44691
  return true;
44262
44692
  }
44263
- function printHelp15() {
44693
+ function printHelp16() {
44264
44694
  helpTitle("keryx modules", "view and toggle Metaproject modules");
44265
44695
  helpUsage([
44266
44696
  "keryx modules",
@@ -44616,7 +45046,7 @@ import {
44616
45046
  renameSync as renameSync3,
44617
45047
  statSync as statSync5,
44618
45048
  unlinkSync as unlinkSync2,
44619
- writeFileSync as writeFileSync7
45049
+ writeFileSync as writeFileSync8
44620
45050
  } from "fs";
44621
45051
  import path120 from "path";
44622
45052
  function serveCredentialPath(dir) {
@@ -44695,7 +45125,7 @@ function writeStore(store, dir) {
44695
45125
  ensureKeryxConfigDir(dir);
44696
45126
  const handle = openSync3(temp, "wx", 384);
44697
45127
  try {
44698
- writeFileSync7(handle, `${JSON.stringify(store, null, 2)}
45128
+ writeFileSync8(handle, `${JSON.stringify(store, null, 2)}
44699
45129
  `, { encoding: "utf8" });
44700
45130
  fsyncSync2(handle);
44701
45131
  } finally {
@@ -45729,7 +46159,7 @@ var ENABLE_FLAG = "--enable";
45729
46159
  var DISABLE_FLAG = "--disable";
45730
46160
  async function serveCommand(args2 = []) {
45731
46161
  if (args2.includes("--help") || args2.includes("-h") || args2[0] === "help") {
45732
- printHelp16();
46162
+ printHelp17();
45733
46163
  return;
45734
46164
  }
45735
46165
  const sub = args2[0];
@@ -45750,14 +46180,14 @@ async function serveCommand(args2 = []) {
45750
46180
  return;
45751
46181
  }
45752
46182
  console.error(`Unknown serve command: ${sanitizeForDisplay(sub)}`);
45753
- printHelp16();
46183
+ printHelp17();
45754
46184
  process.exitCode = 1;
45755
46185
  }
45756
46186
  async function runServe(args2) {
45757
46187
  const parsed = parseArgs3(args2, BIND_FLAGS, [ACK_FLAG]);
45758
46188
  if (!parsed.ok) {
45759
46189
  console.error(parsed.message);
45760
- printHelp16();
46190
+ printHelp17();
45761
46191
  process.exitCode = 1;
45762
46192
  return;
45763
46193
  }
@@ -45859,7 +46289,7 @@ function runStatus6(args2) {
45859
46289
  const parsed = parseArgs3(args2, [], ["--json"]);
45860
46290
  if (!parsed.ok) {
45861
46291
  console.error(parsed.message);
45862
- printHelp16();
46292
+ printHelp17();
45863
46293
  process.exitCode = 1;
45864
46294
  return;
45865
46295
  }
@@ -45906,7 +46336,7 @@ function runToken(args2) {
45906
46336
  const rest = parseArgs3(args2.slice(1), [], []);
45907
46337
  if (!rest.ok) {
45908
46338
  console.error(rest.message);
45909
- printHelp16();
46339
+ printHelp17();
45910
46340
  process.exitCode = 1;
45911
46341
  return;
45912
46342
  }
@@ -45947,7 +46377,7 @@ function runToken(args2) {
45947
46377
  return;
45948
46378
  }
45949
46379
  console.error(sub === undefined ? "Missing token subcommand" : `Unknown token command: ${sanitizeForDisplay(sub)}`);
45950
- printHelp16();
46380
+ printHelp17();
45951
46381
  process.exitCode = 1;
45952
46382
  }
45953
46383
  function printTokenOnce(token) {
@@ -45972,7 +46402,7 @@ function runConfig(args2) {
45972
46402
  const parsed = parseArgs3(args2.slice(1), BIND_FLAGS, [ACK_FLAG, FORCE_FLAG]);
45973
46403
  if (!parsed.ok) {
45974
46404
  console.error(parsed.message);
45975
- printHelp16();
46405
+ printHelp17();
45976
46406
  process.exitCode = 1;
45977
46407
  return;
45978
46408
  }
@@ -46022,7 +46452,7 @@ function runConfig(args2) {
46022
46452
  const parsed = parseArgs3(args2.slice(1), [], ["--json"]);
46023
46453
  if (!parsed.ok) {
46024
46454
  console.error(parsed.message);
46025
- printHelp16();
46455
+ printHelp17();
46026
46456
  process.exitCode = 1;
46027
46457
  return;
46028
46458
  }
@@ -46039,14 +46469,14 @@ function runConfig(args2) {
46039
46469
  return;
46040
46470
  }
46041
46471
  console.error(sub === undefined ? "Missing config subcommand" : `Unknown config command: ${sanitizeForDisplay(sub)}`);
46042
- printHelp16();
46472
+ printHelp17();
46043
46473
  process.exitCode = 1;
46044
46474
  }
46045
46475
  function runConfigSet(args2) {
46046
46476
  const parsed = parseArgs3(args2, BIND_FLAGS, [ACK_FLAG, NO_ACK_FLAG, ENABLE_FLAG, DISABLE_FLAG]);
46047
46477
  if (!parsed.ok) {
46048
46478
  console.error(parsed.message);
46049
- printHelp16();
46479
+ printHelp17();
46050
46480
  process.exitCode = 1;
46051
46481
  return;
46052
46482
  }
@@ -46117,7 +46547,7 @@ function printConfig(config) {
46117
46547
  console.log(` approvals: expire after ${config.approval.expirySeconds}s, max ${config.approval.maxPendingPerSession} pending per session`);
46118
46548
  console.log(` non-loopback acknowledged: ${config.bind.acknowledgeNonLoopback === true}`);
46119
46549
  }
46120
- function printHelp16() {
46550
+ function printHelp17() {
46121
46551
  helpTitle("keryx serve", "loopback-bound HTTP entry over this install (off by default)");
46122
46552
  helpUsage([
46123
46553
  `keryx serve [--bind <addr>] [--port <n>] [--profile <name>] [${ACK_FLAG}]`,
@@ -46162,7 +46592,7 @@ init_git_hooks();
46162
46592
  async function updateCommand(args2 = []) {
46163
46593
  const options = parseUpdateArgs(args2);
46164
46594
  if (options.help) {
46165
- printHelp17();
46595
+ printHelp18();
46166
46596
  return;
46167
46597
  }
46168
46598
  const projectRoot = process.cwd();
@@ -47272,7 +47702,7 @@ function runtimeSourcePath2(relativePath) {
47272
47702
  function escapeRegExp5(value) {
47273
47703
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47274
47704
  }
47275
- function printHelp17() {
47705
+ function printHelp18() {
47276
47706
  helpTitle("keryx update", "refresh .metaproject service files (data left untouched)");
47277
47707
  helpUsage(["keryx update [--skip-runtime] [--hooks] [--no-tasks]"]);
47278
47708
  heading("Default behavior");
@@ -47300,7 +47730,7 @@ async function dashboardCommand(args2 = []) {
47300
47730
  const options = parseOptions(args2);
47301
47731
  const subcommand = options.positionals[0];
47302
47732
  if (!subcommand || options.help) {
47303
- printHelp18();
47733
+ printHelp19();
47304
47734
  return;
47305
47735
  }
47306
47736
  if (subcommand === "build") {
@@ -47318,7 +47748,7 @@ async function dashboardCommand(args2 = []) {
47318
47748
  return;
47319
47749
  }
47320
47750
  console.log(` ${style.red(symbols.cross)} Unknown dashboard command: ${subcommand}`);
47321
- printHelp18();
47751
+ printHelp19();
47322
47752
  process.exitCode = 1;
47323
47753
  }
47324
47754
  function parseOptions(args2) {
@@ -47343,7 +47773,7 @@ async function openFile(filePath) {
47343
47773
  });
47344
47774
  });
47345
47775
  }
47346
- function printHelp18() {
47776
+ function printHelp19() {
47347
47777
  helpTitle("keryx dashboard", "build and open the human dashboard");
47348
47778
  helpUsage([
47349
47779
  "keryx dashboard build",
@@ -48183,7 +48613,7 @@ Usage:
48183
48613
  // package.json
48184
48614
  var package_default = {
48185
48615
  name: "@mrciphersmith/keryx",
48186
- version: "0.2.14",
48616
+ version: "0.2.16",
48187
48617
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
48188
48618
  private: false,
48189
48619
  publishConfig: {
@@ -48255,7 +48685,7 @@ var package_default = {
48255
48685
  var VERSION2 = package_default.version;
48256
48686
  var CLI_ROUTES = {
48257
48687
  init: initCommand,
48258
- status: () => statusCommand(),
48688
+ status: statusCommand,
48259
48689
  modules: modulesCommand,
48260
48690
  projects: projectsCommand,
48261
48691
  serve: serveCommand,
@@ -48290,7 +48720,7 @@ async function main() {
48290
48720
  const args2 = process.argv.slice(2);
48291
48721
  const command = args2[0];
48292
48722
  if (command === "--help" || command === "-h" || command === "help" || !command) {
48293
- printHelp19();
48723
+ printHelp20();
48294
48724
  return;
48295
48725
  }
48296
48726
  if (command === "--version" || command === "-v") {
@@ -48303,22 +48733,25 @@ async function main() {
48303
48733
  return;
48304
48734
  }
48305
48735
  console.error(`Unknown command: ${command}`);
48306
- printHelp19();
48736
+ printHelp20();
48307
48737
  process.exitCode = 1;
48308
48738
  }
48309
- function printHelp19() {
48739
+ function printHelp20() {
48310
48740
  console.log(`keryx ${VERSION2}
48311
48741
 
48312
48742
  Usage:
48313
48743
  keryx Show CLI usage
48314
48744
  keryx shell [-c|--continue] [-r|--resume [id]] [--provider <p>] [--model <m>] [--base-url <url>] [--agent|--chat] [--tui|--no-tui]
48315
48745
  Start TUI agent shell (sessions are per-project)
48316
- keryx sessions list|export <id>|path List / export sessions for the current project
48317
- keryx harness run --provider <fake|anthropic|ollama> --model <m> [--base-url <url>] "<prompt>"
48746
+ keryx sessions list|fork <id>|export <id>|path
48747
+ List / branch / export sessions for the current project
48748
+ keryx harness run --provider <fake|anthropic|ollama> --model <m> [--base-url <url>] [--record <path>] "<prompt>"
48318
48749
  keryx harness exec [--allow-env KEY]... [--max-runtime-ms N] [--allow-real-subprocess]
48319
48750
  [--allowed-domains a,b] [--mask-env NAME@host] [--tls-terminate] [--mask-mode auto|manual|off] [--auto-mask] -- <path> [args...]
48320
48751
  keryx harness extension --spec <path>
48321
48752
  keryx harness wave --spec <path>
48753
+ keryx harness replay --record <path> [--fixture <path>] [--write-fixture <path>] [--json]
48754
+ Validate a recorded run's log against a fixture (no re-execution)
48322
48755
  keryx init [--yes] [--no-gdgraph] [--no-gdctx] [--no-gdwiki] [--no-gdskills] [--gdskills-profile recommended] [--no-health] [--no-testing] [--no-memory] [--no-gdgraph-hook] [--no-gdskills-hook] [--no-health-hook] [--no-testing-post-commit-hook] [--no-testing-pre-push-hook]
48323
48756
  keryx status
48324
48757
  keryx modules [status | enable <name> | disable <name>]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {