@azure-id/orc 2.1.1 → 2.2.0

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 (145) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/README.md +48 -23
  3. package/bin/build-agents.js +207 -205
  4. package/bin/cli.js +48188 -47417
  5. package/bin/fix.js +21 -18
  6. package/bin/graph-extract.js +314 -6
  7. package/bin/graph-gain.js +83 -2
  8. package/bin/graph-map.js +23 -1
  9. package/bin/graph-notes.js +1 -1
  10. package/bin/graph-query.js +1362 -37
  11. package/bin/graph-resolve.js +1 -1
  12. package/bin/graph-shard.js +29 -6
  13. package/bin/graph-signals.js +5 -3
  14. package/bin/graph.js +7 -3
  15. package/bin/habit.js +1 -1
  16. package/bin/pricing.json +8 -1
  17. package/bin/trace-write.js +40 -11
  18. package/bin/verify-contracts.js +5661 -5552
  19. package/bin/verify-package.js +716 -716
  20. package/bin/webui/api.js +8 -0
  21. package/bin/webui/fixtures/extra.js +2036 -2036
  22. package/bin/webui/fixtures/flow.js +1 -1
  23. package/bin/webui/fixtures/index.js +2 -2
  24. package/bin/webui/fixtures/knowledge.js +8 -1
  25. package/bin/webui/fixtures/pact.js +112 -111
  26. package/bin/webui/fixtures/stats.js +2 -2
  27. package/bin/webui/i18n/en/knowledge.json +2 -0
  28. package/bin/webui/i18n/id/knowledge.json +2 -0
  29. package/bin/webui/js/panels/knowledge.js +17 -0
  30. package/guides/code-graph-benefit.md +116 -0
  31. package/guides/configuration.md +150 -0
  32. package/guides/documents.md +262 -0
  33. package/guides/extra-models.md +448 -0
  34. package/guides/habits-and-gotchas.md +152 -0
  35. package/guides/knowledge-reads.md +190 -0
  36. package/guides/model-selection.md +186 -0
  37. package/guides/rules.md +305 -0
  38. package/guides/status-line.md +344 -0
  39. package/mock-run/orc-cli.md +3 -2
  40. package/mock-run/orc-doc.md +452 -448
  41. package/mock-run/orc.md +157 -157
  42. package/package.json +2 -1
  43. package/templates/agents/MODEL-MAPPING.md +176 -176
  44. package/templates/agents/{orc-executor-haiku-4-5.md → orc-executor-haiku-5-high.md} +21 -8
  45. package/templates/agents/orc-executor-opus-4-7-high.md +17 -5
  46. package/templates/agents/orc-executor-opus-4-7-med.md +17 -5
  47. package/templates/agents/orc-executor-opus-4-8-high.md +17 -5
  48. package/templates/agents/orc-executor-opus-5-high.md +17 -5
  49. package/templates/agents/orc-executor-opus-5-low.md +17 -5
  50. package/templates/agents/orc-executor-opus-5-med.md +17 -5
  51. package/templates/agents/orc-executor-sonnet-4-6-high.md +17 -5
  52. package/templates/agents/orc-executor-sonnet-5-high.md +17 -5
  53. package/templates/agents/orc-executor-sonnet-5-low.md +17 -5
  54. package/templates/agents/orc-executor-sonnet-5-med.md +17 -5
  55. package/templates/agents/{orc-graph-noter-sonnet-4-6-med.md → orc-graph-noter-haiku-5-high.md} +4 -4
  56. package/templates/agents/orc-planner-mini-opus-5-med.md +3 -1
  57. package/templates/agents/orc-planner-mini-sonnet-5-high.md +3 -1
  58. package/templates/agents/orc-planner-opus-5-med.md +3 -1
  59. package/templates/agents/{orc-trace-writer-haiku-4-5.md → orc-trace-writer-haiku-5-high.md} +4 -3
  60. package/templates/commands/orc-doc.md +128 -128
  61. package/templates/commands/orc-export.md +51 -46
  62. package/templates/hooks/README.md +8 -3
  63. package/templates/hooks/orc-effort-guard.js +105 -27
  64. package/templates/hooks/orc-graph-hook.js +252 -4
  65. package/templates/hooks/orc-statusline.js +13 -6
  66. package/templates/hooks/orc-trace.js +1 -1
  67. package/templates/skills/_shared/README.md +2 -2
  68. package/templates/skills/_shared/code-graph.md +185 -251
  69. package/templates/skills/_shared/extra-dispatch.md +1331 -1331
  70. package/templates/skills/_shared/lane-contract.md +7 -3
  71. package/templates/skills/_shared/opus5-only.md +137 -137
  72. package/templates/skills/_shared/phases/execution.md +13 -11
  73. package/templates/skills/_shared/phases/planning.md +27 -9
  74. package/templates/skills/_shared/phases/preflight.md +3 -3
  75. package/templates/skills/_shared/phases/review.md +4 -3
  76. package/templates/skills/_shared/phases/ship.md +7 -10
  77. package/templates/skills/_shared/phases/summary.md +2 -3
  78. package/templates/skills/_shared/phases/trace-verbs.md +21 -16
  79. package/templates/skills/_shared/phases/trace.md +3 -3
  80. package/templates/skills/_shared/phases/wiki-consult.md +2 -2
  81. package/templates/skills/_shared/pr-templates.md +108 -106
  82. package/templates/skills/_shared/read-ladder.md +8 -2
  83. package/templates/skills/_shared/return-validation.md +10 -6
  84. package/templates/skills/_shared/review-slice.md +1 -1
  85. package/templates/skills/_shared/stack-plan.md +135 -135
  86. package/templates/skills/_shared/wait.md +14 -2
  87. package/templates/skills/context-combiner/SKILL.md +2 -2
  88. package/templates/skills/orc/SKILL.md +227 -227
  89. package/templates/skills/orc/config.md +2 -2
  90. package/templates/skills/orc/examples/full-run-mock.md +74 -74
  91. package/templates/skills/orc/schemas/planning-output.md +3 -0
  92. package/templates/skills/orc-aftermath/SKILL.md +15 -15
  93. package/templates/skills/orc-analyze/SKILL.md +2 -2
  94. package/templates/skills/orc-analyze-mini/SKILL.md +2 -2
  95. package/templates/skills/orc-boundary/SKILL.md +15 -15
  96. package/templates/skills/orc-boundary/references/card.md +81 -78
  97. package/templates/skills/orc-boundary/references/gate.md +113 -113
  98. package/templates/skills/orc-brainstorm/SKILL.md +15 -15
  99. package/templates/skills/orc-budget/SKILL.md +15 -15
  100. package/templates/skills/orc-challenge/README.md +1 -1
  101. package/templates/skills/orc-challenge/SKILL.md +16 -16
  102. package/templates/skills/orc-challenge/examples/council-full-roster.md +1 -1
  103. package/templates/skills/orc-claude/SKILL.md +4 -4
  104. package/templates/skills/orc-claude/examples/claude-run-mock.md +1 -1
  105. package/templates/skills/orc-diy/SKILL.md +2 -2
  106. package/templates/skills/orc-diy/references/blocks/wiki.md +26 -26
  107. package/templates/skills/orc-diy/references/flow-schema.md +2 -2
  108. package/templates/skills/orc-doc/README.md +2 -2
  109. package/templates/skills/orc-doc/SKILL.md +501 -490
  110. package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +12 -1
  111. package/templates/skills/orc-doc/references/chunking.md +6 -4
  112. package/templates/skills/orc-doc/references/gates.md +311 -311
  113. package/templates/skills/orc-doc/references/resume-protocol.md +229 -228
  114. package/templates/skills/orc-explain/SKILL.md +88 -88
  115. package/templates/skills/orc-export/SKILL.md +190 -187
  116. package/templates/skills/orc-fast/SKILL.md +213 -210
  117. package/templates/skills/orc-grill/SKILL.md +245 -245
  118. package/templates/skills/orc-handoff/SKILL.md +11 -11
  119. package/templates/skills/orc-learn/SKILL.md +13 -13
  120. package/templates/skills/orc-learn/examples/learn-run-mock.md +2 -2
  121. package/templates/skills/orc-mini/SKILL.md +12 -12
  122. package/templates/skills/orc-mini/examples/mini-run-mock.md +4 -5
  123. package/templates/skills/orc-pact/SKILL.md +15 -15
  124. package/templates/skills/orc-pact/references/gate.md +70 -70
  125. package/templates/skills/orc-pattern/SKILL.md +3 -3
  126. package/templates/skills/orc-poly/SKILL.md +4 -4
  127. package/templates/skills/orc-poly/examples/poly-run-mock.md +51 -51
  128. package/templates/skills/orc-pr-driver/SKILL.md +4 -3
  129. package/templates/skills/orc-pr-driver/references/orc-run-split.md +108 -99
  130. package/templates/skills/orc-pr-setup/SKILL.md +2 -2
  131. package/templates/skills/orc-quick/README.md +535 -536
  132. package/templates/skills/orc-quick/SKILL.md +29 -32
  133. package/templates/skills/orc-quick/references/context-doc.md +145 -145
  134. package/templates/skills/orc-quick/references/dispatch-gate.md +220 -220
  135. package/templates/skills/orc-quick/references/gh-mode.md +2 -1
  136. package/templates/skills/orc-quick/references/look.md +34 -12
  137. package/templates/skills/orc-retro/SKILL.md +2 -2
  138. package/templates/skills/orc-retro/examples/retro-mock.md +2 -2
  139. package/templates/skills/orc-route/SKILL.md +2 -2
  140. package/templates/skills/orc-test/SKILL.md +6 -3
  141. package/templates/skills/orc-verify/SKILL.md +2 -2
  142. package/templates/skills/orc-wait/SKILL.md +10 -2
  143. package/templates/skills/orc-wiki/SKILL.md +4 -4
  144. package/templates/skills/orc-wiki/references/pattern-prewarm.md +19 -19
  145. package/templates/agents/orc-executor-sonnet-4-6-med.md +0 -155
