@azure-id/orc 1.9.0 → 1.9.2

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 (134) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/README-id.md +57 -59
  3. package/README.md +53 -54
  4. package/bin/build-agents.js +109 -109
  5. package/bin/cli.js +262 -58
  6. package/bin/graph-audit.js +235 -0
  7. package/bin/graph-extract.js +308 -3
  8. package/bin/graph-gain.js +23 -3
  9. package/bin/graph-map.js +9 -1
  10. package/bin/graph-query.js +152 -10
  11. package/bin/graph-shard.js +6 -1
  12. package/bin/graph-signals.js +287 -4
  13. package/bin/graph.js +717 -658
  14. package/bin/onboarding-content.js +4 -4
  15. package/bin/pricing.json +207 -200
  16. package/bin/verify-contracts.js +117 -2
  17. package/bin/verify-package.js +11 -7
  18. package/bin/webui/css/04-motion.css +162 -152
  19. package/bin/webui/css/panels/knowledge.css +563 -116
  20. package/bin/webui/fixtures/extra.js +2036 -2036
  21. package/bin/webui/fixtures/hookui.js +5 -5
  22. package/bin/webui/fixtures/knowledge.js +21 -4
  23. package/bin/webui/fixtures/settings.js +305 -305
  24. package/bin/webui/i18n/en/knowledge.json +122 -4
  25. package/bin/webui/i18n/id/knowledge.json +122 -4
  26. package/bin/webui/js/panels/knowledge.js +573 -9
  27. package/bin/webui/js/panels/overview.js +13 -4
  28. package/mock-run/context-combiner.md +100 -100
  29. package/mock-run/orc-budget.md +534 -534
  30. package/mock-run/orc-challenge-council.md +262 -262
  31. package/mock-run/orc-ultra.md +103 -103
  32. package/package.json +1 -1
  33. package/templates/agents/MODEL-MAPPING.md +43 -43
  34. package/templates/agents/orc-advisor-opus-5-xhigh.md +56 -56
  35. package/templates/agents/orc-analyze-mini-opus-5-med.md +60 -60
  36. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +58 -58
  37. package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -75
  38. package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -110
  39. package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -114
  40. package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -112
  41. package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -132
  42. package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -109
  43. package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -109
  44. package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -90
  45. package/templates/agents/orc-claude-writer-opus-5-med.md +55 -55
  46. package/templates/agents/orc-context-combiner-opus-5-high.md +88 -88
  47. package/templates/agents/orc-doc-checker-opus-5-low.md +108 -108
  48. package/templates/agents/orc-doc-writer-opus-5-med.md +134 -134
  49. package/templates/agents/orc-executor-haiku-4-5.md +2 -1
  50. package/templates/agents/orc-executor-opus-4-7-high.md +2 -1
  51. package/templates/agents/orc-executor-opus-4-7-med.md +2 -1
  52. package/templates/agents/orc-executor-opus-4-8-high.md +2 -1
  53. package/templates/agents/orc-executor-opus-5-high.md +4 -3
  54. package/templates/agents/orc-executor-opus-5-low.md +4 -3
  55. package/templates/agents/orc-executor-opus-5-med.md +4 -3
  56. package/templates/agents/orc-executor-sonnet-4-6-high.md +2 -1
  57. package/templates/agents/orc-executor-sonnet-4-6-med.md +2 -1
  58. package/templates/agents/orc-executor-sonnet-5-high.md +2 -1
  59. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -1
  60. package/templates/agents/orc-judge-opus-5-xhigh.md +85 -85
  61. package/templates/agents/orc-learn-writer-opus-5-low.md +73 -73
  62. package/templates/agents/orc-pattern-codifier-opus-5-med.md +65 -65
  63. package/templates/agents/orc-planner-mini-opus-5-med.md +4 -4
  64. package/templates/agents/orc-planner-mini-sonnet-5-high.md +1 -1
  65. package/templates/agents/orc-planner-opus-5-med.md +160 -160
  66. package/templates/agents/orc-recon-opus-5-low.md +18 -8
  67. package/templates/agents/orc-recon-sonnet-4-6-med.md +15 -5
  68. package/templates/agents/orc-retro-opus-5-med.md +3 -3
  69. package/templates/agents/orc-reviewer-opus-5-med.md +60 -60
  70. package/templates/agents/orc-scout-opus-5-low.md +40 -40
  71. package/templates/agents/orc-system-analyst-opus-5-high.md +120 -120
  72. package/templates/agents/orc-test-author-opus-5-med.md +71 -71
  73. package/templates/agents/orc-test-designer-opus-5-high.md +158 -158
  74. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -130
  75. package/templates/agents/orc-verifier-opus-5-med.md +69 -69
  76. package/templates/agents/orc-wiki-scanner-opus-5-med.md +81 -81
  77. package/templates/commands/orc-ultra.md +17 -17
  78. package/templates/commands/orc-verify.md +11 -11
  79. package/templates/hooks/README.md +13 -3
  80. package/templates/hooks/orc-effort-guard.js +178 -178
  81. package/templates/hooks/orc-graph-hook.js +53 -7
  82. package/templates/hooks/orc-statusline.js +6 -6
  83. package/templates/skills/_shared/code-graph.md +57 -18
  84. package/templates/skills/_shared/config-precedence.md +198 -198
  85. package/templates/skills/_shared/extra-dispatch.md +1346 -1346
  86. package/templates/skills/_shared/opus5-only.md +8 -8
  87. package/templates/skills/_shared/phases/analyst-gates.md +136 -136
  88. package/templates/skills/_shared/phases/execution.md +4 -4
  89. package/templates/skills/_shared/phases/planning.md +3 -3
  90. package/templates/skills/_shared/phases/preflight.md +1 -1
  91. package/templates/skills/_shared/phases/review.md +4 -2
  92. package/templates/skills/_shared/phases/ship.md +1 -1
  93. package/templates/skills/_shared/phases/trace.md +3 -2
  94. package/templates/skills/_shared/read-ladder.md +14 -1
  95. package/templates/skills/_shared/return-validation.md +1 -1
  96. package/templates/skills/context-combiner/SKILL.md +1 -1
  97. package/templates/skills/context-combiner/schemas/combined-report.md +78 -78
  98. package/templates/skills/orc/README.md +2 -2
  99. package/templates/skills/orc/SKILL.md +8 -8
  100. package/templates/skills/orc/config.md +9 -9
  101. package/templates/skills/orc/examples/full-run-mock.md +73 -73
  102. package/templates/skills/orc/references/effort-and-mode.md +222 -222
  103. package/templates/skills/orc/references/preflight-report.md +2 -2
  104. package/templates/skills/orc/references/ultra-mode.md +3 -3
  105. package/templates/skills/orc/subskills/orc-planner/SKILL.md +2 -2
  106. package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -121
  107. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +76 -76
  108. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +45 -45
  109. package/templates/skills/orc-analyze/SKILL.md +2 -2
  110. package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -42
  111. package/templates/skills/orc-analyze/schemas/report-audit.md +83 -83
  112. package/templates/skills/orc-analyze/schemas/report-prose.md +63 -63
  113. package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -78
  114. package/templates/skills/orc-challenge/README.md +142 -142
  115. package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -273
  116. package/templates/skills/orc-challenge/references/council.md +315 -315
  117. package/templates/skills/orc-challenge/references/intake.md +171 -171
  118. package/templates/skills/orc-claude/SKILL.md +14 -14
  119. package/templates/skills/orc-diy/README.md +143 -143
  120. package/templates/skills/orc-diy/references/flow-schema.md +1 -1
  121. package/templates/skills/orc-doc/README.md +229 -229
  122. package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -325
  123. package/templates/skills/orc-doc/references/chunking.md +527 -527
  124. package/templates/skills/orc-fast/SKILL.md +4 -4
  125. package/templates/skills/orc-learn/SKILL.md +14 -14
  126. package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -61
  127. package/templates/skills/orc-mini/SKILL.md +16 -20
  128. package/templates/skills/orc-mini/references/complexity.md +25 -11
  129. package/templates/skills/orc-quick/SKILL.md +12 -13
  130. package/templates/skills/orc-quick/references/gh-mode.md +1 -1
  131. package/templates/skills/orc-quick/references/look.md +5 -5
  132. package/templates/skills/orc-verify/SKILL.md +15 -15
  133. package/templates/skills/orc-verify/examples/verify-mock.md +33 -33
  134. package/templates/skills/orc-wiki/references/extra.md +1 -1
package/bin/cli.js CHANGED
@@ -180,8 +180,10 @@ function writeStdoutSync(str) {
180
180
  }
181
181
 
182
182
  // Print the object and (optionally) exit with the human path's code.
183
- function emitJson(obj, exitCode) {
184
- writeStdoutSync(JSON.stringify(obj, null, 2) + "\n");
183
+ // `compact` (v1.9.1 B1) writes the answer with no indentation: a `--brief`
184
+ // answer is read by a model, and the card inside it carries its own newlines.
185
+ function emitJson(obj, exitCode, compact) {
186
+ writeStdoutSync((compact ? JSON.stringify(obj) : JSON.stringify(obj, null, 2)) + "\n");
185
187
  if (exitCode !== undefined) process.exit(exitCode);
186
188
  }
187
189
 
@@ -1065,6 +1067,8 @@ function upgrade() {
1065
1067
  // ---------------------------------------------------------------------------
1066
1068
 
1067
1069
  const KNOWN_MODELS = [
1070
+ "claude-opus-5-5",
1071
+ // Kept so a config written before v1.9.2 still validates.
1068
1072
  "claude-opus-5",
1069
1073
  "claude-opus-4-8",
1070
1074
  "claude-opus-4-7",
@@ -1251,7 +1255,7 @@ const CONFIG_FAMILIES = {
1251
1255
  // without being a CONFIG_META key, because where it sits in the
1252
1256
  // precedence is a fact even though `orc config set` refuses to write it.
1253
1257
  registry_less: true,
1254
- shadow_note: "shadowed by {by} — executors use the fixed 2-band Opus 5 ladder",
1258
+ shadow_note: "shadowed by {by} — executors use the fixed 2-band Opus 5.5 ladder",
1255
1259
  },
1256
1260
  { prio: "P3", key: null, terminal: "the shipped score→model table" },
1257
1261
  ],
@@ -1438,7 +1442,7 @@ const CONFIG_META = [
1438
1442
  // decision with a recorded reason (the /orc-pact retirement rule).
1439
1443
  { key: "extra_demote_after", def: 2, tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: [], validate: vInt(0), options: [0, 2, 3, 5], desc: "Consecutive `stalled` dispatches on ONE profile, inside one run, before that profile is DEMOTED to the bottom of the ladder for the rest of the run — so `opus5_only` (or the shipped score table) becomes the effective P0 and the work stays on Claude. Only `stalled` counts: a 401 or a rate limit has its own answer (`extra_on_failure`, the vault, `extra_resume`), and demoting on one would hide a credential problem behind a routing change. A resume of the same stalled attempt is the SAME stall, never a second one. A demotion is RUN state — it never writes your config — it is ANNOUNCED before the next dispatch, and it is never auto-promoted back: `orc extra promote <run> --reason \"<why>\"`. 0 turns this clock off and leaves only extra_demote_stale_min." },
1440
1444
  { key: "extra_demote_stale_min", def: 20, tier: "common", answers: [{ family: "extra", prio: "P2", mode: "replace" }], gated_by: "extra_enabled", lanes: [], validate: vInt(0), options: [0, 10, 20, 45], desc: "Minutes a LIVE foreign attempt may show no observable progress before its profile is demoted. This is a different question from `extra_stall_s`, which stops ONE dispatch after 180s of silence: this clock is about the RUN — two workers in flight, both quiet, and a wave that is going nowhere — so the two have their own budgets and their own off values and neither is a simplification of the other. It reads the journal's own progress file on disk, never a remembered fact. 0 turns this clock off and leaves only extra_demote_after." },
1441
- { key: "opus5_only", def: false, tier: "common", answers: [{ family: "executor-band", prio: "P1", mode: "replace" }, { family: "fixed-role-model", prio: "P1", mode: "replace" }], lanes: ["orc", "orc-analyze", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-pattern", "orc-quick", "orc-retro", "orc-wiki"], validate: vEnum("true", "false"), options: ["true", "false"], desc: "EVERY dispatched role uses ONE model — Opus 5 — with EFFORT as the cost dial (executors: [0,40) low · [40,80) medium · [80,100] high; each fixed role its own pinned effort). Deep SWE-benchmark work on cost vs efficiency across Claude models finds a single Opus 5 agent with the effort ladder the most efficient setup. It FORCES: while on it outranks a hand-written rubric_bands_override. Needs an Opus 5 main session or EVERY dispatch silently downgrades. Excludes the Haiku trace writer and orc-diy (compile-owned)." },
1445
+ { key: "opus5_only", def: false, tier: "common", answers: [{ family: "executor-band", prio: "P1", mode: "replace" }, { family: "fixed-role-model", prio: "P1", mode: "replace" }], lanes: ["orc", "orc-analyze", "orc-challenge", "orc-claude", "orc-diy", "orc-doc", "orc-fast", "orc-mini", "orc-pattern", "orc-quick", "orc-retro", "orc-wiki"], validate: vEnum("true", "false"), options: ["true", "false"], desc: "EVERY dispatched role uses ONE model — Opus 5.5 — with EFFORT as the cost dial (executors: [0,40) low · [40,80) medium · [80,100] high; each fixed role its own pinned effort). Deep SWE-benchmark work on cost vs efficiency across Claude models finds a single Opus 5.5 agent with the effort ladder the most efficient setup. It FORCES: while on it outranks a hand-written rubric_bands_override. Needs an Opus 5.5 main session or EVERY dispatch silently downgrades. Excludes the Haiku trace writer and orc-diy (compile-owned)." },
1442
1446
  // --- v0.46.0 — the six new lanes ------------------------------------------
1443
1447
  { key: "pact_gate", def: "warn", tier: "common", answers: [{ family: "pact", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pact"], validate: vEnum("off", "warn"), options: ["off", "warn"], desc: "Invariant ledger at Phase 1 + planning: warn = print the one pact line and inject a DRIFTED/BROKEN promise whose anchors intersect the plan's declared files as a planner constraint; off = nothing. NEVER blocks — a promise is advice with a receipt, not a gate. See /orc-pact." },
1444
1448
  { key: "pact_recheck_on_verify", def: "true", tier: "common", answers: [{ family: "pact", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pact"], validate: vEnum("true", "false"), options: ["true", "false"], desc: "Phase 6: re-run the cheap checks for ONLY the invariants the change touched (`orc pact check`), so a promise that just leaked is caught in the run that broke it." },
@@ -1521,7 +1525,7 @@ const LEGACY_KEYS = {
1521
1525
  // So `config list` prints them, `config list --json` carries them, and
1522
1526
  // `config set` refuses them BY NAME instead of with a generic unknown-key list.
1523
1527
  const RETIRED_KEYS = {
1524
- fable5_enabled: { removed_in: "1.0.0", why: "the Fable 5 role override was removed — every role dispatches its shipped Claude agent, or the Opus 5 variant under opus5_only" },
1528
+ fable5_enabled: { removed_in: "1.0.0", why: "the Fable 5 role override was removed — every role dispatches its shipped Claude agent, or the Opus 5.5 variant under opus5_only" },
1525
1529
  fable5_roles: { removed_in: "1.0.0", why: "the Fable 5 role override was removed" },
1526
1530
  fable5_effort: { removed_in: "1.0.0", why: "the Fable 5 role override was removed" },
1527
1531
  };
@@ -2080,7 +2084,7 @@ function shadowReason(key, map, claudeDir) {
2080
2084
  //
2081
2085
  // v1.0.0 W4 — TWO BANDS, not three, and the same 90 edge as the default table's
2082
2086
  // top two rows (D13). That symmetry is the point: once the default table's high
2083
- // end is already Opus 5 with effort as the dial, the forcing mode differs from
2087
+ // end is already Opus 5.5 with effort as the dial, the forcing mode differs from
2084
2088
  // it only BELOW 65, so a third band here would be a distinction the default
2085
2089
  // table stopped making.
2086
2090
  //
@@ -2126,7 +2130,7 @@ function scoreTableJson(map, claudeDir) {
2126
2130
  // > the default 6-band table
2127
2131
  //
2128
2132
  // Extra is an OVERLAY, not a replacement, which is what makes "cheap grunt
2129
- // work goes to DeepSeek, hard work stays on Opus 5" a two-command setup
2133
+ // work goes to DeepSeek, hard work stays on Opus 5.5" a two-command setup
2130
2134
  // rather than a full table rewrite. `active` can therefore read as a
2131
2135
  // COMPOSITE — because the truth is a composite, and a single word would be
2132
2136
  // a lie about what the next dispatch will do.
@@ -2371,7 +2375,7 @@ function extraLegacyRoleWarn(value) {
2371
2375
 
2372
2376
  // A shadowed setting must never be silent — the v0.36.0 rule, applied to a
2373
2377
  // shadow that runs BOTH WAYS. Turning on opus5_only while route rows exist
2374
- // does not give you the Opus 5 ladder everywhere, and turning on Extra does
2378
+ // does not give you the Opus 5.5 ladder everywhere, and turning on Extra does
2375
2379
  // not take the Claude table away; saying either would be wrong. So name the
2376
2380
  // ranges, which is the only statement that is true from both sides.
2377
2381
  function extraShadowNotice(claudeDir) {
@@ -2432,17 +2436,17 @@ function opus5Notice(on, claudeDir) {
2432
2436
  console.log(
2433
2437
  "\n " + ui.color.bold("Opus-5-only dispatch is now ACTIVE — for EVERY role, not just executors.") + "\n" +
2434
2438
  "\n Why: deep SWE-benchmark work on cost vs efficiency across Claude models finds a\n" +
2435
- " single Opus 5 agent, with the EFFORT ladder as the cost dial, the most efficient\n" +
2439
+ " single Opus 5.5 agent, with the EFFORT ladder as the cost dial, the most efficient\n" +
2436
2440
  " configuration. You trade model-class variety for effort variety.\n" +
2437
2441
  "\n Scored executors (/orc + /orc-ultra):\n\n" +
2438
2442
  ladderTable(OPUS5_SCORE_TABLE) +
2439
2443
  "\n Fixed roles now dispatched instead of their defaults:\n\n" + roleRows + "\n" +
2440
- "\n Already Opus 5, unchanged: analyst · planner · reviewer · verifier · test-author\n" +
2444
+ "\n Already Opus 5.5, unchanged: analyst · planner · reviewer · verifier · test-author\n" +
2441
2445
  " · combiner · learn-writer · advisor · judge.\n" +
2442
2446
  " NEVER forced: orc-trace-writer-haiku-4-5 (it transcribes a packet, no reasoning)\n" +
2443
2447
  " and orc-diy (its table is compile-owned — re-run `orc diy compile` to change it).\n" +
2444
2448
  "\n " + ui.mark.warn("Tier requirement — read this one:") + "\n" +
2445
- " Today only the [90,100] executor band needs an Opus 5 main session. With this ON,\n" +
2449
+ " Today only the [90,100] executor band needs an Opus 5.5 main session. With this ON,\n" +
2446
2450
  " EVERY dispatch does. A subagent can never outrank the main session, so on a lower\n" +
2447
2451
  " session every role silently falls back to the session model and the tier-honesty\n" +
2448
2452
  " rule reports a downgrade on EVERY return instead of occasionally. Hooks cannot\n" +
@@ -2858,17 +2862,17 @@ const LANE_INERT = {
2858
2862
  { key: "extra_fallback_agent", reason: "re-opening the gate IS the ask, so a second menu composed from a config key would be the same question twice in different words" },
2859
2863
  ],
2860
2864
  "orc-challenge": [
2861
- { key: "opus5_only", reason: "every agent in this lane is already claude-opus-5, so this is a no-op — the lane is unaffected, not exempt" },
2865
+ { key: "opus5_only", reason: "every agent in this lane is already claude-opus-5-5, so this is a no-op — the lane is unaffected, not exempt" },
2862
2866
  ],
2863
2867
  "orc-doc": [
2864
- { key: "opus5_only", reason: "both agents in this lane are already claude-opus-5, so this is a no-op — the lane is unaffected, not exempt" },
2868
+ { key: "opus5_only", reason: "both agents in this lane are already claude-opus-5-5, so this is a no-op — the lane is unaffected, not exempt" },
2865
2869
  ],
2866
- // v1.5.0 — both agents in this lane ship as claude-opus-5, and the
2870
+ // v1.5.0 — both agents in this lane ship as claude-opus-5-5, and the
2867
2871
  // interpreter's `low` is a MEASUREMENT choice rather than a cost one: a
2868
2872
  // harder-thinking interpreter reasons its way to why a leaked stack trace is
2869
2873
  // probably fine in staging, which is exactly the gap it exists to find.
2870
2874
  "orc-test": [
2871
- { key: "opus5_only", reason: "both agents in this lane are already claude-opus-5, so this is a no-op — the lane is unaffected, not exempt" },
2875
+ { key: "opus5_only", reason: "both agents in this lane are already claude-opus-5-5, so this is a no-op — the lane is unaffected, not exempt" },
2872
2876
  ],
2873
2877
  // v1.1.0 W2 — this lane DISPATCHES NOTHING. A detached command does the
2874
2878
  // waiting, so every family that answers "which model runs this" has no work
@@ -3143,7 +3147,7 @@ function laneAnnounce(lane, map, claudeDir, families) {
3143
3147
  out.push(
3144
3148
  off
3145
3149
  ? `opus5_only: ON, and INERT in this lane — ${off.reason}`
3146
- : "opus5_only: ON — every dispatched role uses Opus 5, with effort as the only cost dial"
3150
+ : "opus5_only: ON — every dispatched role uses Opus 5.5, with effort as the only cost dial"
3147
3151
  );
3148
3152
  }
3149
3153
  if (isTrue(map.extra_enabled)) {
@@ -3182,9 +3186,10 @@ function laneAnnounce(lane, map, claudeDir, families) {
3182
3186
  const graphOn = String(Object.prototype.hasOwnProperty.call(map, "code_graph") ? map.code_graph : metaFor("code_graph").def) === "on";
3183
3187
  if (graphOn && LANE_CALLS["graph-status"].lanes.includes(lane))
3184
3188
  out.push(
3185
- "graph: on — use the code-graph cache: (1) now: `orc graph status --if-enabled --heal --json` (builds or updates it); " +
3186
- "(2) every code-writing slice: `orc graph ctx <declared files> --if-enabled --json` cards; " +
3187
- "(3) after every code change: `orc graph update --if-enabled --json` (keeps it for the next run). Copy each `line` and `trace` verbatim"
3189
+ "graph: on — use the code-graph cache: (1) now: `orc graph status --if-enabled --heal --json --brief` (builds or updates it); " +
3190
+ "(2) every code-writing slice: `orc graph ctx <declared files> --if-enabled --json --brief` cards; " +
3191
+ "(3) after every code change: `orc graph update --if-enabled --json --brief` (keeps it for the next run). " +
3192
+ "Copy each `line` and `trace` verbatim; `--brief` drops the rows you never print"
3188
3193
  );
3189
3194
  // A shadowed setting must never be silent, and neither must an inert one.
3190
3195
  const shadowed = [];
@@ -3480,7 +3485,24 @@ const LANE_CALLS = {
3480
3485
  on_absent: "exit 4 is an ANSWER, not a miss — plan exactly as before",
3481
3486
  canonical: "_shared/code-graph.md",
3482
3487
  never: "never read a co-change as a DEPENDENCY — it says what people changed together, never what needs what",
3483
- lanes: ["orc", "orc-diy", "orc-mini"],
3488
+ // v1.9.1: mini no longer calls it. `impact --complexity` answers the same
3489
+ // question inside the SAME process, against the same per-HEAD cache.
3490
+ lanes: ["orc", "orc-diy"],
3491
+ },
3492
+ // v1.9.1 A2 — a USER command. `lanes: []` is the contract: nothing a lane
3493
+ // does is made better by an audit, and a lane that ran one would pay for an
3494
+ // answer about the PARSER in a context that is about the code.
3495
+ "graph-audit": {
3496
+ cmd: "orc graph audit [--top=N] --if-enabled [--json]",
3497
+ what: "WHY the map is thin — density by language, which extractor read each one, the files the parser read as EMPTY longest first, and what their first declaration looks like",
3498
+ exits: { 0: "answered", 1: "no index yet", 3: "off — `code_graph` is off" },
3499
+ states: null,
3500
+ cost: "free",
3501
+ when: "by hand, after `orc graph status` says THIN — never in a lane",
3502
+ on_absent: "exit 1 means there is no graph to audit; build one first",
3503
+ canonical: "_shared/code-graph.md",
3504
+ never: "never read an empty file as a defect — a component with no named function is a file the parser read correctly; only a shape marked `parser gap` is worth an issue",
3505
+ lanes: [],
3484
3506
  },
3485
3507
  "graph-gain": {
3486
3508
  cmd: "orc graph gain --run <trace name> --if-enabled [--json]",
@@ -4962,7 +4984,7 @@ const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
4962
4984
  // The tier-clip roster. It must contain EVERY agent the default table names —
4963
4985
  // `diyScoreTable()` looks each row's agent up here, so a table row naming an
4964
4986
  // agent this map does not know is a crash, not a fallback. v1.0.0 W4 added the
4965
- // two Opus 5 rows for exactly that reason.
4987
+ // two Opus 5.5 rows for exactly that reason.
4966
4988
  //
4967
4989
  // It also feeds `fixed_executor`'s option list, so every entry here is offerable
4968
4990
  // — which is what keeps the four agents no band names reachable (D14).
@@ -4994,10 +5016,10 @@ const DIY_TIERS = {
4994
5016
  "opus-4-8-high": { model: 5, effort: 2, modelId: "claude-opus-4-8", effortName: "high" },
4995
5017
  "opus-4-8-xhigh": { model: 5, effort: 3, modelId: "claude-opus-4-8", effortName: "xhigh" },
4996
5018
  "opus-4-8-max": { model: 5, effort: 4, modelId: "claude-opus-4-8", effortName: "max" },
4997
- "opus-5-med": { model: 6, effort: 1, modelId: "claude-opus-5", effortName: "medium" },
4998
- "opus-5-high": { model: 6, effort: 2, modelId: "claude-opus-5", effortName: "high" },
4999
- "opus-5-xhigh": { model: 6, effort: 3, modelId: "claude-opus-5", effortName: "xhigh" },
5000
- "opus-5-max": { model: 6, effort: 4, modelId: "claude-opus-5", effortName: "max" },
5019
+ "opus-5-med": { model: 6, effort: 1, modelId: "claude-opus-5-5", effortName: "medium" },
5020
+ "opus-5-high": { model: 6, effort: 2, modelId: "claude-opus-5-5", effortName: "high" },
5021
+ "opus-5-xhigh": { model: 6, effort: 3, modelId: "claude-opus-5-5", effortName: "xhigh" },
5022
+ "opus-5-max": { model: 6, effort: 4, modelId: "claude-opus-5-5", effortName: "max" },
5001
5023
  "fable-5-med": { model: 7, effort: 1, modelId: "claude-fable-5", effortName: "medium" },
5002
5024
  "fable-5-high": { model: 7, effort: 2, modelId: "claude-fable-5", effortName: "high" },
5003
5025
  "fable-5-xhigh": { model: 7, effort: 3, modelId: "claude-fable-5", effortName: "xhigh" },
@@ -5234,15 +5256,15 @@ function diyValidate(cfg) {
5234
5256
  if (cfg.autonomy === "hands-off" && (cfg.ship_mode === "commit" || cfg.ship_mode === "pr")) {
5235
5257
  warnings.push(`hands-off + ship_mode ${cfg.ship_mode}: git actions will run fully unattended`);
5236
5258
  }
5237
- // The pinned reviewer/verifier moved to claude-opus-5 (model rank 6) in
5259
+ // The pinned reviewer/verifier moved to claude-opus-5-5 (model rank 6) in
5238
5260
  // v0.34.0 — anything below that tier silently runs them at the session model.
5239
5261
  if (tier && tier.model < 6 && (cfg.review !== "off" || cfg.verify !== "off")) {
5240
5262
  // Careful wording (v0.34.7): `session_tier` is a DECLARATION, not the real
5241
5263
  // session model — a hook cannot read the model, only the effort. On a
5242
- // session that actually outranks the declared tier the pinned Opus 5 roles
5264
+ // session that actually outranks the declared tier the pinned Opus 5.5 roles
5243
5265
  // run at FULL pin (observed), so asserting they "will" downgrade is a false
5244
5266
  // alarm; what is certain is only that a LOWER real session caps them.
5245
- warnings.push(`session_tier ${cfg.session_tier} is below the pinned Opus 5 reviewer/verifier: if the REAL session is also below Opus 5 they run capped at it (the tier-honesty rule reports the actual model). A session above this tier runs them at full pin — but your executor table stays clipped to ${cfg.session_tier}; recompile to use the full ladder`);
5267
+ warnings.push(`session_tier ${cfg.session_tier} is below the pinned Opus 5.5 reviewer/verifier: if the REAL session is also below Opus 5.5 they run capped at it (the tier-honesty rule reports the actual model). A session above this tier runs them at full pin — but your executor table stays clipped to ${cfg.session_tier}; recompile to use the full ladder`);
5246
5268
  }
5247
5269
  return { errors, warnings };
5248
5270
  }
@@ -7626,7 +7648,7 @@ const WIKI_TIER_LADDER = [
7626
7648
  { id: "small-delta", tier: "light", why: "small delta, no new surface" },
7627
7649
  ];
7628
7650
 
7629
- // The POSITION each tier resolves to (v0.55.0). Two slots and ONE Opus 5 agent
7651
+ // The POSITION each tier resolves to (v0.55.0). Two slots and ONE Opus 5.5 agent
7630
7652
  // is not a contradiction: a slot names the POSITION, not the model, and the
7631
7653
  // ladder already collapses both tiers onto that agent while `opus5_only` is on.
7632
7654
  const WIKI_TIER_SLOT = { deep: "wiki-scanner-deep", light: "wiki-scanner-light" };
@@ -8742,6 +8764,14 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
8742
8764
  paid.source = K.tok(r.source.text);
8743
8765
  paid.card = Math.max(0, paid.card - paid.source);
8744
8766
  }
8767
+ // R1 (v1.9.1): the callers' blocks are source the same way — exactly the
8768
+ // lines a range read of each caller would have returned.
8769
+ const callerBlocks = Array.isArray(r.callers_source) ? r.callers_source : [];
8770
+ if (callerBlocks.length) {
8771
+ const t = callerBlocks.reduce((a, b) => a + K.tok(b.text || ""), 0);
8772
+ paid.source += t;
8773
+ paid.card = Math.max(0, paid.card - t);
8774
+ }
8745
8775
  const base = { cmd: sub, targets: operands.slice(0, 5), gen, ms, paid };
8746
8776
 
8747
8777
  if (sub === "impact" || (sub === "ctx" && forSlice)) {
@@ -8773,6 +8803,7 @@ function gainRowFor(sub, Q, model, operands, r, ms, forSlice) {
8773
8803
  const hits = Q.nameHits(model, r.target.qname);
8774
8804
  const c = K.ctxSymbol(model, r.target, hits, (r.callers || []).map((x) => x.file));
8775
8805
  if (r.source) c.avoided = addAvoided(c.avoided, K.sourceAvoided(model, r.source.file, r.source.from, r.source.to));
8806
+ for (const b of callerBlocks) c.avoided = addAvoided(c.avoided, K.sourceAvoided(model, b.file, b.from, b.to));
8776
8807
  return { ...base, ...c };
8777
8808
  }
8778
8809
  return { ...base, avoided: { low: 0, high: 0, calls_low: 0, calls_high: 0 }, basis: { files: [], grep_hits: 0 } };
@@ -8815,23 +8846,79 @@ function graphGainAppend(claudeDir, row) {
8815
8846
  // A thin router. The engine is `bin/graph.js`; this function resolves the
8816
8847
  // config (the engine never reads it) and owns the human lines. The exit code is
8817
8848
  // the same on the human path and the `--json` path (S7).
8849
+ // ── B1 (v1.9.1): the BRIEF answer ─────────────────────────────────────────
8850
+ // The orchestrator reads a graph answer on the main-session surface, where it
8851
+ // is re-sent on every later turn — and it prints the `card`, never the rows.
8852
+ // Measured on this tree the rows are 4 to 8 times the card. `--brief` keeps
8853
+ // every fact a lane prints and drops every row array it does not.
8854
+ //
8855
+ // Two rules, in this order:
8856
+ // 1. an array NAMED here is dropped (`calls[]` and `names[]` are strings, and
8857
+ // they are still rows);
8858
+ // 2. any other array OF OBJECTS is dropped, unless it IS the answer
8859
+ // (`nearest[]` and `candidates[]` on an exit-4; `cards[]` on a multi-target
8860
+ // `ctx`, whose rows are three scalars each).
8861
+ // Every drop leaves its COUNT behind, at the level it was dropped from, except
8862
+ // where the answer already carries that count under another name.
8863
+ const GRAPH_BRIEF_DROP = new Set(["callers", "calls", "effects", "symbols", "imports", "importers", "blocks", "names", "rows", "chain", "waiting"]);
8864
+ const GRAPH_BRIEF_KEEP = new Set(["nearest", "candidates", "cards"]);
8865
+ // V2/V3 answers that ARE the answer: `complexity` and `facts` are ten rows at
8866
+ // most, and they are the whole reason the call was made. They pass through
8867
+ // whole, never walked.
8868
+ const GRAPH_BRIEF_WHOLE = new Set(["complexity", "facts"]);
8869
+ const GRAPH_BRIEF_NO_COUNT = { impact: ["callers"], map: ["files"], changes: ["symbols"] };
8870
+
8871
+ function briefOf(sub, obj) {
8872
+ const noCount = new Set(GRAPH_BRIEF_NO_COUNT[sub] || []);
8873
+ const walk = (o) => {
8874
+ const out = {};
8875
+ const counts = {};
8876
+ for (const [k, v] of Object.entries(o)) {
8877
+ if (Array.isArray(v)) {
8878
+ const rows = GRAPH_BRIEF_DROP.has(k) || (!GRAPH_BRIEF_KEEP.has(k) && v.length > 0 && v[0] !== null && typeof v[0] === "object");
8879
+ if (!rows) out[k] = v;
8880
+ else if (!noCount.has(k)) counts[k] = v.length;
8881
+ continue;
8882
+ }
8883
+ if (k === "source" && v && typeof v === "object") {
8884
+ const from = Number(v.from);
8885
+ const to = Number(v.to);
8886
+ counts.source_lines = Number.isFinite(from) && Number.isFinite(to) ? Math.max(0, to - from + 1) : 0;
8887
+ continue;
8888
+ }
8889
+ out[k] = v && typeof v === "object" && !GRAPH_BRIEF_WHOLE.has(k) ? walk(v) : v;
8890
+ }
8891
+ if (Object.keys(counts).length) out.counts = { ...(out.counts || {}), ...counts };
8892
+ return out;
8893
+ };
8894
+ return walk(obj);
8895
+ }
8896
+
8818
8897
  function graphCmd() {
8819
8898
  const usage =
8820
- "Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc\n" +
8821
- " orc graph ctx <symbol|file[:line]>… [--source [N]] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
8822
- " orc graph coverage <file…> | changes [--base <ref>] | cochange <file>\n" +
8899
+ "Usage: orc graph status [--heal] | update [--notes-pending --files <a,b>] | gc | audit [--top=N]\n" +
8900
+ " orc graph ctx <symbol|file[:line]>… [--source [N]] [--callers-source] | ctx --for-slice <file…> | impact <file…> | path <from> <to>\n" +
8901
+ " orc graph coverage <file…> | changes [--base <ref>] [--files=<a,b>] | cochange <file>\n" +
8823
8902
  " orc graph map [--focus <file|name,…>] [--budget N]\n" +
8824
8903
  " orc graph gain [--run <name>] [--since 7d] [--history [--limit 40]] [--measured] [--reset --yes]\n" +
8825
8904
  " orc graph notes pending --files <a,b> [--cap 40] [--min 5] [--with-source] | notes apply <file|->\n" +
8826
- " [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--dir <path>]\n" +
8905
+ " [--if-enabled] [--depth N] [--budget TOKENS] [--format prose|tree] [--offset N] [--json] [--brief] [--dir <path>]\n" +
8906
+ " --brief with --json: the card, the line, the trace, every scalar and every count — and none of the row\n" +
8907
+ " arrays a lane never prints. Compact output. Without --brief, --json is unchanged.\n" +
8827
8908
  " status exit 0 FRESH · 1 NONE · 2 DRIFTED · 3 OFF (--heal builds or updates a NONE/DRIFTED graph first)\n" +
8828
8909
  " update exit 0 done · 1 unavailable or locked · 3 off (with --if-enabled)\n" +
8829
8910
  " --notes-pending adds the `notes pending` answer to the SAME call (one process, one lock)\n" +
8830
8911
  " gc exit 0 done · 1 no index or locked\n" +
8912
+ " audit exit 0 answered · 1 no index · 3 off (with --if-enabled) WHY the map is thin: density by language,\n" +
8913
+ " the files the parser read as EMPTY, longest first, and what their first declaration looks like\n" +
8831
8914
  " ctx exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 not found or ambiguous\n" +
8832
8915
  " --source [N] appends the target's own lines (default 80, cap 200), charged to the same budget\n" +
8916
+ " --callers-source appends 6 lines around each CONFIDENT call site (≤ 5, never the target's own\n" +
8917
+ " file), charged to the same budget after everything else; symbol cards only\n" +
8833
8918
  " --for-slice prints only the OUTSIDE view of each declared file — no symbol table\n" +
8834
8919
  " impact exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 none of the files is in the graph\n" +
8920
+ " --complexity [--risk=<class>[@<file:line>],…] adds the ONE line that says whether this change\n" +
8921
+ " fits one executor, with the numbers behind it — impact and co-change in ONE call\n" +
8835
8922
  " path exit 0 found · 1 no graph · 3 off (with --if-enabled) · 4 no confident path, or an unknown end\n" +
8836
8923
  " map exit 0 always when a graph exists · 1 no graph · 3 off (with --if-enabled) the files by RANK, most\n" +
8837
8924
  " connected first, each with its top symbols; --focus re-ranks the repository around those files or names\n" +
@@ -8852,6 +8939,16 @@ function graphCmd() {
8852
8939
  const root = repoRootOf(claudeDir);
8853
8940
  const sub = positionals()[1];
8854
8941
  const asJson = wantsJson();
8942
+ // B1: a BARE switch, read from `args`. `flag()` reads the next word as a
8943
+ // value, so `--brief` there would swallow the first operand.
8944
+ let briefOn = args.includes("--brief");
8945
+ // Every flag this release adds that carries a VALUE is written
8946
+ // `--name=value`, for the same reason: one word, so a CLI that does not
8947
+ // know the flag skips it whole instead of losing an operand behind it.
8948
+ const eqFlag = (name) => {
8949
+ const hit = args.find((a) => a.startsWith(name + "="));
8950
+ return hit === undefined ? undefined : hit.slice(name.length + 1);
8951
+ };
8855
8952
  const ovr = readOverride(claudeDir).map;
8856
8953
  const cfg = (k) => (Object.prototype.hasOwnProperty.call(ovr, k) ? ovr[k] : metaFor(k).def);
8857
8954
  const enabled = String(cfg("code_graph")) === "on";
@@ -8930,7 +9027,12 @@ function graphCmd() {
8930
9027
  return null;
8931
9028
  }
8932
9029
  };
8933
- const finish = (r, line, trace, jsonLine = true) => {
9030
+ // B3 (v1.9.1): the ledger append moved INSIDE `finish`, because the one
9031
+ // number the meter called EXACT — the card — was 4 to 8 times too small. The
9032
+ // ENVELOPE is everything the answer carries beyond its card, and it can only
9033
+ // be counted once the text that will be written exists. The append is still
9034
+ // fail-quiet and still happens after the answer is built, never before it.
9035
+ const finish = (r, line, trace, jsonLine = true, gainRow = null) => {
8934
9036
  const { exit, ...obj } = r;
8935
9037
  if (obj.generation === undefined) {
8936
9038
  const m = metaNow();
@@ -8939,7 +9041,19 @@ function graphCmd() {
8939
9041
  obj.gen_id = m.gen_id || null;
8940
9042
  }
8941
9043
  }
8942
- if (asJson) emitJson({ ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) }, exit);
9044
+ const full = { ...obj, ...(line && jsonLine ? { line } : {}), ...(trace ? { trace } : {}) };
9045
+ const payload = asJson && briefOn ? briefOf(sub, full) : full;
9046
+ if (gainRow) {
9047
+ try {
9048
+ const K = require("./graph-gain.js");
9049
+ const text = asJson ? (briefOn ? JSON.stringify(payload) : JSON.stringify(payload, null, 2)) : String(line || "");
9050
+ if (gainRow.paid) gainRow.paid.envelope = Math.max(0, K.tok(text) - K.tok(obj.card || line || ""));
9051
+ graphGainAppend(claudeDir, gainRow);
9052
+ } catch (_) {
9053
+ // K6: the meter is never allowed to fail the read it is measuring.
9054
+ }
9055
+ }
9056
+ if (asJson) emitJson(payload, exit, briefOn);
8943
9057
  if (line) console.log(line);
8944
9058
  process.exit(exit);
8945
9059
  };
@@ -8962,6 +9076,32 @@ function graphCmd() {
8962
9076
  r.auto_update = autoUpdate;
8963
9077
  r.notes = String(cfg("code_graph_notes"));
8964
9078
  if (healed) r.healed = healed;
9079
+ // A1 (v1.9.1): the density, and the BACKFILL for a store built before
9080
+ // this release. A FRESH store never updates, so waiting for the next real
9081
+ // update would leave a long-lived graph without the line forever. It runs
9082
+ // under `--heal` only, once, and it is bounded by the same cap every heal
9083
+ // obeys — the index did not change, so `generation` and `gen_id` do not.
9084
+ const SIGD = require("./graph-signals.js");
9085
+ let meta = metaNow();
9086
+ if (flag("--heal") === true && enabled && meta && !meta.density && r.exists) {
9087
+ const capMs = Number(cfg("code_graph_heal_ms")) || 0;
9088
+ if (!capMs || !meta.update_ms || meta.update_ms <= capMs) {
9089
+ try {
9090
+ const p = G.graphPaths(claudeDir);
9091
+ const idx = JSON.parse(fs.readFileSync(p.index, "utf8"));
9092
+ const fl = JSON.parse(fs.readFileSync(p.files, "utf8"));
9093
+ meta.density = G.densityOf(idx, fl);
9094
+ G.atomicWrite(p.meta, JSON.stringify(meta, null, 2) + "\n");
9095
+ r.density_backfilled = true;
9096
+ } catch (_) {
9097
+ meta = metaNow();
9098
+ }
9099
+ }
9100
+ }
9101
+ if (meta && meta.density) {
9102
+ r.density = meta.density;
9103
+ r.thin = SIGD.isThin(meta.density, r.files);
9104
+ }
8965
9105
  const b = r.behind || {};
8966
9106
  const lines = {
8967
9107
  off: "graph: off",
@@ -8975,6 +9115,12 @@ function graphCmd() {
8975
9115
  };
8976
9116
  let line = lines[r.state];
8977
9117
  let consult = r.state;
9118
+ // The density rides on the line a lane already prints, never on a line of
9119
+ // its own: a second line is a second thing to skip.
9120
+ const densityText = r.density
9121
+ ? ` · ${r.density.symbols_per_file} symbols/file` +
9122
+ (r.thin ? ` · THIN (${Math.round(r.density.zero_share * 100)} % of files have no symbol) — run: orc graph audit` : "")
9123
+ : "";
8978
9124
  if (healed && healed.parsed !== undefined) {
8979
9125
  const moved = healed.added + healed.changed + healed.deleted;
8980
9126
  const size = `${plural(r.files, "file")} · ${plural(r.symbols, "symbol")}`;
@@ -8986,7 +9132,15 @@ function graphCmd() {
8986
9132
  ? `graph: index upgraded to the new engine → updated (${healed.ms} ms) — ${size}`
8987
9133
  : `graph: ${plural(moved, "file")} changed outside ORC → updated (${healed.ms} ms) — ${size}`;
8988
9134
  } else if (healed) line += ` · could not heal: ${healed.reason}`;
8989
- return finish(r, line, consult === "off" ? "GRAPH-CONSULT off" : `GRAPH-CONSULT ${consult} :: files=${r.files} symbols=${r.symbols} gen=${r.generation}`);
9135
+ if (densityText && ["fresh", "drifted", "built", "updated"].includes(consult)) line += densityText;
9136
+ return finish(
9137
+ r,
9138
+ line,
9139
+ consult === "off"
9140
+ ? "GRAPH-CONSULT off"
9141
+ : `GRAPH-CONSULT ${consult} :: files=${r.files} symbols=${r.symbols} gen=${r.generation}` +
9142
+ (r.density ? ` density=${r.density.symbols_per_file}${r.thin ? " thin=1" : ""}` : "")
9143
+ );
8990
9144
  }
8991
9145
 
8992
9146
  if (sub === "update") {
@@ -9150,8 +9304,11 @@ function graphCmd() {
9150
9304
  const SIG = require("./graph-signals.js");
9151
9305
  if (sub === "changes") {
9152
9306
  const base = typeof flag("--base") === "string" ? flag("--base") : null;
9307
+ // V1: the lane hands the files it just wrote, and the rows for every
9308
+ // other file never leave this process.
9309
+ const onlyFiles = String(eqFlag("--files") || "").split(",").map((x) => x.trim()).filter(Boolean);
9153
9310
  const healed = healOnRead(SIG.changedPaths(root, base));
9154
- const r = SIG.graphChanges(claudeDir, root, { base });
9311
+ const r = SIG.graphChanges(claudeDir, root, { base, files: onlyFiles });
9155
9312
  if (healed) r.healed = healed;
9156
9313
  if (r.exit !== 0) return finish(r, r.reason === "no-index" ? "graph: none — no index yet (run: orc graph update)" : `graph: unavailable — ${r.reason}`);
9157
9314
  const c = r.counts;
@@ -9163,6 +9320,7 @@ function graphCmd() {
9163
9320
  : `graph changes vs ${r.base} — no indexed symbol was touched${r.not_in_graph.length ? ` (changed, but not in the graph: ${r.not_in_graph.join(", ")})` : ""}`;
9164
9321
  // K1/K2: `changes` avoids one Grep per touched symbol. The diff itself
9165
9322
  // is NOT counted — the reviewer reads it either way.
9323
+ let gainRow = null;
9166
9324
  if (r.symbols.length) {
9167
9325
  try {
9168
9326
  const K = require("./graph-gain.js");
@@ -9171,11 +9329,11 @@ function graphCmd() {
9171
9329
  if (m) {
9172
9330
  const hits = r.symbols.reduce((a, x) => a + Q.nameHits(m, x.qname), 0);
9173
9331
  const callerFiles = r.symbols.flatMap((x) => (x.callers || []).map((cc) => cc.file || cc));
9174
- graphGainAppend(claudeDir, { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, paid: { card: K.tok(line), source: 0, hints: 0 }, ...K.changesCost(m, r.symbols.length, hits, callerFiles) });
9332
+ gainRow = { cmd: "changes", targets: [r.base], gen: r.generation, ms: r.ms || 0, paid: { card: K.tok(line), source: 0, hints: 0 }, ...K.changesCost(m, r.symbols.length, hits, callerFiles) };
9175
9333
  }
9176
9334
  } catch (_) {}
9177
9335
  }
9178
- return finish(r, line, `GRAPH-CHANGES ${r.symbols.length ? "found" : "none"} :: symbols=${r.symbols.length} high=${c.high} medium=${c.medium} low=${c.low} gen=${r.generation}`);
9336
+ return finish(r, line, `GRAPH-CHANGES ${r.symbols.length ? "found" : "none"} :: symbols=${r.symbols.length} high=${c.high} medium=${c.medium} low=${c.low} gen=${r.generation}`, true, gainRow);
9179
9337
  }
9180
9338
  const file = plain[2];
9181
9339
  if (!file) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
@@ -9190,6 +9348,18 @@ function graphCmd() {
9190
9348
  return finish(r, line, `GRAPH-COCHANGE ${r.rows.length ? "found" : "none"} :: rows=${r.rows.length} commits=${r.commits}`);
9191
9349
  }
9192
9350
 
9351
+ // A2 (v1.9.1) — `orc graph audit`. A THIN line says the map is thin; this
9352
+ // says WHY, and it is a USER command: no lane runs it (`LANE_CALLS` pins
9353
+ // `lanes: []`). It reads the index, the file table and the first lines of
9354
+ // each empty file. It never parses, never writes, and costs no model tokens.
9355
+ if (sub === "audit") {
9356
+ if (!enabled && flag("--if-enabled") === true) return finish({ ok: true, enabled: false, state: "off", exit: 3 }, "graph: off");
9357
+ const A = require("./graph-audit.js");
9358
+ const r = A.audit(claudeDir, root, { top: eqFlag("--top") });
9359
+ if (r.exit === 1) return finish(r, "graph: none — no index yet (run: orc graph update)");
9360
+ return finish(r, r.line, r.trace, false);
9361
+ }
9362
+
9193
9363
  if (sub === "gc") {
9194
9364
  const r = G.graphGc(claudeDir);
9195
9365
  const line =
@@ -9289,16 +9459,21 @@ function graphCmd() {
9289
9459
  if (flag("--history") !== undefined) {
9290
9460
  const r = K.history(claudeDir, { limit: flag("--limit") });
9291
9461
  if (!r.ok) return finish(r, "graph gain: no calls recorded yet");
9462
+ // `--history` IS its rows. Dropping them would leave an answer with
9463
+ // nothing in it, so the switch is refused here — out loud.
9464
+ const briefAsked = briefOn;
9465
+ briefOn = false;
9292
9466
  const line =
9293
9467
  `graph gain — last ${r.rows.length} of ${r.total} call(s), newest first\n` +
9294
9468
  r.rows
9295
9469
  .map(
9296
9470
  (x) =>
9297
9471
  ` ${x.at} ${String(x.cmd).padEnd(10)} ${String((x.targets || []).join(",")).slice(0, 40).padEnd(40)}` +
9298
- ` paid ${String(x.paid.card + x.paid.source + x.paid.hints).padStart(6)} avoided ~${rng(x.avoided.low, x.avoided.high)}`
9472
+ ` paid ${String(x.paid.card + x.paid.source + x.paid.hints + (x.paid.envelope || 0)).padStart(6)} avoided ~${rng(x.avoided.low, x.avoided.high)}`
9299
9473
  )
9300
9474
  .join("\n") +
9301
- "\n `avoided` is an ESTIMATE of retrieval the ladder would have paid for — never a bill";
9475
+ "\n `avoided` is an ESTIMATE of retrieval the ladder would have paid for — never a bill" +
9476
+ (briefAsked ? " (--brief ignored on --history)" : "");
9302
9477
  return finish(r, line);
9303
9478
  }
9304
9479
 
@@ -9318,11 +9493,21 @@ function graphCmd() {
9318
9493
  const scope = r.run ? `run ${r.run}` : r.since ? `last ${r.since}` : "this project";
9319
9494
  const line =
9320
9495
  U.header(`graph gain — ${scope} · ${plural(r.calls_recorded, "call")} · ${plural(r.runs, "run")}${g ? ` · gen ${g[0]}${g[1] !== g[0] ? `–${g[1]}` : ""}` : ""}`) +
9321
- `\n paid ${kTok(r.paid.total).padStart(10)} tokens (cards ${kTok(r.paid.card)} · source ${kTok(r.paid.source)} · hints ${kTok(r.paid.hints)})` +
9496
+ `\n paid ${kTok(r.paid.total).padStart(10)} tokens (cards ${kTok(r.paid.card)} · source ${kTok(r.paid.source)} · hints ${kTok(r.paid.hints)} · envelope ${kTok(r.paid.envelope)})` +
9322
9497
  `\n avoided ~${rng(r.avoided.low, r.avoided.high)} tokens (estimate — the read ladder done well … done badly)` +
9323
9498
  `\n net ~${rng(r.net.low, r.net.high)} tokens · ~${r.calls.low} – ${r.calls.high} tool calls` +
9324
9499
  (byCmd ? `\n by command ${byCmd}` : "") +
9325
9500
  `\n hints ${r.hints.injected} injected · ${r.hints.read_notes} read notes · ${r.hints.updates} hook updates` +
9501
+ // R2 (v1.9.1): the reads the `on,read` hint would have named ranges for.
9502
+ // A COUNT, printed only when there is one — it is the data DE-8 waits
9503
+ // for, and nothing changes a setting because of it.
9504
+ (r.hints.wide_unhinted
9505
+ ? `\n wide reads ${r.hints.wide_unhinted} whole-file reads of files with ${require("./graph-resolve.js").WIDE_MIN}+ symbols were not hinted — \`code_graph_hooks: on,read\` names their ranges`
9506
+ : "") +
9507
+ // M2: a read nobody made is a fact about this project, printed with no
9508
+ // advice after it. The graph is not wrong for holding an answer nobody
9509
+ // asked for, and the meter does not tell anyone to start asking.
9510
+ (r.never_called && r.never_called.length ? `\n never called ${r.never_called.join(" · ")}` : "") +
9326
9511
  (r.unsized ? `
9327
9512
  ${plural(r.unsized, "row")} answered from SHARDS, where the size of the search replaced cannot be known — their estimate leaves it out` : "") +
9328
9513
  "\n an estimate of avoided RETRIEVAL, never a bill — searches are a fraction of a percent of a session" +
@@ -9365,8 +9550,8 @@ function graphCmd() {
9365
9550
  // gets it too. Appending it here would have shown it only to a human.
9366
9551
  const line = r.card;
9367
9552
  const trace = `GRAPH-MAP ${focus.length ? "focused" : "repo"} :: files=${r.shown}/${r.total_files}${focus.length ? ` focus=${r.focus.join(",")}` : ""} gen=${r.generation}`;
9368
- if (r.exit === 0) graphGainAppend(claudeDir, gainRowFor("map", Q, model, focus, r, Date.now() - t0, false));
9369
- return finish(r, line, trace, false);
9553
+ const gainRow = r.exit === 0 ? gainRowFor("map", Q, model, focus, r, Date.now() - t0, false) : null;
9554
+ return finish(r, line, trace, false, gainRow);
9370
9555
  }
9371
9556
 
9372
9557
  if (sub === "ctx" || sub === "impact" || sub === "path") {
@@ -9391,6 +9576,8 @@ function graphCmd() {
9391
9576
  // D2 is a SWITCH, so it is read from `args` — `flag()` would read the first
9392
9577
  // declared file as its value.
9393
9578
  const forSlice = args.includes("--for-slice");
9579
+ // R1 (v1.9.1): a SWITCH too — six lines around each confident call site.
9580
+ const callersSource = args.includes("--callers-source");
9394
9581
  // `path` names two symbols, not files, so it has nothing cheap to check —
9395
9582
  // it reads whatever generation is on disk, like every other command did
9396
9583
  // before EW3.
@@ -9411,7 +9598,7 @@ function graphCmd() {
9411
9598
  let fast = null;
9412
9599
  if (sub === "ctx" && !forSlice && operands.length === 1) {
9413
9600
  try {
9414
- fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0]);
9601
+ fast = require("./graph-shard.js").fastModel(claudeDir, root, operands[0], { callersSource });
9415
9602
  } catch (_) {
9416
9603
  fast = null;
9417
9604
  }
@@ -9439,6 +9626,7 @@ function graphCmd() {
9439
9626
  format: String(flag("--format") || "prose") === "tree" ? "tree" : "prose",
9440
9627
  offset: flag("--offset"),
9441
9628
  source: sourceLines,
9629
+ callersSource,
9442
9630
  wikiDocsFor,
9443
9631
  };
9444
9632
  let r;
@@ -9498,11 +9686,22 @@ function graphCmd() {
9498
9686
  } else if (sub === "impact") {
9499
9687
  if (!operands.length) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
9500
9688
  r = Q.impact(model, operands, opts);
9689
+ // V2/V3 (v1.9.1): the complexity read, and the `graph_facts` the
9690
+ // planner pastes — computed here, in ONE process, from the model this
9691
+ // call already loaded. Mini used to run `impact` plus one `cochange`
9692
+ // per declared file and count the rows in its own context.
9693
+ if (args.includes("--complexity") && r.exit === 0) {
9694
+ const SIG = require("./graph-signals.js");
9695
+ const cx = SIG.complexityOf(claudeDir, root, model, r.files, { risk: eqFlag("--risk"), tests: r.tests });
9696
+ r.complexity = { verdict: cx.verdict, why: cx.why, numbers: cx.numbers, line: cx.line, thresholds: cx.thresholds };
9697
+ r.facts = cx.facts;
9698
+ }
9501
9699
  } else {
9502
9700
  if (operands.length < 2) return finish({ ok: false, reason: "missing-operand", usage, exit: 1 }, usage);
9503
9701
  r = Q.pathBetween(model, operands[0], operands[1]);
9504
9702
  }
9505
9703
  let line = r.card;
9704
+ if (line && r.complexity) line = `${line}\n ${r.complexity.line}`;
9506
9705
  if (!line && r.state === "not-found") {
9507
9706
  const near = (r.nearest || []).map((s) => `${s.qname} (${s.file}:${s.lines[0]})`);
9508
9707
  line = r.missing
@@ -9526,21 +9725,26 @@ ${line || ""}`;
9526
9725
  ` +
9527
9726
  (line || "");
9528
9727
  }
9728
+ const cx = r.complexity || null;
9529
9729
  const trace =
9530
- sub === "ctx" && r.exit === 0
9730
+ cx
9731
+ ? `GRAPH-COMPLEXITY ${cx.verdict} :: files=${cx.numbers.files} callers=${cx.numbers.callers_outside} caller_files=${cx.numbers.caller_files_outside}` +
9732
+ ` maybe=${cx.numbers.maybe} tests=${cx.numbers.tests_reaching} risk=${cx.numbers.risk.length} cochange=${cx.numbers.cochange_missing.length} gen=${r.generation || (r.facts && r.facts.generation) || 0}`
9733
+ : sub === "ctx" && r.exit === 0
9531
9734
  ? `GRAPH-CONSULT ${r.view === "for-slice" ? "slice" : "card"} :: targets=${(r.view === "for-slice" ? r.files : r.cards ? r.cards.filter((c) => c.state === "found").map((c) => c.query) : [operands[0]]).join(",")}` +
9532
9735
  (healed && healed.state && healed.state !== "skipped" ? ` healed=${healed.trigger}` : "")
9533
9736
  : null;
9737
+ let gainRow = null;
9534
9738
  if (r.exit === 0) {
9535
9739
  try {
9536
- graphGainAppend(claudeDir, gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice));
9740
+ gainRow = gainRowFor(sub, Q, model, operands, r, Date.now() - t0, forSlice);
9537
9741
  } catch (_) {
9538
9742
  // K6: the meter is never allowed to fail the read it is measuring. The
9539
9743
  // append was already fail-quiet; the PRICING was not, because it was an
9540
9744
  // argument to it.
9541
9745
  }
9542
9746
  }
9543
- return finish(r, line, trace, !r.card);
9747
+ return finish(r, line, trace, !r.card, gainRow);
9544
9748
  }
9545
9749
 
9546
9750
  if (asJson) emitJson({ ok: false, reason: "unknown-subcommand", usage }, 1);
@@ -11034,7 +11238,7 @@ function resume() {
11034
11238
  // assumed the opposite, so an interrupted turn produced a SECOND agent on the
11035
11239
  // same task while the first was still working. A graded run put THREE
11036
11240
  // `orc-executor-opus-5-low` agents on one task for 50m19s + 115m22s + 100m53s
11037
- // — 266 minutes of Opus 5 for one authorised dispatch, all editing the same
11241
+ // — 266 minutes of Opus 5.5 for one authorised dispatch, all editing the same
11038
11242
  // files. The hook already recorded every one of them; nothing ever READ it.
11039
11243
  //
11040
11244
  // The pending sidecar (`<trace>.pending.json`, written by orc-trace.js on every
@@ -21038,7 +21242,7 @@ function docForecastCmd(claudeDir, slugArg) {
21038
21242
  for (const part of roleParts) {
21039
21243
  const vec = vecFor(part.agent, part.n, pct);
21040
21244
  if (!part.target) {
21041
- const m = priceVector(claudeDir, vec, "claude-opus-5");
21245
+ const m = priceVector(claudeDir, vec, "claude-opus-5-5");
21042
21246
  if (m.usd === null) priceable = false;
21043
21247
  else total += m.usd;
21044
21248
  continue;
@@ -21062,8 +21266,8 @@ function docForecastCmd(claudeDir, slugArg) {
21062
21266
  };
21063
21267
  const usd50 = usdAt("p50");
21064
21268
  const usd90 = usdAt("p90");
21065
- const money50 = priceVector(claudeDir, p50, "claude-opus-5");
21066
- const money90 = priceVector(claudeDir, p90, "claude-opus-5");
21269
+ const money50 = priceVector(claudeDir, p50, "claude-opus-5-5");
21270
+ const money90 = priceVector(claudeDir, p90, "claude-opus-5-5");
21067
21271
  const extraView = {
21068
21272
  resolved: docExtra.resolved,
21069
21273
  why: docExtra.why,
@@ -21282,7 +21486,7 @@ function docCostCmd(claudeDir, slugArg) {
21282
21486
  runRows.push({ trace: r.name, lane: r.lane, date: r.date, tokens: runVec, weighted: weightedTokens(runVec) });
21283
21487
  }
21284
21488
 
21285
- const money = priceVector(claudeDir, total, "claude-opus-5");
21489
+ const money = priceVector(claudeDir, total, "claude-opus-5-5");
21286
21490
  const sections = Object.values(bySection).map((s) => ({
21287
21491
  id: s.id,
21288
21492
  heading: s.heading,
@@ -21291,7 +21495,7 @@ function docCostCmd(claudeDir, slugArg) {
21291
21495
  joined: s.joined > 0,
21292
21496
  tokens: s.joined ? s.vec : null,
21293
21497
  weighted: s.joined ? weightedTokens(s.vec) : null,
21294
- usd: s.joined ? priceVector(claudeDir, s.vec, "claude-opus-5").usd : null,
21498
+ usd: s.joined ? priceVector(claudeDir, s.vec, "claude-opus-5-5").usd : null,
21295
21499
  }));
21296
21500
  const unjoined = sections.filter((s) => !s.joined).length;
21297
21501
  const code = joined === 0 ? 3 : unjoined ? 1 : 0;
@@ -25625,8 +25829,8 @@ function extraResolveFor(claudeDir, score, opts) {
25625
25829
  //
25626
25830
  // `claude` is an ARRAY because `quick-executor` has two: a menu is what that
25627
25831
  // lane is. `claude_opus5` is the variant `opus5_only` would have used, derived
25628
- // from the shipped ladders — NULL where the agent is already Opus 5, so two
25629
- // wiki slots collapsing onto one Opus 5 scanner is not a contradiction: the
25832
+ // from the shipped ladders — NULL where the agent is already Opus 5.5, so two
25833
+ // wiki slots collapsing onto one Opus 5.5 scanner is not a contradiction: the
25630
25834
  // slot names the POSITION, not the model. NO AGENT IS ADDED by this release
25631
25835
  // and no pair is needed (the floor stays 51).
25632
25836
  const EXTRA_SLOTS = [
@@ -43401,7 +43605,7 @@ function slEngine() {
43401
43605
  // panel really hands over — including `tokenCount`, which is the number v1.2.0
43402
43606
  // concluded could not be measured.
43403
43607
  const SL_TASK_FIXTURES = {
43404
- healthy: { id: "t1", name: "orc-executor-opus-5-low", type: "orc-executor-opus-5-low", description: "wire the retry ladder", status: "running", model: "claude-opus-5", effort: "low", tokenCount: 84000, contextWindowSize: 200000, startTime: 1767225600000 - 17 * 60000 },
43608
+ healthy: { id: "t1", name: "orc-executor-opus-5-low", type: "orc-executor-opus-5-low", description: "wire the retry ladder", status: "running", model: "claude-opus-5-5", effort: "low", tokenCount: 84000, contextWindowSize: 200000, startTime: 1767225600000 - 17 * 60000 },
43405
43609
  degraded: { id: "t2", name: "orc-executor-sonnet-4-6-med", type: "orc-executor-sonnet-4-6-med", description: "rename two files", status: "failed", model: "claude-sonnet-4-6", effort: "medium", tokenCount: 191000, contextWindowSize: 200000, startTime: 1767225600000 - 96 * 60000 },
43406
43610
  empty: { id: "t3" },
43407
43611
  };
@@ -43410,7 +43614,7 @@ const SL_FIXTURES = {
43410
43614
  healthy: {
43411
43615
  label: "a healthy session mid-run",
43412
43616
  payload: {
43413
- model: { id: "claude-opus-5", display_name: "Opus 5" },
43617
+ model: { id: "claude-opus-5-5", display_name: "Opus 5.5" },
43414
43618
  effort: { level: "high" },
43415
43619
  version: "2.1.80",
43416
43620
  context_window: { used_percentage: 38, remaining_percentage: 62, context_window_size: 200000 },