@@ -39,7 +39,11 @@
39
39
  * · a graph exists on disk
40
40
  * · (the three delivery events) a SUBAGENT is acting — `agent_id` is present.
41
41
  * The orchestrator gets cards through the lane; a second copy in the main
42
- * session is the same tokens spent twice.
42
+ * session is the same tokens spent twice. ONE exception (R12, v2.2.0): a
43
+ * MAIN-session Grep / Glob / shell search of a name the graph knows gets
44
+ * the brief ctx card (callers, tests, coverage, the --anchors line), ONCE
45
+ * per name and at most 3 per run, under `code_graph: on` only. Live runs
46
+ * showed the main session ignore the graph text and hunt the tests by hand.
43
47
  *
44
48
  * FAIL-OPEN, ALWAYS, AND FAIL-QUIET. Exit 0 on every path, output nothing on
45
49
  * any error, and never write to stderr — a PreToolUse hook that exits non-zero
@@ -60,6 +64,7 @@
60
64
  * hooks.PreToolUse[] { matcher:"Grep|Glob", hooks:[{…}] }
61
65
  * hooks.PreToolUse[] { matcher:"Bash|Read", hooks:[{…}] }
62
66
  * hooks.PostToolUse[] { matcher:"Read", hooks:[{…}] }
67
+ * hooks.PostToolUse[] { matcher:"Task|Agent", hooks:[{…}] } (R13, main session)
63
68
  *
64
69
  * Wired even though it does nothing until `code_graph` is on — arming the
65
70
  * feature is a config edit, never an install step the user has to discover.
@@ -275,7 +280,11 @@ function run() {
275
280
  const event = String(data.hook_event_name || data.hookEventName || "");
276
281
  if (!event) return;
277
282
 
278
- if (String(cfg("code_graph") || "off").toLowerCase() !== "on") return;
283
+ // `semi` (v2.2.0) keeps the graph CURRENT but no lane reads it: only the
284
+ // SubagentStop update gets through. No hint, no additionalContext, no rows.
285
+ const graphMode = String(cfg("code_graph") || "off").toLowerCase();
286
+ if (graphMode !== "on" && graphMode !== "semi") return;
287
+ if (graphMode === "semi" && event !== "SubagentStop") return;
279
288
  // `code_graph_hooks` is `off`, `on`, or `on,read` (D3/DE-J). `on` is every
280
289
  // hint that was on in 1.8.1 PLUS the Bash search (DE-C: a shell grep is a
281
290
  // Grep). The whole-file READ hint is the one that changes how an agent reads,
@@ -295,7 +304,13 @@ function run() {
295
304
  // a subagent payload and absent in a main-session one — the same positive
296
305
  // test orc-read-gate.js measured in W1. Never test for the absence of some
297
306
  // other key, which asserts nothing.
298
- if (data.agent_id == null) return;
307
+ if (data.agent_id == null) {
308
+ // R12 — the one main-session delivery: a search of a known name.
309
+ if (event === "PreToolUse" && graphMode === "on" && String(data.tool_name || "") !== "Read") return onMainSearch(data, runName, meta);
310
+ // R13 — an executor came back: hand the main session the touchpoint-4 card.
311
+ if (event === "PostToolUse" && graphMode === "on" && /^(Agent|Task)$/.test(String(data.tool_name || ""))) return onExecutorReturn(data, runName, meta);
312
+ return;
313
+ }
299
314
 
300
315
  if (event === "SubagentStart") return onSubagentStart(data, runName, meta);
301
316
  if (event === "PreToolUse") {
@@ -377,7 +392,7 @@ function onSubagentStart(data, runName, meta) {
377
392
  // ── E4b — a search for a name the graph already knows ──────────────────────
378
393
  // It reads names.json ONLY: a name lookup, no index parse, no resolution. This
379
394
  // is what keeps the hook off the critical path of every Grep.
380
- function onSearch(data, runName, meta) {
395
+ function searchHit(data, meta) {
381
396
  const tool = String(data.tool_name || "");
382
397
  const input = data.tool_input || {};
383
398
  let pattern = "";
@@ -409,6 +424,12 @@ function onSearch(data, runName, meta) {
409
424
  }
410
425
  }
411
426
  if (!hit || !Array.isArray(hit.rows) || !hit.rows.length) return;
427
+ return hit;
428
+ }
429
+
430
+ function onSearch(data, runName, meta) {
431
+ const hit = searchHit(data, meta);
432
+ if (!hit) return;
412
433
  // Once per run per name. A wave of greps for the same symbol pays once.
413
434
  if (!bump(runName, "injected", "name:" + hit.word)) return;
414
435
 
@@ -423,6 +444,233 @@ function onSearch(data, runName, meta) {
423
444
  ledger(runName, "hint", body, { low: 0, high: HINT_RANGE_TOKENS, calls_low: 0, calls_high: 1 });
424
445
  }
425
446
 
447
+ // ── R12 (v2.2.0) — the MAIN session searched a name the graph knows ────────
448
+ // In /orc-quick the main session does the dig, and live runs showed it ignore
449
+ // the graph text and spend calls hunting the tests by hand. So the hook answers
450
+ // the search itself with the ctx card: callers, tests (also via a caller),
451
+ // coverage and the one-call anchors line. Once per name, at most 3 per run, so
452
+ // the main context cannot grow without a bound. A CLI that is missing, slow or
453
+ // broken falls back to the names.json one-liner. The search still runs.
454
+ const MAIN_CARDS = 3;
455
+ const MAIN_CALLERS = 8;
456
+ const MAIN_TEXT = 1800;
457
+
458
+ // R12b — the main session chains: `cd x && cat y; grep -rn "name" src`. Any
459
+ // segment (split on && || ; | and newlines) that starts with a search program
460
+ // counts, after `cd`, env assignments and `rtk` are dropped. The pattern is the
461
+ // first quoted argument of THAT segment, else its first bare non-flag word.
462
+ // MAIN PATH ONLY: the subagent path keeps BASH_SEARCH and FIRST_QUOTED as-is.
463
+ const SEARCH_HEAD = /^(?:grep|rg|ag|ack|findstr|git\s+grep|select-string|sls)\b/i;
464
+ // Split on && || ; | and newlines OUTSIDE quotes, so a grep pattern like
465
+ // "a\|b" stays one argument.
466
+ function shellSegments(cmd) {
467
+ const segs = [];
468
+ let cur = "";
469
+ let quote = null;
470
+ for (let i = 0; i < cmd.length; i++) {
471
+ const c = cmd[i];
472
+ if (quote) {
473
+ if (c === quote) quote = null;
474
+ cur += c;
475
+ continue;
476
+ }
477
+ if (c === "'" || c === '"') {
478
+ quote = c;
479
+ cur += c;
480
+ continue;
481
+ }
482
+ if (c === ";" || c === "|" || c === "&" || c === "\n" || c === "\r") {
483
+ segs.push(cur);
484
+ cur = "";
485
+ continue;
486
+ }
487
+ cur += c;
488
+ }
489
+ segs.push(cur);
490
+ return segs;
491
+ }
492
+
493
+ function mainPattern(data) {
494
+ const tool = String(data.tool_name || "");
495
+ const input = data.tool_input || {};
496
+ if (tool === "Grep" || tool === "Glob") return String(input.pattern || "");
497
+ if (tool !== "Bash") return "";
498
+ for (let seg of shellSegments(String(input.command || ""))) {
499
+ seg = seg.trim().replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)+/, "").replace(/^rtk\s+/i, "");
500
+ const m = SEARCH_HEAD.exec(seg);
501
+ if (!m) continue;
502
+ const rest = seg.slice(m[0].length);
503
+ const q = FIRST_QUOTED.exec(rest);
504
+ if (q) return q[1] !== undefined ? q[1] : q[2];
505
+ const bare = rest.split(/\s+/).find((t) => t && !t.startsWith("-"));
506
+ if (bare) return bare;
507
+ }
508
+ return "";
509
+ }
510
+
511
+ // R12b — the word with the HIGHEST fan-in wins; a word with fan-in 0 never
512
+ // spends a slot (a card that says "callers: none found" is noise).
513
+ function mainHit(data, meta) {
514
+ const pattern = mainPattern(data);
515
+ if (!pattern) return null;
516
+ const words = (pattern.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).filter((w) => w.length >= MIN_TOKEN);
517
+ if (!words.length) return null;
518
+ const names = readJson(path.join(GRAPH_DIR, "names.json"));
519
+ if (!names || !names.names || names.generation !== meta.generation) return null;
520
+ let best = null;
521
+ for (const w of words) {
522
+ if (!Object.prototype.hasOwnProperty.call(names.names, w)) continue;
523
+ const rows = names.names[w];
524
+ if (!Array.isArray(rows) || !rows.length) continue;
525
+ const fan = Math.max(...rows.map((x) => Number(x[5]) || 0));
526
+ if (fan > 0 && (!best || fan > best.fan)) best = { word: w, rows, fan };
527
+ }
528
+ return best;
529
+ }
530
+
531
+ function onMainSearch(data, runName, meta) {
532
+ const hit = mainHit(data, meta);
533
+ if (!hit) return;
534
+ const counters = readJson(seenFile(runName)) || {};
535
+ if ((Number(counters.main_cards) || 0) >= MAIN_CARDS) return;
536
+ if (!bump(runName, "injected", "main:" + hit.word)) return;
537
+ bump(runName, "main_cards");
538
+ const word = clean(hit.word, 80);
539
+
540
+ let out = null;
541
+ try {
542
+ const stamp = readJson(path.join(CLAUDE_DIR, "hooks", "orc-version.json"));
543
+ const cli = stamp && stamp.cli;
544
+ if (cli && fs.existsSync(cli)) {
545
+ const deadline = Math.max(1500, Number(cfg("code_graph_heal_ms")) || DEFAULT_DEADLINE_MS);
546
+ // Full --json, not --brief: --brief drops the callers[] rows this card
547
+ // names. The answer stays inside the hook; only the card text goes out.
548
+ const r = spawnSync(process.execPath, [cli, "graph", "ctx", hit.word, "--if-enabled", "--json", "--dir", PROJECT_ROOT], {
549
+ encoding: "utf8",
550
+ timeout: deadline,
551
+ windowsHide: true,
552
+ });
553
+ if (!r.error) out = JSON.parse(r.stdout || "{}");
554
+ }
555
+ } catch (_) {
556
+ out = null;
557
+ }
558
+
559
+ let text;
560
+ if (out && out.ok && out.target && out.target.file) {
561
+ const t = out.target;
562
+ const lines = Array.isArray(t.lines) ? t.lines : [];
563
+ const parts = [`graph: ${word} — ${clean(t.file, 160)}:${Number(lines[0]) || 0}-${Number(lines[1]) || 0}`];
564
+ const callers = Array.isArray(out.callers) ? out.callers.slice(0, MAIN_CALLERS) : [];
565
+ parts.push("callers: " + (callers.length ? callers.map((c) => `${clean(c.qname, 80)} ${clean(c.file, 160)}:${Number(c.line) || 0}`).join(", ") : "none found"));
566
+ const tests = new Set((Array.isArray(out.tests) ? out.tests : []).map((x) => clean(x, 160)));
567
+ for (const v of Array.isArray(out.tests_via) ? out.tests_via : []) if (v && v.file) tests.add(clean(v.file, 160));
568
+ parts.push("tests: " + (tests.size ? [...tests].slice(0, 8).join(", ") : "none found"));
569
+ const cov = out.coverage && typeof out.coverage === "object" ? out.coverage.verdict : out.coverage;
570
+ parts.push("coverage: " + (clean(cov, 40) || "unknown"));
571
+ parts.push(`slice anchors in one call: orc graph ctx ${word} --anchors --focus=${word} --if-enabled --json --brief`);
572
+ text = parts.join(" · ") + ". The search below still runs.";
573
+ } else {
574
+ const rows = hit.rows.slice(0, MAX_ROWS).map((r) => `${clean(r[0], 80)} (${clean(r[1], 20)}) ${clean(r[2], 160)}:${Number(r[3]) || 0}-${Number(r[4]) || 0} fan-in ${Number(r[5]) || 0}`);
575
+ text = `"${word}" is defined at ${rows.join(" · ")}. Read those ranges, or run orc graph ctx ${word} --json for callers and callees. The search below still runs.`;
576
+ }
577
+ const body = clean(text, MAIN_TEXT);
578
+ if (!body) return;
579
+ try {
580
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: `${PREFIX} ${body}` } }));
581
+ } catch (_) {}
582
+ const tokens = Math.ceil((PREFIX.length + 1 + body.length) / 4);
583
+ ledger(runName, "main-hint", body, { low: 0, high: HINT_RANGE_TOKENS, calls_low: 0, calls_high: 1 }, {
584
+ targets: [word],
585
+ gen: meta.generation,
586
+ paid: { card: tokens, source: 0, hints: 0, envelope: 0 },
587
+ });
588
+ }
589
+
590
+ // ── R13 (v2.2.0) — an executor returned to the MAIN session ──────────────
591
+ // The SubagentStop update already ran, so the graph is current. The hook asks
592
+ // `orc graph changes` for the files git sees as changed and injects ONE card:
593
+ // the line, the tests to run first, and the risky symbols. Once per dispatch
594
+ // (tool_use_id). Fail quiet: no card, and the lane runs `changes` itself.
595
+ const CHANGES_TEXT = 1500;
596
+ const CHANGES_MAX_FILES = 30;
597
+ const CHANGES_SKIP = /^(?:\.claude\/|orc-quick\/|coverage\/|node_modules\/)|\/node_modules\//;
598
+
599
+ function changedFiles() {
600
+ const r = spawnSync("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: PROJECT_ROOT, encoding: "utf8", timeout: 3000, windowsHide: true });
601
+ if (r.error || r.status !== 0) return [];
602
+ const out = [];
603
+ for (const line of String(r.stdout || "").split(/\r?\n/)) {
604
+ if (line.length < 4) continue;
605
+ const xy = line.slice(0, 2);
606
+ if (xy.includes("D")) continue; // no deletions
607
+ let p = line.slice(3);
608
+ const arrow = p.indexOf(" -> ");
609
+ if (arrow >= 0) p = p.slice(arrow + 4);
610
+ p = p.replace(/^"|"$/g, "").replace(/\\/g, "/");
611
+ if (!p || p.endsWith("/") || CHANGES_SKIP.test(p)) continue;
612
+ out.push(p);
613
+ if (out.length >= CHANGES_MAX_FILES) break;
614
+ }
615
+ return out;
616
+ }
617
+
618
+ function onExecutorReturn(data, runName, meta) {
619
+ const input = data.tool_input || {};
620
+ const agent = String(input.subagent_type || "");
621
+ if (!agent || !EXECUTOR.test(agent)) return;
622
+ const id = String(data.tool_use_id || "");
623
+ if (!id) return;
624
+ const counters = readJson(seenFile(runName)) || {};
625
+ if (Array.isArray(counters.tokens) && counters.tokens.includes("changes:" + id)) return;
626
+
627
+ const stamp = readJson(path.join(CLAUDE_DIR, "hooks", "orc-version.json"));
628
+ const cli = stamp && stamp.cli;
629
+ if (!cli || !fs.existsSync(cli)) return;
630
+ const files = changedFiles();
631
+ if (!files.length) return;
632
+
633
+ let out = null;
634
+ try {
635
+ const deadline = Math.max(1500, Number(cfg("code_graph_heal_ms")) || DEFAULT_DEADLINE_MS);
636
+ // Full --json, not --brief: --brief drops the symbols[] rows the risk list
637
+ // needs. The answer stays inside the hook; only the card text goes out.
638
+ const r = spawnSync(process.execPath, [cli, "graph", "changes", "--files=" + files.join(","), "--if-enabled", "--json", "--dir", PROJECT_ROOT], {
639
+ encoding: "utf8",
640
+ timeout: deadline,
641
+ windowsHide: true,
642
+ });
643
+ if (!r.error) out = JSON.parse(r.stdout || "{}");
644
+ } catch (_) {
645
+ out = null;
646
+ }
647
+ if (!out || !out.ok || typeof out.line !== "string") return;
648
+ if (!bump(runName, "changes_cards", "changes:" + id)) return;
649
+
650
+ const parts = [clean(String(out.line).split(/\r?\n/)[0], 300)];
651
+ const tests = (Array.isArray(out.tests) ? out.tests : []).map((x) => clean(x, 160)).filter(Boolean);
652
+ parts.push("tests to run first: " + (tests.length ? tests.slice(0, 12).join(", ") : "none"));
653
+ const risky = (Array.isArray(out.symbols) ? out.symbols : []).filter((x) => x && (x.risk === "high" || x.risk === "medium")).slice(0, 5);
654
+ if (risky.length) {
655
+ parts.push(
656
+ "risk: " +
657
+ risky.map((x) => `${clean(x.risk, 8)} ${clean(x.qname, 80)} ${clean(x.file, 160)}:${Number((x.lines || [])[0]) || 0} (${(Array.isArray(x.why) ? x.why : []).map((w) => clean(w, 40)).join(", ")})`).join("; ")
658
+ );
659
+ }
660
+ const tail = " This is the touchpoint-4 changes answer — do not run orc graph changes for this dispatch.";
661
+ const body = clean(parts.join(" · "), CHANGES_TEXT - PREFIX.length - tail.length - 2) + tail;
662
+ try {
663
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: `${PREFIX} ${body}` } }));
664
+ } catch (_) {}
665
+ const tokens = Math.ceil((PREFIX.length + 1 + body.length) / 4);
666
+ ledger(runName, "changes-hint", body, null, {
667
+ agent: clean(agent, 60),
668
+ targets: files.slice(0, 10).map((x) => clean(x, 160)),
669
+ gen: out.generation || meta.generation,
670
+ paid: { card: tokens, source: 0, hints: 0, envelope: 0 },
671
+ });
672
+ }
673
+
426
674
  // ── D3 (v1.8.2 W3) — a whole-file Read of a WIDE file ──────────────────────
427
675
  // OFF by default (`code_graph_hooks: on,read` arms it). The read STILL RUNS —
428
676
  // this hook never blocks and never rewrites an input. `updatedInput` exists and
@@ -51,9 +51,11 @@
51
51
  *
52
52
  * This is the ONLY place Claude Code exposes the live model id, so it also
53
53
  * writes a fail-silent session-model bridge (.claude/orc/session-model.json)
54
- * that the PreToolUse effort guard reads — the guard can't see the model id
55
- * on its own, so the bridge is how the Opus 5.5 / Fable 5 medium-effort allowance
56
- * reaches it.
54
+ * = {model_id, effort, session_id, written_at}. The effort guard reads it on
55
+ * the Skill call (the guard can't see the model id on its own, so the bridge is
56
+ * how the Opus 5.5 / Fable 5 medium-effort allowance reaches it) and on a TYPED
57
+ * /orc (the effort itself); the session id stops one session's reading gating
58
+ * another.
57
59
  *
58
60
  * Also appends a "newer orc version available" hint from the 24h update cache
59
61
  * (cache-only here — never a network call in the statusline hot path; the
@@ -542,8 +544,10 @@ process.stdin.on("end", () => {
542
544
 
543
545
  // ── Session-model bridge (fail-silent) ─────────────────────────────────────
544
546
  // The PreToolUse effort guard cannot see the model id; it can only read
545
- // effort. Persist {model_id, effort, written_at} here so the guard can grant
546
- // the Opus 5.5 / Fable 5 medium-effort allowance. The statusline re-renders constantly
547
+ // effort. Persist {model_id, effort, session_id, written_at} here: the guard
548
+ // reads it on the Skill call (the Opus 5.5 / Fable 5 medium allowance) and on
549
+ // a TYPED /orc (the effort itself); the session id stops one session's
550
+ // reading gating another (v2.1.2, F13). The statusline re-renders constantly
547
551
  // while a session is active, so written_at stays fresh; the guard treats a
548
552
  // stale file (older than its freshness window) as absent and never blocks on
549
553
  // it. Any error (no dir, read-only fs) is swallowed — this is a nicety, not a
@@ -558,7 +562,7 @@ process.stdin.on("end", () => {
558
562
  fs.mkdirSync(orcDir, { recursive: true });
559
563
  fs.writeFileSync(
560
564
  path.join(orcDir, "session-model.json"),
561
- JSON.stringify({ model_id: model, effort, written_at: Date.now() }) + "\n"
565
+ JSON.stringify({ model_id: model, effort, session_id: String(d.session_id || ""), written_at: Date.now() }) + "\n"
562
566
  );
563
567
  }
564
568
  } catch (_) {}
@@ -1474,6 +1478,9 @@ function extendedScan(d, wants, wantsProvider) {
1474
1478
  try {
1475
1479
  raw = fs.readFileSync(path.join(projectDir, ".claude", "orc.config.yaml"), "utf8");
1476
1480
  } catch (_) {}
1481
+ // `semi` (v2.2.0): kept current, never read by a lane. Still a floor: it
1482
+ // says only `semi`, never fresh, behind or drifted.
1483
+ if (/^[ \t]*code_graph:[ \t]*["']?semi["']?[ \t]*\r?$/m.test(raw)) return { state: "semi" };
1477
1484
  if (!/^[ \t]*code_graph:[ \t]*["']?on["']?[ \t]*\r?$/m.test(raw)) return { state: "off" };
1478
1485
  const meta = readJson(path.join(orc, "graph", "meta.json"));
1479
1486
  if (!meta) return { state: "none" };
@@ -91,7 +91,7 @@
91
91
  * Rich run filenames (v0.32.0): the canonical trace name is
92
92
  * `run-<lane>-<slug>-<DDMMYY>-<HHMMSS>.txt`, written by the orchestrating lane at
93
93
  * run start. The hook still bootstraps its own generic `run-<DDMMYY>-<HHMMSS>.txt`
94
- * when the pointer is missing; the first `orc-trace-writer-haiku-4-5` dispatch
94
+ * when the pointer is missing; the first `orc-trace-writer-haiku-5-high` dispatch
95
95
  * RENAMES that bootstrap file (+ sidecars) and rewrites `.current`. The hook is
96
96
  * name-agnostic (rotation/STALE logic is pointer-based) and holds no open
97
97
  * handles, so a rename between two hook events is safe.
@@ -23,8 +23,8 @@ loaded on demand when the step fires.
23
23
  - `detecting-artifacts.md` — the deterministic wiki/pattern existence probes.
24
24
  - `read-ladder.md` — the escalating read discipline (locate → outline → range →
25
25
  full) for every role that reads code it is not about to edit.
26
- - `code-graph.md` — the local code graph (`orc graph`): when a code-changing
27
- lane consults it, when it updates it, the notes dispatch rule, and the rule
26
+ - `code-graph.md` — the local code graph (`orc graph`): the four touchpoints every
27
+ code lane shares (consult, orient, pack, affected tests), the `update_step` rule, the notes dispatch rule, and the rule
28
28
  that keeps it safe — the graph is a LOCATOR, never the truth. No lane reads
29
29
  its keys; every call carries `--if-enabled`.
30
30
  - `_shared/interview.md` — the interview mechanic (design tree → frontier rounds