@azure-id/orc 1.9.1 → 2.0.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 (224) hide show
  1. package/CHANGELOG.md +390 -0
  2. package/README-id.md +20 -36
  3. package/README.md +24 -34
  4. package/bin/build-agents.js +206 -109
  5. package/bin/cli.js +760 -62
  6. package/bin/gotcha-import.js +1081 -0
  7. package/bin/gotcha.js +1286 -0
  8. package/bin/graph-query.js +1 -1
  9. package/bin/graph.js +717 -717
  10. package/bin/habit.js +1453 -0
  11. package/bin/mockrun-catalog.js +281 -276
  12. package/bin/onboarding-content.js +4 -4
  13. package/bin/pricing.json +207 -200
  14. package/bin/run-undo.js +398 -0
  15. package/bin/trace-write.js +657 -0
  16. package/bin/verify-contracts.js +527 -81
  17. package/bin/verify-package.js +58 -11
  18. package/bin/webui/api.js +26 -0
  19. package/bin/webui/app.html +239 -232
  20. package/bin/webui/css/00-tokens.css +110 -92
  21. package/bin/webui/css/04-motion.css +249 -152
  22. package/bin/webui/css/06-responsive.css +203 -178
  23. package/bin/webui/css/panels/behaviour.css +205 -0
  24. package/bin/webui/css/panels/knowledge.css +563 -116
  25. package/bin/webui/fixtures/behaviour.js +532 -0
  26. package/bin/webui/fixtures/extra.js +2036 -2036
  27. package/bin/webui/fixtures/hookui.js +5 -5
  28. package/bin/webui/fixtures/index.js +34 -0
  29. package/bin/webui/fixtures/knowledge.js +7 -1
  30. package/bin/webui/fixtures/settings.js +305 -305
  31. package/bin/webui/fixtures/stats.js +16 -0
  32. package/bin/webui/i18n/en/behaviour.json +143 -0
  33. package/bin/webui/i18n/en/knowledge.json +117 -2
  34. package/bin/webui/i18n/en/nav.json +25 -24
  35. package/bin/webui/i18n/en/tour.json +37 -35
  36. package/bin/webui/i18n/id/behaviour.json +143 -0
  37. package/bin/webui/i18n/id/knowledge.json +117 -2
  38. package/bin/webui/i18n/id/nav.json +25 -24
  39. package/bin/webui/i18n/id/tour.json +37 -35
  40. package/bin/webui/js/01-i18n.js +155 -154
  41. package/bin/webui/js/90-tour.js +498 -494
  42. package/bin/webui/js/91-shortcuts.js +126 -126
  43. package/bin/webui/js/99-boot.js +121 -118
  44. package/bin/webui/js/panels/behaviour.js +1022 -0
  45. package/bin/webui/js/panels/knowledge.js +552 -7
  46. package/bin/webui/js/panels/overview.js +13 -4
  47. package/mock-run/INDEX.md +109 -107
  48. package/mock-run/context-combiner.md +100 -100
  49. package/mock-run/gotcha-import.md +118 -0
  50. package/mock-run/habits.md +129 -0
  51. package/mock-run/orc-budget.md +534 -534
  52. package/mock-run/orc-challenge-council.md +262 -262
  53. package/mock-run/orc-quick.md +6 -1
  54. package/mock-run/orc-ultra.md +103 -103
  55. package/package.json +1 -1
  56. package/templates/agents/MODEL-MAPPING.md +49 -49
  57. package/templates/agents/orc-advisor-opus-5-xhigh.md +50 -56
  58. package/templates/agents/orc-analyze-mini-opus-5-med.md +55 -60
  59. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +55 -58
  60. package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -75
  61. package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -110
  62. package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -114
  63. package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -112
  64. package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -132
  65. package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -109
  66. package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -109
  67. package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -90
  68. package/templates/agents/orc-claude-writer-opus-4-8-high.md +48 -53
  69. package/templates/agents/orc-claude-writer-opus-5-med.md +48 -55
  70. package/templates/agents/orc-context-combiner-opus-5-high.md +78 -88
  71. package/templates/agents/orc-doc-checker-opus-5-low.md +108 -108
  72. package/templates/agents/orc-doc-writer-opus-5-med.md +134 -134
  73. package/templates/agents/orc-executor-haiku-4-5.md +14 -6
  74. package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
  75. package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
  76. package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
  77. package/templates/agents/orc-executor-opus-5-high.md +15 -7
  78. package/templates/agents/orc-executor-opus-5-low.md +15 -7
  79. package/templates/agents/orc-executor-opus-5-med.md +15 -7
  80. package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
  81. package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
  82. package/templates/agents/orc-executor-sonnet-5-high.md +14 -6
  83. package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
  84. package/templates/agents/orc-judge-opus-5-xhigh.md +81 -85
  85. package/templates/agents/orc-learn-writer-opus-5-low.md +67 -73
  86. package/templates/agents/orc-pattern-codifier-opus-5-med.md +58 -65
  87. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
  88. package/templates/agents/orc-planner-mini-opus-5-med.md +4 -7
  89. package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
  90. package/templates/agents/orc-planner-opus-5-med.md +157 -160
  91. package/templates/agents/orc-recon-opus-5-low.md +3 -10
  92. package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
  93. package/templates/agents/orc-retro-opus-5-med.md +8 -10
  94. package/templates/agents/orc-retro-sonnet-5-high.md +6 -7
  95. package/templates/agents/orc-reviewer-opus-5-med.md +96 -60
  96. package/templates/agents/orc-scout-opus-5-low.md +35 -40
  97. package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
  98. package/templates/agents/orc-system-analyst-opus-5-high.md +115 -120
  99. package/templates/agents/orc-test-author-opus-5-med.md +70 -71
  100. package/templates/agents/orc-test-designer-opus-5-high.md +158 -158
  101. package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -130
  102. package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
  103. package/templates/agents/orc-verifier-opus-5-med.md +77 -69
  104. package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +74 -79
  105. package/templates/agents/orc-wiki-scanner-opus-5-med.md +74 -81
  106. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
  107. package/templates/commands/orc-analyze.md +13 -21
  108. package/templates/commands/orc-fast.md +10 -15
  109. package/templates/commands/orc-poly.md +12 -21
  110. package/templates/commands/orc-pr-driver.md +11 -30
  111. package/templates/commands/orc-pr-setup.md +10 -31
  112. package/templates/commands/orc-route.md +11 -41
  113. package/templates/commands/orc-test.md +5 -60
  114. package/templates/commands/orc-ultra.md +17 -17
  115. package/templates/commands/orc-verify.md +11 -11
  116. package/templates/hooks/README.md +36 -2
  117. package/templates/hooks/orc-effort-guard.js +178 -178
  118. package/templates/hooks/orc-session-hook.js +264 -0
  119. package/templates/hooks/orc-statusline.js +9 -7
  120. package/templates/skills/_shared/README.md +9 -0
  121. package/templates/skills/_shared/code-graph.md +48 -56
  122. package/templates/skills/_shared/config-precedence.md +200 -198
  123. package/templates/skills/_shared/extra-dispatch.md +1331 -1346
  124. package/templates/skills/_shared/gotchas.md +228 -177
  125. package/templates/skills/_shared/habits.md +101 -0
  126. package/templates/skills/_shared/lane-contract.md +84 -0
  127. package/templates/skills/_shared/opus5-only.md +8 -8
  128. package/templates/skills/_shared/phases/README.md +142 -83
  129. package/templates/skills/_shared/phases/analyst-gates.md +125 -136
  130. package/templates/skills/_shared/phases/execution.md +8 -14
  131. package/templates/skills/_shared/phases/house-rules.md +27 -32
  132. package/templates/skills/_shared/phases/intake.md +127 -133
  133. package/templates/skills/_shared/phases/mock-example.md +46 -56
  134. package/templates/skills/_shared/phases/plan-handoff.md +91 -97
  135. package/templates/skills/_shared/phases/planning.md +7 -17
  136. package/templates/skills/_shared/phases/preflight.md +19 -42
  137. package/templates/skills/_shared/phases/review.md +23 -27
  138. package/templates/skills/_shared/phases/rules.md +18 -42
  139. package/templates/skills/_shared/phases/scoring.md +55 -65
  140. package/templates/skills/_shared/phases/security-checklist.md +46 -50
  141. package/templates/skills/_shared/phases/security.md +45 -55
  142. package/templates/skills/_shared/phases/ship.md +6 -15
  143. package/templates/skills/_shared/phases/stop-resume.md +2 -5
  144. package/templates/skills/_shared/phases/summary.md +73 -48
  145. package/templates/skills/_shared/phases/testgen.md +41 -51
  146. package/templates/skills/_shared/phases/trace-verbs.md +433 -0
  147. package/templates/skills/_shared/phases/trace.md +136 -367
  148. package/templates/skills/_shared/phases/verify.md +62 -70
  149. package/templates/skills/_shared/phases/wave-grouping.md +128 -133
  150. package/templates/skills/_shared/phases/wiki-consult.md +10 -6
  151. package/templates/skills/_shared/read-ladder.md +2 -55
  152. package/templates/skills/_shared/return-validation.md +17 -70
  153. package/templates/skills/_shared/review-slice.md +79 -0
  154. package/templates/skills/_shared/smoke-gate.md +46 -28
  155. package/templates/skills/context-combiner/SKILL.md +16 -45
  156. package/templates/skills/context-combiner/schemas/combined-report.md +78 -78
  157. package/templates/skills/orc/README.md +2 -2
  158. package/templates/skills/orc/SKILL.md +41 -69
  159. package/templates/skills/orc/config.md +9 -9
  160. package/templates/skills/orc/examples/full-run-mock.md +73 -73
  161. package/templates/skills/orc/references/effort-and-mode.md +222 -222
  162. package/templates/skills/orc/references/pattern-gate.md +89 -89
  163. package/templates/skills/orc/references/phases/intake.md +41 -47
  164. package/templates/skills/orc/references/phases/integration.md +13 -19
  165. package/templates/skills/orc/references/preflight-report.md +10 -11
  166. package/templates/skills/orc/references/ultra-mode.md +11 -9
  167. package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
  168. package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
  169. package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
  170. package/templates/skills/orc/subskills/orc-planner/SKILL.md +2 -2
  171. package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -121
  172. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +35 -76
  173. package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
  174. package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
  175. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +34 -45
  176. package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
  177. package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
  178. package/templates/skills/orc-advisor/SKILL.md +56 -60
  179. package/templates/skills/orc-analyze/SKILL.md +32 -68
  180. package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -42
  181. package/templates/skills/orc-analyze/schemas/report-audit.md +84 -83
  182. package/templates/skills/orc-analyze/schemas/report-prose.md +64 -63
  183. package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -78
  184. package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
  185. package/templates/skills/orc-challenge/README.md +142 -142
  186. package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -273
  187. package/templates/skills/orc-challenge/references/council.md +315 -315
  188. package/templates/skills/orc-challenge/references/intake.md +171 -171
  189. package/templates/skills/orc-claude/SKILL.md +14 -14
  190. package/templates/skills/orc-diy/README.md +174 -143
  191. package/templates/skills/orc-diy/SKILL.md +23 -74
  192. package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
  193. package/templates/skills/orc-diy/references/flow-schema.md +1 -1
  194. package/templates/skills/orc-doc/README.md +229 -229
  195. package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -325
  196. package/templates/skills/orc-doc/references/chunking.md +527 -527
  197. package/templates/skills/orc-fast/SKILL.md +45 -73
  198. package/templates/skills/orc-judge/SKILL.md +77 -82
  199. package/templates/skills/orc-learn/SKILL.md +14 -14
  200. package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -61
  201. package/templates/skills/orc-mini/SKILL.md +65 -113
  202. package/templates/skills/orc-pattern/SKILL.md +27 -48
  203. package/templates/skills/orc-poly/SKILL.md +32 -61
  204. package/templates/skills/orc-pr-driver/SKILL.md +25 -51
  205. package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
  206. package/templates/skills/orc-pr-setup/SKILL.md +20 -45
  207. package/templates/skills/orc-quick/README.md +43 -2
  208. package/templates/skills/orc-quick/SKILL.md +76 -107
  209. package/templates/skills/orc-quick/references/dispatch-gate.md +16 -5
  210. package/templates/skills/orc-quick/references/gh-mode.md +48 -1
  211. package/templates/skills/orc-quick/references/look.md +3 -1
  212. package/templates/skills/orc-retro/SKILL.md +19 -18
  213. package/templates/skills/orc-retro/examples/retro-mock.md +1 -1
  214. package/templates/skills/orc-route/SKILL.md +25 -45
  215. package/templates/skills/orc-test/SKILL.md +16 -37
  216. package/templates/skills/orc-verify/SKILL.md +21 -39
  217. package/templates/skills/orc-verify/examples/verify-mock.md +33 -33
  218. package/templates/skills/orc-wait/SKILL.md +156 -163
  219. package/templates/skills/orc-wiki/references/extra.md +1 -1
  220. package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -6
  221. package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
  222. package/templates/skills/orc-wiki/references/phases/phase-2.md +1 -6
  223. package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
  224. package/templates/skills/orc-wiki/references/phases/phase-3c.md +1 -6
package/bin/cli.js CHANGED
@@ -179,10 +179,19 @@ function writeStdoutSync(str) {
179
179
  }
180
180
  }
181
181
 
182
+ let PROBE_CAPTURE = null;
183
+
182
184
  // Print the object and (optionally) exit with the human path's code.
183
185
  // `compact` (v1.9.1 B1) writes the answer with no indentation: a `--brief`
184
186
  // answer is read by a model, and the card inside it carries its own newlines.
185
187
  function emitJson(obj, exitCode, compact) {
188
+ // v2.0.0 T20 — an in-process probe (`orc lane config` `probes{}`) takes the
189
+ // object instead of stdout. Outside a probe this line does nothing.
190
+ if (PROBE_CAPTURE) {
191
+ PROBE_CAPTURE.obj = obj;
192
+ if (exitCode !== undefined) process.exit(exitCode);
193
+ return;
194
+ }
186
195
  writeStdoutSync((compact ? JSON.stringify(obj) : JSON.stringify(obj, null, 2)) + "\n");
187
196
  if (exitCode !== undefined) process.exit(exitCode);
188
197
  }
@@ -427,6 +436,35 @@ function installGuards(claudeDir) {
427
436
  wireGraph("PreToolUse", "Bash|Read");
428
437
  wireGraph("PostToolUse", "Read");
429
438
 
439
+ // 7) The session hook (v2.0.0 Q8/Q9) — TWO entries, one file. `Stop` rings
440
+ // the terminal bell when a turn of an ORC run ends, and only under
441
+ // `notify: bell` (default off: the hook reads the key and returns, so an
442
+ // unarmed bell is byte-identical to not having it — the read-gate rule).
443
+ // `SessionStart` on `compact` prints ONE line naming the run's
444
+ // state-of-play.md while a run is in flight. Same (event, matcher) match as
445
+ // the graph hook, so re-running `orc update` changes nothing.
446
+ const sessionHookCmd = nodeCmd(path.join(hooksDest, "orc-session-hook.js"));
447
+ const wireSession = (arrName, matcher) => {
448
+ settings.hooks[arrName] = settings.hooks[arrName] || [];
449
+ for (const entry of settings.hooks[arrName]) {
450
+ if ((entry.matcher || null) !== (matcher || null)) continue;
451
+ for (const h of entry.hooks || []) {
452
+ if (typeof h.command === "string" && h.command.includes("orc-session-hook")) {
453
+ h.command = sessionHookCmd;
454
+ return;
455
+ }
456
+ }
457
+ }
458
+ const entry = { hooks: [{ type: "command", command: sessionHookCmd }] };
459
+ if (matcher) entry.matcher = matcher;
460
+ settings.hooks[arrName].push(entry);
461
+ console.log(` add settings.json → ${arrName} session hook${matcher ? ` (${matcher})` : ""}`);
462
+ };
463
+ wireSession("Stop", null);
464
+ wireSession("SessionStart", "compact");
465
+ // v2.0.2 (eval D13): the reviewer / verifier / judge get the gotcha card here.
466
+ wireSession("SubagentStart", null);
467
+
430
468
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
431
469
  }
432
470
 
@@ -1067,6 +1105,8 @@ function upgrade() {
1067
1105
  // ---------------------------------------------------------------------------
1068
1106
 
1069
1107
  const KNOWN_MODELS = [
1108
+ "claude-opus-5-5",
1109
+ // Kept so a config written before v1.9.2 still validates.
1070
1110
  "claude-opus-5",
1071
1111
  "claude-opus-4-8",
1072
1112
  "claude-opus-4-7",
@@ -1128,6 +1168,15 @@ const vFallbackAgent = tag((raw) => {
1128
1168
  "pinned to one agent — this overrides the score table AND a slot's own pinned agent for every fallback, whatever the task. `band` is what keeps a fallback a change of WHO and not a change of tier.",
1129
1169
  };
1130
1170
  }, { kind: "text", choices: ["band", "ask", "orc-executor-opus-5-med", "orc-executor-opus-5-low", "orc-executor-sonnet-4-6-high"] });
1171
+ // v2.0.0 W2 (DE-16) — `auto` is refused BY NAME. It is the one value a reader
1172
+ // of the research expects, and ORC does not have it: no habit is ever applied
1173
+ // without a yes, so a generic "must be one of" would read as a missing feature.
1174
+ const vHabits = tag((raw) => {
1175
+ const v = String(raw || "").trim();
1176
+ if (v === "auto")
1177
+ return { err: "`auto` is not a habits level. ORC never applies a habit without your yes. Use off, observe or propose." };
1178
+ return ["off", "observe", "propose"].includes(v) ? { value: v } : { err: "must be one of: off, observe, propose" };
1179
+ }, { kind: "enum", choices: ["off", "observe", "propose"] });
1131
1180
  const vPath = tag(
1132
1181
  (raw) => (raw && raw.trim() ? { value: raw } : { err: "must be a non-empty path" }),
1133
1182
  { kind: "path" }
@@ -1159,6 +1208,26 @@ const vRepo = tag(
1159
1208
  : { err: "must be a GitHub owner/repo (e.g. azure-id/orc)" },
1160
1209
  { kind: "repo" }
1161
1210
  );
1211
+ // v2.0.0 W4 — the reviewer card budget. Refused BY NAME below the floor: the
1212
+ // budget is a size, and review learning has no off switch (DE-10).
1213
+ const vCardBudget = tag((raw) => {
1214
+ const n = Number(raw);
1215
+ if (!Number.isInteger(n)) return { err: "gotcha_card_budget must be an integer >= 200" };
1216
+ if (n < 200)
1217
+ return { err: `gotcha_card_budget must be 200 or more (got ${n}). It is the SIZE of the reviewer card, not an off switch — review learning is always on` };
1218
+ return { value: n };
1219
+ }, { kind: "int", min: 200 });
1220
+ // An optional one-word value: empty clears it.
1221
+ const vOptText = tag((raw) => {
1222
+ const s = String(raw == null ? "" : raw).trim();
1223
+ if (/\s/.test(s)) return { err: "must be one word (no spaces)" };
1224
+ return { value: s };
1225
+ }, { kind: "text" });
1226
+ const vSonarUrl = tag((raw) => {
1227
+ const s = String(raw == null ? "" : raw).trim().replace(/\/+$/, "");
1228
+ if (s && !/^https?:\/\/[^\s/]+(\/\S*)?$/.test(s)) return { err: "must be an http(s) URL (e.g. https://sonarcloud.io)" };
1229
+ return { value: s };
1230
+ }, { kind: "text" });
1162
1231
 
1163
1232
  // Roles `orc extra` may hand to a non-Claude worker. Declared HERE, above
1164
1233
  // CONFIG_META, because CONFIG_META is evaluated at module load and a `const`
@@ -1253,7 +1322,7 @@ const CONFIG_FAMILIES = {
1253
1322
  // without being a CONFIG_META key, because where it sits in the
1254
1323
  // precedence is a fact even though `orc config set` refuses to write it.
1255
1324
  registry_less: true,
1256
- shadow_note: "shadowed by {by} — executors use the fixed 2-band Opus 5 ladder",
1325
+ shadow_note: "shadowed by {by} — executors use the fixed 2-band Opus 5.5 ladder",
1257
1326
  },
1258
1327
  { prio: "P3", key: null, terminal: "the shipped score→model table" },
1259
1328
  ],
@@ -1317,6 +1386,14 @@ const CONFIG_FAMILIES = {
1317
1386
  // structure costs no model tokens, so nothing about a dispatched model can
1318
1387
  // shadow whether it is built.
1319
1388
  graph: { contested: false, question: "whether code lanes build and read a local map of how the repo is connected" },
1389
+ // v2.0.0 W2 — habits. UNCONTESTED: an accepted habit is not a key, it is the
1390
+ // `learned` RANK under your config file (bin/habit.js), so nothing here
1391
+ // competes with anything. `offers` holds the end-of-run offers a habit can
1392
+ // answer; before 2.0.0 no key existed, so nothing could remember them.
1393
+ habits: { contested: false, question: "whether ORC learns your usual answers and proposes them — never applied without your yes" },
1394
+ offers: { contested: false, question: "which offers a lane still asks after the work, and which your setting answers" },
1395
+ notify: { contested: false, question: "whether a turn of an ORC run ends with the terminal bell" },
1396
+ rules: { contested: false, question: "how large the rules card is in an executor slice" },
1320
1397
  };
1321
1398
 
1322
1399
  // Ordered, tiered metadata. Common first, then advanced.
@@ -1346,6 +1423,15 @@ const CONFIG_META = [
1346
1423
  { key: "pattern_findings", def: "ask", tier: "common", answers: [{ family: "patterns", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-pattern", "orc-wiki"], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Code-pattern gate on an FE/BE cache miss: ask = prompt, on = auto-codify, off = always agnostic." },
1347
1424
  { key: "gotchas", def: "on", tier: "common", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-brainstorm", "orc-claude", "orc-diy", "orc-doc", "orc-fast", "orc-learn", "orc-mini", "orc-retro", "orc-wiki"], validate: vEnum("on", "off"), options: ["on", "off"], desc: "Repair memory: record a gotcha when a repair loop goes red → green, and inject the scope-matching ones into executor slices. Never injected unfiltered; see .claude/orc/gotchas.md." },
1348
1425
  { key: "gotchas_max", def: 40, tier: "common", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: ["orc"], validate: vInt(5), options: [20, 40, 60, 100], desc: "Live gotcha entries kept before the lowest-value tail is archived to gotchas-archive.md (never deleted)." },
1426
+ // --- v2.0.0 W4 — the gotchas engine (bin/gotcha.js) ---------------------------
1427
+ // Read by the CLI (`orc gotcha card`, `import`, `sync`), never by a spine,
1428
+ // so each `lanes[]` is empty and each key is on the seed-empty allowlist.
1429
+ // Review learning is ALWAYS ON (DE-10): the budget is a SIZE, never a switch.
1430
+ { key: "gotcha_card_budget", def: 600, tier: "common", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vCardBudget, options: [200, 400, 600, 900], desc: "The size of the reviewer card from `orc gotcha card`, in tokens. It is a SIZE, never an off switch: review learning is always on. The minimum is 200; a lower value is refused. Entries that do not fit are COUNTED in the card header, never dropped in silence." },
1431
+ { key: "gotcha_sync_hours", def: 6, tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 6, 24], desc: "How old the last `orc gotcha sync` may be before the next review step runs it again." },
1432
+ { key: "sonar_url", def: "", tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vSonarUrl, desc: "The SonarQube / SonarCloud base URL for `orc gotcha import sonar` (e.g. https://sonarcloud.io). The token is NEVER a key: set SONAR_TOKEN in the environment." },
1433
+ { key: "sonar_project", def: "", tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vOptText, desc: "The Sonar project key for `orc gotcha import sonar`, so the import needs no flags." },
1434
+ { key: "sonar_org", def: "", tier: "advanced", answers: [{ family: "gotchas", prio: "P2", mode: "replace" }], lanes: [], validate: vOptText, desc: "The SonarCloud organization for `orc gotcha import sonar` (empty for a self-hosted SonarQube)." },
1349
1435
  { key: "security_review", def: "off", tier: "common", answers: [{ family: "security", prio: "P2", mode: "replace" }], lanes: ["orc"], validate: vEnum("off", "ask", "on"), options: ["off", "ask", "on"], desc: "Opt-in Phase 5.5 security pass on runs with a task scored >= 70 (risk floor). OFF by default." },
1350
1436
  { key: "run_budget_dispatches", def: 0, tier: "common", answers: [{ family: "waves", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-budget"], validate: vInt(0), options: [0, 8, 12, 20, 30], desc: "Subagent budget for one run. The Phase-1 forecast estimates how many subagents the run will dispatch; if that exceeds this number the run STOPS before wave 1 (a hard gate like the batch pause, not a hint) and offers proceed / a cheaper lane / re-plan smaller. 0 = off, nothing changes." },
1351
1437
  { key: "mock_example", def: "ask", tier: "common", answers: [{ family: "mock", prio: "P2", mode: "replace" }], lanes: ["orc", "orc-brainstorm", "orc-diy", "orc-fast", "orc-grill", "orc-mini"], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Post-verify mocked runnable example (mock-examples/<slug>/, never committed): ask = offer after a green verify, on = always, off = never." },
@@ -1440,7 +1526,7 @@ const CONFIG_META = [
1440
1526
  // decision with a recorded reason (the /orc-pact retirement rule).
1441
1527
  { 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." },
1442
1528
  { 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." },
1443
- { 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)." },
1529
+ { 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)." },
1444
1530
  // --- v0.46.0 — the six new lanes ------------------------------------------
1445
1531
  { 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." },
1446
1532
  { 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." },
@@ -1475,7 +1561,23 @@ const CONFIG_META = [
1475
1561
  { key: "test_max_rps", def: 4, tier: "common", answers: [{ family: "test", prio: "P2", mode: "replace" }], lanes: [], validate: vInt(1), options: [1, 2, 4, 8], desc: "Requests per second the runner is allowed to send, with a fixed small concurrency beside it. The CLI PACES — this is not a suggestion, because ORC's own run must never be the incident. A 429 is a RESULT and not an error: it means rate limiting works, so the runner records it and backs off rather than pushing through." },
1476
1562
  { key: "test_case_budget", def: 200, tier: "common", answers: [{ family: "test", prio: "P2", mode: "replace" }], lanes: ["orc-test"], validate: vInt(1), options: [50, 100, 200, 500], desc: "Cases one run may expand to before it STOPS expanding. A PLANNED stop, not an interrupt (the wiki_refresh_budget shape): the matrix is combinatorial, and a run that silently grows to 4,000 cases against a staging box is a denial of service you wrote yourself. On reaching it the CLI names the targets it did not finish and offers `--budget`." },
1477
1563
  { key: "test_ui_driver", def: "playwright", tier: "common", answers: [{ family: "test", prio: "P2", mode: "replace" }], lanes: ["orc-test"], validate: vEnum("playwright", "none"), options: ["playwright", "none"], desc: "What drives the front-end half. `playwright` authors a journey SCRIPT and runs it — a step an LLM took is not a step you can re-run, and the artifacts (trace.zip, video, HAR) are the evidence. `none` skips the FE half entirely. ORC ships zero dependencies and NEVER installs Playwright into your project: an absent driver is a STATE with a named install command, not a failure." },
1564
+ // --- v2.0.0 W2 — habits (bin/habit.js) -------------------------------------
1565
+ // `habits` is read by the CLI, never by a lane: `orc lane config` resolves it
1566
+ // and hands the lane a `habits{}` block (the `code_graph` answer). The other
1567
+ // three are the offers a habit can answer; W3 wires the lanes that read them,
1568
+ // so their `lanes[]` is empty until then. OFF by default, and off is
1569
+ // specified as ZERO BYTES in `orc lane config` — that is a test.
1570
+ { key: "habits", def: "off", tier: "common", answers: [{ family: "habits", prio: "P2", mode: "replace" }], lanes: [], validate: vHabits, options: ["off", "observe", "propose"], desc: "Whether ORC learns the answers you give to its questions, from the ASK lines in your run traces. off = nothing is read and nothing changes (the default). observe = ORC computes your usual answers and shows them in `orc habit show`, and never proposes one. propose = at the END of a run, at most once, ORC asks whether an answer you keep giving should become your usual. NOTHING is applied without your yes, and there is no automatic level. An accepted habit is the `learned` rank, below your own config file: `orc config set` always wins, and `orc habit forget <id>` undoes it." },
1571
+ { key: "review_before_push", def: "ask", tier: "common", answers: [{ family: "offers", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Whether a lane offers a code review before it pushes. ask = offer it (the default), on = always review first, off = never offer it. A habit can learn only `on`." },
1572
+ { key: "mini_tdd", def: "ask", tier: "common", answers: [{ family: "testing", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("ask", "on", "off"), options: ["ask", "on", "off"], desc: "Whether /orc-mini asks at intake for a test-driven loop. ask = ask (the default), on = always use it, off = never. A habit can learn only `on`." },
1573
+ { key: "quick_update_tests", def: "ask", tier: "common", answers: [{ family: "offers", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("ask", "on"), options: ["ask", "on"], desc: "Whether /orc-quick offers to update the tests after a code change. ask = offer it (the default), on = always update them. There is no `off`: tests are the careful side. It affects only the offer after the work, never the dispatch gate." },
1574
+ // v2.0.0 W6c — Q8. The terminal bell when a turn of an ORC run ends. The hook is
1575
+ // wired always and reads this key first, so off is byte-identical to no hook.
1576
+ { key: "notify", def: "off", tier: "common", answers: [{ family: "notify", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "bell"), options: ["off", "bell"], desc: "Whether the installed session hook rings the terminal bell when a turn of an ORC run ends. off = silent (the default). bell = one bell through the Stop hook, only while an ORC run is open in this session and only when the run moved since the last bell, so a normal chat never rings. No OS notification and no network." },
1478
1577
  { key: "doc_dir", def: DOC_DIR_DEFAULT, tier: "advanced", answers: [{ family: "paths", prio: "P2", mode: "replace" }], lanes: ["orc-doc"], validate: vPath, desc: "Where /orc-doc folders live. Project root, not .claude/ — a document is a deliverable a human opens, and the same call /orc-quick, /orc-brainstorm and poly-repo-implementation/ already made." },
1578
+ // v2.0.0 W6c — DE-17. The compact rules card for orc-mini / orc-fast executors.
1579
+ // OFF until eval E4 passes (eval/results/2.0.0/E4.md); off = today's card, byte for byte.
1580
+ { key: "rules_card_compact", def: "on", tier: "advanced", answers: [{ family: "rules", prio: "P2", mode: "replace" }], lanes: [], validate: vEnum("off", "on"), options: ["off", "on"], desc: "Whether `orc rules slice` builds the COMPACT rules card for the orc-mini and orc-fast lanes too (orc-quick always gets it). The compact card keeps every rule id and the first line of each HARD rule, and drops the worked examples. on (the default since v2.0.2 — eval E4 passed: no new unmet[] items, no new rules_conflicts[], the smoke gate green on the first try at least as often). off = the full card, byte-identical to 2.0.0." },
1479
1581
  { key: "wiki_scan_tier", def: "ladder", tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc-wiki"], validate: vEnum("ladder", "always_deep"), desc: "Wiki scan tier: ladder picks light/deep per delta (first scan, STRUCTURAL, wide delta or a new exported symbol → deep; otherwise light), always_deep restores pre-v0.46.0 behaviour. The resolved tier is always printed — a cheaper model is never a quiet substitution." },
1480
1582
  { key: "wiki_tier_deep_files", def: 3, tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc-wiki"], validate: vInt(1), desc: "Covered files touched at or above this count send the refresh to the DEEP scanner." },
1481
1583
  { key: "wiki_refresh_budget", def: 0, tier: "advanced", answers: [{ family: "wiki", prio: "P2", mode: "replace" }], lanes: ["orc-wiki"], validate: vInt(0), desc: "Max scan-tasks per refresh run; 0 = no cap. A capped refresh is a PLANNED stop, not an interrupt: sync has already run, so the wiki is registered and consistent, and the remaining docs are AGING, not broken. Separate from the fixed pause-every-5 rule — do not merge them." },
@@ -1523,7 +1625,7 @@ const LEGACY_KEYS = {
1523
1625
  // So `config list` prints them, `config list --json` carries them, and
1524
1626
  // `config set` refuses them BY NAME instead of with a generic unknown-key list.
1525
1627
  const RETIRED_KEYS = {
1526
- 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" },
1628
+ 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" },
1527
1629
  fable5_roles: { removed_in: "1.0.0", why: "the Fable 5 role override was removed" },
1528
1630
  fable5_effort: { removed_in: "1.0.0", why: "the Fable 5 role override was removed" },
1529
1631
  };
@@ -1825,7 +1927,7 @@ function groupRule(title) {
1825
1927
  // `not-read`: a higher rank in this key's family resolved, so nobody looked;
1826
1928
  // `inert`: this key's master gate is off, which removes it from the
1827
1929
  // conversation rather than losing it a precedence contest (`design-03` §5).
1828
- function configKeyState(m, map, claudeDir) {
1930
+ function configKeyState(m, map, claudeDir, learned) {
1829
1931
  const has = Object.prototype.hasOwnProperty.call(map, m.key);
1830
1932
  if (m.gated_by) {
1831
1933
  const g = metaFor(m.gated_by);
@@ -1839,14 +1941,35 @@ function configKeyState(m, map, claudeDir) {
1839
1941
  }
1840
1942
  const why = shadowReason(m.key, map, claudeDir);
1841
1943
  if (why) return { state: "not-read", reason: why, source: has ? "overridden" : "default" };
1944
+ // v2.0.0 W2 — the `learned` rank: below the file, above the shipped default.
1945
+ const l = !has && learned && learned[m.key];
1946
+ if (l)
1947
+ return {
1948
+ state: "learned",
1949
+ reason: `learned from habit ${l.id} (${l.count} of ${l.total}) — undo: orc habit forget ${l.id}`,
1950
+ source: `learned:${l.id}`,
1951
+ value: l.value,
1952
+ };
1842
1953
  return { state: has ? "overridden" : "default", reason: null, source: has ? "overridden" : "default" };
1843
1954
  }
1844
1955
 
1956
+ // The learned values in force project-wide, or {} under `habits: off`.
1957
+ function configLearned(map, claudeDir) {
1958
+ const H = require("./habit.js");
1959
+ if (H.habitsMode(map) === "off") return {};
1960
+ try {
1961
+ return H.learnedFor(claudeDir, habitDeps(), map, null);
1962
+ } catch (_) {
1963
+ return {};
1964
+ }
1965
+ }
1966
+
1845
1967
  const STATE_PAINT = {
1846
1968
  overridden: (s) => ui.color.green(s),
1847
1969
  default: (s) => ui.color.gray(s),
1848
1970
  "not-read": (s) => ui.color.yellow(s),
1849
1971
  inert: (s) => ui.color.yellow(s),
1972
+ learned: (s) => ui.color.cyan(s),
1850
1973
  };
1851
1974
 
1852
1975
  function configList(claudeDir) {
@@ -1861,6 +1984,7 @@ function configList(claudeDir) {
1861
1984
  // UI axis and it still drives the interactive menu, so it rides along as a row
1862
1985
  // marker instead of being the table's spine.
1863
1986
  const families = laneFamilies(null, map, claudeDir);
1987
+ const learned = configLearned(map, claudeDir);
1864
1988
  const byFamily = new Map();
1865
1989
  for (const m of CONFIG_META) {
1866
1990
  const f = m.answers[0].family;
@@ -1889,9 +2013,9 @@ function configList(claudeDir) {
1889
2013
  return (ra === -1 ? 99 : ra) - (rb === -1 ? 99 : rb);
1890
2014
  });
1891
2015
  for (const m of metas) {
1892
- const st = configKeyState(m, map, claudeDir);
2016
+ const st = configKeyState(m, map, claudeDir, learned);
1893
2017
  const has = Object.prototype.hasOwnProperty.call(map, m.key);
1894
- const val = has ? map[m.key] : m.def;
2018
+ const val = has ? map[m.key] : st.value !== undefined ? st.value : m.def;
1895
2019
  const src = (STATE_PAINT[st.state] || ((s) => s))(st.state.padEnd(10));
1896
2020
  const opts = m.options ? ` ${ui.color.gray("[options: " + m.options.join(" | ") + "]")}` : "";
1897
2021
  // v0.55.0 — a DEPRECATED member of a live key is marked on the row that
@@ -2082,7 +2206,7 @@ function shadowReason(key, map, claudeDir) {
2082
2206
  //
2083
2207
  // v1.0.0 W4 — TWO BANDS, not three, and the same 90 edge as the default table's
2084
2208
  // top two rows (D13). That symmetry is the point: once the default table's high
2085
- // end is already Opus 5 with effort as the dial, the forcing mode differs from
2209
+ // end is already Opus 5.5 with effort as the dial, the forcing mode differs from
2086
2210
  // it only BELOW 65, so a third band here would be a distinction the default
2087
2211
  // table stopped making.
2088
2212
  //
@@ -2128,7 +2252,7 @@ function scoreTableJson(map, claudeDir) {
2128
2252
  // > the default 6-band table
2129
2253
  //
2130
2254
  // Extra is an OVERLAY, not a replacement, which is what makes "cheap grunt
2131
- // work goes to DeepSeek, hard work stays on Opus 5" a two-command setup
2255
+ // work goes to DeepSeek, hard work stays on Opus 5.5" a two-command setup
2132
2256
  // rather than a full table rewrite. `active` can therefore read as a
2133
2257
  // COMPOSITE — because the truth is a composite, and a single word would be
2134
2258
  // a lie about what the next dispatch will do.
@@ -2193,10 +2317,11 @@ function scoreTableJson(map, claudeDir) {
2193
2317
  function configListJson(claudeDir) {
2194
2318
  const { path: p, map } = readOverride(claudeDir);
2195
2319
  const has = (k) => Object.prototype.hasOwnProperty.call(map, k);
2320
+ const learned = configLearned(map, claudeDir);
2196
2321
  const keys = CONFIG_META.map((m) => {
2197
2322
  const v = m.validate || {};
2198
2323
  const why = shadowReason(m.key, map, claudeDir);
2199
- const st = configKeyState(m, map, claudeDir);
2324
+ const st = configKeyState(m, map, claudeDir, learned);
2200
2325
  return {
2201
2326
  key: m.key,
2202
2327
  tier: m.tier,
@@ -2215,7 +2340,7 @@ function configListJson(claudeDir) {
2215
2340
  // The master gate that makes this key inert. A gate does not win a
2216
2341
  // precedence contest; it removes its dependants from the conversation.
2217
2342
  gated_by: m.gated_by || null,
2218
- value: has(m.key) ? map[m.key] : m.def,
2343
+ value: has(m.key) ? map[m.key] : st.value !== undefined ? st.value : m.def,
2219
2344
  default: m.def,
2220
2345
  is_overridden: has(m.key),
2221
2346
  is_shadowed: !!why,
@@ -2226,7 +2351,7 @@ function configListJson(claudeDir) {
2226
2351
  // answers "does anything read it", from `LANE_RANK_STATES`' own words.
2227
2352
  state: st.state,
2228
2353
  state_reason: st.reason,
2229
- source: has(m.key) ? "overridden" : "default",
2354
+ source: st.source,
2230
2355
  desc: m.desc,
2231
2356
  options: m.options || null,
2232
2357
  control: {
@@ -2290,7 +2415,7 @@ function configListJson(claudeDir) {
2290
2415
  // lane-scoped answer asks `orc lane config <lane> --json`, which is the
2291
2416
  // command that owns it.
2292
2417
  families_resolved: laneFamilies(null, map, claudeDir),
2293
- rank_states: LANE_RANK_STATES,
2418
+ rank_states: require("./habit.js").habitsMode(map) !== "off" ? LANE_RANK_STATES.concat("learned") : LANE_RANK_STATES,
2294
2419
  // Permanently on and deliberately not a key — say so, or a reader hunts for
2295
2420
  // the switch (only the folder is configurable).
2296
2421
  behavior_trace: { always_on: true, configurable_key: "log_dir" },
@@ -2356,6 +2481,14 @@ function configSet(claudeDir, key, rawValue) {
2356
2481
  // precedent: a renamed mechanism must never be a silent revert.
2357
2482
  if (key === "extra_roles") extraLegacyRoleWarn(res.value);
2358
2483
  if (key === "opus5_only") opus5Notice(String(res.value) === "true", claudeDir);
2484
+ // DE-27 — the v0.40.0 key stays so an existing file is never silently
2485
+ // overridden, but it is not a feature switch any more.
2486
+ if (key === "gotchas" && String(res.value) === "off")
2487
+ console.error(
2488
+ " ⚠ review learning is designed to stay on. `gotchas: off` is kept for compatibility: it stops the\n" +
2489
+ " gotcha block in EXECUTOR slices only. The reviewer card (`orc gotcha card`) is always built.\n" +
2490
+ " To make the card smaller, set gotcha_card_budget (min 200)."
2491
+ );
2359
2492
  if (key === "opus5_only" || key === "extra_enabled") extraShadowNotice(claudeDir);
2360
2493
  }
2361
2494
 
@@ -2373,7 +2506,7 @@ function extraLegacyRoleWarn(value) {
2373
2506
 
2374
2507
  // A shadowed setting must never be silent — the v0.36.0 rule, applied to a
2375
2508
  // shadow that runs BOTH WAYS. Turning on opus5_only while route rows exist
2376
- // does not give you the Opus 5 ladder everywhere, and turning on Extra does
2509
+ // does not give you the Opus 5.5 ladder everywhere, and turning on Extra does
2377
2510
  // not take the Claude table away; saying either would be wrong. So name the
2378
2511
  // ranges, which is the only statement that is true from both sides.
2379
2512
  function extraShadowNotice(claudeDir) {
@@ -2434,17 +2567,17 @@ function opus5Notice(on, claudeDir) {
2434
2567
  console.log(
2435
2568
  "\n " + ui.color.bold("Opus-5-only dispatch is now ACTIVE — for EVERY role, not just executors.") + "\n" +
2436
2569
  "\n Why: deep SWE-benchmark work on cost vs efficiency across Claude models finds a\n" +
2437
- " single Opus 5 agent, with the EFFORT ladder as the cost dial, the most efficient\n" +
2570
+ " single Opus 5.5 agent, with the EFFORT ladder as the cost dial, the most efficient\n" +
2438
2571
  " configuration. You trade model-class variety for effort variety.\n" +
2439
2572
  "\n Scored executors (/orc + /orc-ultra):\n\n" +
2440
2573
  ladderTable(OPUS5_SCORE_TABLE) +
2441
2574
  "\n Fixed roles now dispatched instead of their defaults:\n\n" + roleRows + "\n" +
2442
- "\n Already Opus 5, unchanged: analyst · planner · reviewer · verifier · test-author\n" +
2575
+ "\n Already Opus 5.5, unchanged: analyst · planner · reviewer · verifier · test-author\n" +
2443
2576
  " · combiner · learn-writer · advisor · judge.\n" +
2444
2577
  " NEVER forced: orc-trace-writer-haiku-4-5 (it transcribes a packet, no reasoning)\n" +
2445
2578
  " and orc-diy (its table is compile-owned — re-run `orc diy compile` to change it).\n" +
2446
2579
  "\n " + ui.mark.warn("Tier requirement — read this one:") + "\n" +
2447
- " Today only the [90,100] executor band needs an Opus 5 main session. With this ON,\n" +
2580
+ " Today only the [90,100] executor band needs an Opus 5.5 main session. With this ON,\n" +
2448
2581
  " EVERY dispatch does. A subagent can never outrank the main session, so on a lower\n" +
2449
2582
  " session every role silently falls back to the session model and the tier-honesty\n" +
2450
2583
  " rule reports a downgrade on EVERY return instead of occasionally. Hooks cannot\n" +
@@ -2860,17 +2993,17 @@ const LANE_INERT = {
2860
2993
  { 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" },
2861
2994
  ],
2862
2995
  "orc-challenge": [
2863
- { 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" },
2996
+ { 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" },
2864
2997
  ],
2865
2998
  "orc-doc": [
2866
- { 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" },
2999
+ { 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" },
2867
3000
  ],
2868
- // v1.5.0 — both agents in this lane ship as claude-opus-5, and the
3001
+ // v1.5.0 — both agents in this lane ship as claude-opus-5-5, and the
2869
3002
  // interpreter's `low` is a MEASUREMENT choice rather than a cost one: a
2870
3003
  // harder-thinking interpreter reasons its way to why a leaked stack trace is
2871
3004
  // probably fine in staging, which is exactly the gap it exists to find.
2872
3005
  "orc-test": [
2873
- { 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" },
3006
+ { 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" },
2874
3007
  ],
2875
3008
  // v1.1.0 W2 — this lane DISPATCHES NOTHING. A detached command does the
2876
3009
  // waiting, so every family that answers "which model runs this" has no work
@@ -3145,7 +3278,7 @@ function laneAnnounce(lane, map, claudeDir, families) {
3145
3278
  out.push(
3146
3279
  off
3147
3280
  ? `opus5_only: ON, and INERT in this lane — ${off.reason}`
3148
- : "opus5_only: ON — every dispatched role uses Opus 5, with effort as the only cost dial"
3281
+ : "opus5_only: ON — every dispatched role uses Opus 5.5, with effort as the only cost dial"
3149
3282
  );
3150
3283
  }
3151
3284
  if (isTrue(map.extra_enabled)) {
@@ -3232,9 +3365,26 @@ function laneConfig(lane, claudeDir) {
3232
3365
  inert_reason: hit ? hit.reason : gate ? `${m.gated_by} is off, so nothing in its block is consulted` : null,
3233
3366
  };
3234
3367
  });
3368
+ // v2.0.0 W2 — the `learned` rank (bin/habit.js, DE-6). `habits: off` (the
3369
+ // default) adds ZERO BYTES: the habit keys leave `not_read`, no key gains a
3370
+ // field, and there is no `habits` block — the answer is the one 1.9.2 gave.
3371
+ const H = require("./habit.js");
3372
+ const hmode = H.habitsMode(map);
3373
+ let hab = null;
3374
+ if (hmode !== "off") {
3375
+ hab = H.laneBlock(claudeDir, habitDeps(), map, lane);
3376
+ for (const k of keys) {
3377
+ const l = !has(k.key) && hab.learned[k.key];
3378
+ if (l) k.value = l.value;
3379
+ k.source = has(k.key) ? "overridden" : l ? `learned:${l.id}` : "default";
3380
+ k.state = k.is_inert ? "inert" : k.is_shadowed ? "not-read" : l ? "learned" : has(k.key) ? "overridden" : "default";
3381
+ }
3382
+ }
3235
3383
  const effective = {};
3236
3384
  for (const k of keys) effective[k.key] = k.value;
3237
- return {
3385
+ const announce = laneAnnounce(lane, map, claudeDir, families);
3386
+ if (hab) for (const k of keys) if (k.state === "learned") announce.push(H.announceLine(k.key, hab.learned[k.key]));
3387
+ const out = {
3238
3388
  lane,
3239
3389
  command: (LANES.find((l) => l.lane === lane) || {}).command || null,
3240
3390
  command_note: (LANES.find((l) => l.lane === lane) || {}).note || null,
@@ -3245,16 +3395,31 @@ function laneConfig(lane, claudeDir) {
3245
3395
  effective,
3246
3396
  families,
3247
3397
  roles: laneRoles(lane, map, claudeDir),
3248
- announce: laneAnnounce(lane, map, claudeDir, families),
3398
+ announce,
3249
3399
  stops: LANE_STOPS.filter((s) => rows.some((m) => m.key === s.key))
3250
3400
  .filter((s) => s.arms(has(s.key) ? map[s.key] : metaFor(s.key).def))
3251
3401
  .map((s) => ({ key: s.key, value: has(s.key) ? map[s.key] : metaFor(s.key).def, when: s.when, action: s.action })),
3252
3402
  // The keys this lane deliberately ignores. An empty answer is an ANSWER
3253
3403
  // (v0.43.0): "this lane does not read doc_language" is information, and it
3254
3404
  // is what makes the two-way lint possible at all.
3255
- not_read: CONFIG_META.filter((m) => !(m.lanes || []).includes(lane)).map((m) => m.key),
3256
- rank_states: LANE_RANK_STATES,
3405
+ not_read: CONFIG_META.filter((m) => !(m.lanes || []).includes(lane))
3406
+ .filter((m) => hab || !H.HABIT_KEYS.includes(m.key))
3407
+ .map((m) => m.key),
3408
+ rank_states: hab ? LANE_RANK_STATES.concat("learned") : LANE_RANK_STATES,
3257
3409
  };
3410
+ if (hab) out.habits = hab.block;
3411
+ else for (const f of H.HABIT_FAMILIES) delete out.families[f];
3412
+ return out;
3413
+ }
3414
+
3415
+ // The helpers `bin/habit.js` borrows, so it REUSES the trace listing and the
3416
+ // timestamp parser `orc stats` / `orc run list` already use.
3417
+ function runUndoDeps() {
3418
+ return { args, wantsJson, emitJson, resolveClaudeDir, resolveRunDir, ensureRunDir, repoRootOf };
3419
+ }
3420
+
3421
+ function habitDeps() {
3422
+ return { flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, listTraces, traceTs };
3258
3423
  }
3259
3424
 
3260
3425
  function laneList(claudeDir) {
@@ -3328,6 +3493,19 @@ const LANE_CALLS = {
3328
3493
  never: "never read `clear` as proof that an AD-HOC dispatch finished — the hook writes no SPAWN for one, so no record exists",
3329
3494
  lanes: ["orc", "orc-doc", "orc-fast", "orc-mini", "orc-quick", "orc-test", "orc-wiki"],
3330
3495
  },
3496
+ // v2.0.0 T21 — the CLI holds the trace pen: one packet → the .txt AND the .jsonl.
3497
+ "trace-write": {
3498
+ cmd: "orc trace write --packet - [--json]",
3499
+ what: "write ONE phase packet (stdin, plain YAML or JSON) into the run's trace pair",
3500
+ exits: { 0: "written", 1: "usage or unreadable input", 2: "invalid packet — unknown verb, bad ts, parse error; nothing written", 3: "trace-file state — no `.current` on a later packet, or a rename target in the way; nothing written" },
3501
+ states: null,
3502
+ cost: "free",
3503
+ when: "at every phase close, in the same tool block as the next phase's first dispatch",
3504
+ on_absent: "exit ≠ 0 → dispatch `orc-trace-writer-haiku-4-5` with the SAME packet (the fallback)",
3505
+ canonical: "_shared/phases/trace.md",
3506
+ never: "never append trace lines by hand, and never retry a refused packet with a \"now\" timestamp",
3507
+ lanes: ["orc", "orc-fast", "orc-mini", "orc-quick", "orc-test"],
3508
+ },
3331
3509
  "lane-phases": {
3332
3510
  cmd: "orc lane phases <lane> [--json]",
3333
3511
  what: "which SHARED phases this lane runs, in order — the file, the layers to read, and when",
@@ -3936,7 +4114,7 @@ function laneCallsCmd(lane, claudeDir) {
3936
4114
  //
3937
4115
  // Per phase: \`file\` + \`layers\` (never a line number — /orc-doc rule 2, a
3938
4116
  // stored line number is a wrong line number one edit later), \`when\` and
3939
- // \`read\` (the partial-read declarations registered in \`_shared/read-ladder.md\`
4117
+ // \`read\` (the partial-read declarations registered in \`_shared/phases/README.md\`
3940
4118
  // at W10), \`optional_when\` (a config KEY, resolved through the priority ladder
3941
4119
  // by \`orc lane config\`, never read raw), and \`calls\` — catalogue ids from
3942
4120
  // \`LANE_CALLS\`, DERIVED rather than restated, so a lane can ask for one
@@ -3964,9 +4142,25 @@ const PHASE_FILES = {
3964
4142
  why_single_layer: null,
3965
4143
  why_two_layers:
3966
4144
  "every other lane READS the protocol; orc-diy STITCHES a flow-shaped restatement of it into FLOW-COMPILED.md, and a composed flow owes one packet per ENABLED phase group rather than per phase",
4145
+ // v2.0.0 T1 — the verbs are NOT here: every lane owes TRACE_ALWAYS_VERBS,
4146
+ // and the rest arrive with the phase that emits them.
4147
+ trace_verbs: [],
4148
+ },
4149
+ // v2.0.0 T1 — the full human verb table, split out of trace.md so a run
4150
+ // stops reading ~30 KB of grammar it never emits. `when: on-demand`: a lane
4151
+ // opens it only for a verb its `trace_grammar` does not carry. `via` names
4152
+ // the phase file that points at it — the lint accepts that pointer instead
4153
+ // of one per lane spine.
4154
+ "trace-verbs": {
4155
+ file: "_shared/phases/trace-verbs.md",
4156
+ layers: ["core"],
4157
+ via: "trace",
4158
+ why_single_layer: "it is a reference table, and TRACE_VERBS in bin/cli.js is the data behind it; a lane reads its own rows from `trace_grammar`",
4159
+ trace_verbs: [],
3967
4160
  },
3968
4161
  preflight: {
3969
4162
  file: "_shared/phases/preflight.md",
4163
+ trace_verbs: ["GATE", "GRAPH-CONSULT"],
3970
4164
  layers: ["core", "full"],
3971
4165
  // W13 moved this off a hardcoded `lane !== "orc"` branch and onto the same
3972
4166
  // lane->layer map the build phases use — one mechanism, not two.
@@ -3981,6 +4175,7 @@ const PHASE_FILES = {
3981
4175
  },
3982
4176
  "plan-handoff": {
3983
4177
  file: "_shared/phases/plan-handoff.md",
4178
+ trace_verbs: ["PHASE", "GATE"],
3984
4179
  layers: ["core"],
3985
4180
  why_single_layer:
3986
4181
  "executing a plan another session wrote is one procedure, and /orc-route READS it to define what a plan is — a second definition is drift the lint cannot see (v0.42.0)",
@@ -3992,12 +4187,14 @@ const PHASE_FILES = {
3992
4187
  },
3993
4188
  "analyst-gates": {
3994
4189
  file: "_shared/phases/analyst-gates.md",
4190
+ trace_verbs: ["GATE"],
3995
4191
  layers: ["core"],
3996
4192
  why_single_layer:
3997
4193
  "these are the orchestrator-side gates on a returned analysis or plan, and a trimmed lane runs fewer of them rather than different ones",
3998
4194
  },
3999
4195
  "wiki-consult": {
4000
4196
  file: "_shared/phases/wiki-consult.md",
4197
+ trace_verbs: ["WIKI-CONSULT", "CROSSLINK"],
4001
4198
  layers: ["core"],
4002
4199
  why_single_layer: "the precedence ladder (code > fresh wiki > stale wiki > model priors) does not bend for a faster lane",
4003
4200
  },
@@ -4022,6 +4219,7 @@ const PHASE_FILES = {
4022
4219
  },
4023
4220
  "stop-resume": {
4024
4221
  file: "_shared/phases/stop-resume.md",
4222
+ trace_verbs: ["GATE"],
4025
4223
  layers: ["core"],
4026
4224
  why_single_layer:
4027
4225
  "the stop sequence is identical in every lane that stops; which moments are MANDATORY stops is already a per-lane rule in the spine",
@@ -4029,6 +4227,7 @@ const PHASE_FILES = {
4029
4227
 
4030
4228
  "planning": {
4031
4229
  file: "_shared/phases/planning.md",
4230
+ trace_verbs: ["PHASE", "CONFIG", "WIKI-CONSULT", "CROSSLINK", "GRAPH-MAP", "GRAPH-COCHANGE", "GATE", "PACT"],
4032
4231
  layers: ["full", "composed"],
4033
4232
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4034
4233
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4041,6 +4240,7 @@ const PHASE_FILES = {
4041
4240
  },
4042
4241
  "scoring": {
4043
4242
  file: "_shared/phases/scoring.md",
4243
+ trace_verbs: ["PHASE", "SCORE"],
4044
4244
  layers: ["full", "composed"],
4045
4245
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4046
4246
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4053,6 +4253,7 @@ const PHASE_FILES = {
4053
4253
  },
4054
4254
  "execution": {
4055
4255
  file: "_shared/phases/execution.md",
4256
+ trace_verbs: ["PHASE", "GATE", "OUTCOME", "TDD-RED", "TDD-GREEN", "REPRO", "REPLAN", "QUESTION", "CONTEXT-GAP", "BOUNDARY", "EXTRA", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-HINT"],
4056
4257
  layers: ["full", "composed"],
4057
4258
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4058
4259
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4065,6 +4266,7 @@ const PHASE_FILES = {
4065
4266
  },
4066
4267
  "review": {
4067
4268
  file: "_shared/phases/review.md",
4269
+ trace_verbs: ["PHASE", "FINDING", "GRAPH-CHANGES"],
4068
4270
  layers: ["full", "composed"],
4069
4271
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4070
4272
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4089,6 +4291,7 @@ const PHASE_FILES = {
4089
4291
  },
4090
4292
  "verify": {
4091
4293
  file: "_shared/phases/verify.md",
4294
+ trace_verbs: ["PHASE", "VERDICT", "PACT"],
4092
4295
  layers: ["full", "composed"],
4093
4296
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4094
4297
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4113,6 +4316,7 @@ const PHASE_FILES = {
4113
4316
  },
4114
4317
  "mock-example": {
4115
4318
  file: "_shared/phases/mock-example.md",
4319
+ trace_verbs: ["PHASE", "DRIFT"],
4116
4320
  layers: ["full", "composed"],
4117
4321
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4118
4322
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4125,6 +4329,7 @@ const PHASE_FILES = {
4125
4329
  },
4126
4330
  "ship": {
4127
4331
  file: "_shared/phases/ship.md",
4332
+ trace_verbs: ["PHASE", "GATE", "GRAPH-UPDATE", "GRAPH-GAIN"],
4128
4333
  layers: ["full", "composed"],
4129
4334
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4130
4335
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4137,6 +4342,7 @@ const PHASE_FILES = {
4137
4342
  },
4138
4343
  "summary": {
4139
4344
  file: "_shared/phases/summary.md",
4345
+ trace_verbs: ["PHASE", "FINDING-OUTCOME"],
4140
4346
  layers: ["full", "composed"],
4141
4347
  // v1.0.0 W13 — orc-diy became the second reader, so the phase left
4142
4348
  // orc/references/phases/ for the library. `full` is /orc's procedure;
@@ -4167,12 +4373,205 @@ const PREFLIGHT_CALL_IDS = new Set([
4167
4373
  "run-list",
4168
4374
  ]);
4169
4375
 
4376
+ // ── v2.0.0 T20 — `probes{}`: one preflight call instead of 4–6 ──────────────
4377
+ //
4378
+ // W0 counted 2–6 CLI calls at preflight before the first dispatch, each one a
4379
+ // round trip that re-sends the whole cached session. `orc lane config` is
4380
+ // already the one call every lane makes first, so it now carries the ANSWERS of
4381
+ // the probes that lane makes at start.
4382
+ //
4383
+ // Each probe runs the stand-alone command's OWN code, in this process, with its
4384
+ // own argv: the same function, the same exit code, the same fields — so the
4385
+ // entry cannot drift from the command. The entry is BRIEF (T17): `exit`, `line`
4386
+ // and the fields a lane branches on. `--probes-full` adds the whole stand-alone
4387
+ // answer as `full`; `--no-probes` leaves the block out.
4388
+ //
4389
+ // READ-ONLY, always. `graph status` runs WITHOUT `--heal` (a heal builds or
4390
+ // writes the index) — `heal_needed: true` says the lane still makes that one
4391
+ // call. `extra resolve` runs without the `orc extra` credential sweep (a
4392
+ // delete). A probe that throws or gives no answer is `{exit, error}` — never a
4393
+ // thrown error, and never a change to `lane config`'s own exit code.
4394
+ //
4395
+ // The set per lane is DERIVED: PREFLIGHT_CALL_IDS ∩ the lane's LANE_CALLS rows,
4396
+ // plus `graph-status` and `rules-slice` where the lane makes them, plus
4397
+ // /orc-quick's slot probe. `challenge-status` needs a slug, so it is not here.
4398
+ const PROBE_DEFS = {
4399
+ "wiki-status": {
4400
+ argv: () => ["wiki", "status"],
4401
+ run: () => wiki(),
4402
+ pick: ["state", "tier", "docs", "distance", "last_scan"],
4403
+ line: (j) =>
4404
+ j.state === "none"
4405
+ ? "wiki: none — no wiki yet"
4406
+ : j.state === "registered"
4407
+ ? `wiki: ${j.tier || "tier unknown"}${j.distance === null ? "" : ` (${j.distance}c)`} — ${plural(j.docs, "doc")} · last scan ${j.last_scan || "?"}`
4408
+ : `wiki: ${String(j.state).toUpperCase()} — ${plural(j.docs, "doc")} (run: orc wiki sync)`,
4409
+ },
4410
+ "pattern-status": {
4411
+ argv: () => ["pattern", "status"],
4412
+ run: () => pattern(),
4413
+ pick: ["cached"],
4414
+ derive: (j) => ({ langs: (j.patterns || []).map((p) => p.lang) }),
4415
+ line: (j) => (j.cached ? `pattern: cached — ${(j.patterns || []).map((p) => p.lang).join(", ")}` : "pattern: none cached"),
4416
+ },
4417
+ "gotcha-status": {
4418
+ argv: () => ["gotcha", "status"],
4419
+ run: () => gotcha(),
4420
+ pick: ["count", "gotchas_max"],
4421
+ line: (j) => (j.count ? `gotchas: ${j.count} live (cap ${j.gotchas_max})` : "gotchas: none recorded — nothing to inject"),
4422
+ },
4423
+ "pact-status": {
4424
+ argv: () => ["pact", "status"],
4425
+ run: () => pact(),
4426
+ pick: ["ok", "reason", "entries", "counts"],
4427
+ line: (j) => j.line || "pact: no ledger yet — a first run",
4428
+ },
4429
+ "boundary-status": {
4430
+ argv: () => ["boundary", "status"],
4431
+ run: () => boundary(),
4432
+ pick: ["ok", "reason", "counts", "stale"],
4433
+ line: (j) => j.line || "boundary: no cards yet — every area is UNKNOWN, never assumed safe",
4434
+ },
4435
+ "aftermath-status": {
4436
+ argv: () => ["aftermath", "status"],
4437
+ run: () => aftermath(),
4438
+ pick: ["ok", "reason", "window_days", "counts"],
4439
+ line: (j) =>
4440
+ j.ok
4441
+ ? `aftermath: ${plural((j.runs || []).length, "run")} in the last ${j.window_days} days · ${Object.entries(j.counts || {}).map(([k, v]) => `${v} ${k}`).join(" · ") || "no grade"}`
4442
+ : j.reason === "shallow"
4443
+ ? `aftermath: no runs in the last ${j.window_days} days — too shallow to grade`
4444
+ : `aftermath: ${j.reason}`,
4445
+ },
4446
+ "diy-status": {
4447
+ argv: () => ["diy", "status"],
4448
+ run: () => diy(),
4449
+ pick: ["state", "reason"],
4450
+ line: (j) => `diy: ${j.state}${j.reason ? " — " + j.reason : ""}`,
4451
+ },
4452
+ "run-list": {
4453
+ argv: () => ["run", "list"],
4454
+ run: () => runCmd(),
4455
+ pick: ["total"],
4456
+ line: (j) => `runs: ${j.total}`,
4457
+ },
4458
+ "graph-status": {
4459
+ argv: () => ["graph", "status", "--if-enabled", "--brief"],
4460
+ run: () => graphCmd(),
4461
+ pick: ["state", "enabled", "exists", "files", "symbols", "generation", "auto_update", "notes", "trace"],
4462
+ derive: (j) => ({ heal_needed: !!(j.enabled && j.auto_update && (j.state === "none" || j.state === "drifted")) }),
4463
+ line: (j) => j.line,
4464
+ },
4465
+ "rules-slice": {
4466
+ argv: (lane) => ["rules", "slice", "--lane", lane],
4467
+ run: () => rulesCmd(),
4468
+ pick: ["packs", "overrides"],
4469
+ line: (j) => j.line,
4470
+ },
4471
+ "extra-slot": {
4472
+ argv: () => ["extra", "resolve", "--slot", "quick-executor"],
4473
+ run: () => extraResolveCmd(resolveClaudeDir(), undefined),
4474
+ pick: ["resolved", "via", "slot", "needs_reping", "held_back"],
4475
+ line: (j) => (j.resolved === "extra" ? `extra: ${j.announce}` : `extra: ${j.slot} → claude ${(j.claude && j.claude.agent) || ""}`.trim()),
4476
+ },
4477
+ };
4478
+
4479
+ function laneProbeIds(lane) {
4480
+ const ids = Object.entries(LANE_CALLS)
4481
+ .filter(([id, c]) => id !== "lane-config" && PREFLIGHT_CALL_IDS.has(id) && c.lanes.includes(lane) && PROBE_DEFS[id])
4482
+ .map(([id]) => id);
4483
+ if (LANE_CALLS["graph-status"].lanes.includes(lane)) ids.push("graph-status");
4484
+ if (LANE_CALLS["rules-slice"].lanes.includes(lane)) ids.push("rules-slice");
4485
+ if (lane === "orc-quick") ids.push("extra-slot");
4486
+ return ids;
4487
+ }
4488
+
4489
+ // Run ONE stand-alone command in this process and take its answer. The argv is
4490
+ // swapped in for the call and put back after it; stdout, stderr and exit are
4491
+ // caught, so nothing reaches the terminal and nothing ends this process.
4492
+ function runProbe(argv, fn) {
4493
+ const saved = args.slice();
4494
+ const pass = [];
4495
+ const di = saved.indexOf("--dir");
4496
+ if (di !== -1 && saved[di + 1] !== undefined) pass.push("--dir", saved[di + 1]);
4497
+ if (saved.includes("--global")) pass.push("--global");
4498
+ args.splice(0, args.length, ...argv, "--json", ...pass);
4499
+ const cap = { obj: null, text: [], err: [] };
4500
+ const SENT = { probe: true };
4501
+ let code = null;
4502
+ let error = null;
4503
+ const keep = { exit: process.exit, log: console.log, error: console.error, warn: console.warn };
4504
+ process.exit = (c) => {
4505
+ code = c === undefined ? 0 : c;
4506
+ throw SENT;
4507
+ };
4508
+ console.log = (...a) => cap.text.push(a.map(String).join(" "));
4509
+ console.error = console.warn = (...a) => cap.err.push(a.map(String).join(" "));
4510
+ PROBE_CAPTURE = cap;
4511
+ try {
4512
+ fn();
4513
+ if (code === null) code = 0;
4514
+ } catch (e) {
4515
+ if (e !== SENT) {
4516
+ error = String((e && e.message) || e).split("\n")[0];
4517
+ if (code === null) code = 1;
4518
+ }
4519
+ } finally {
4520
+ PROBE_CAPTURE = null;
4521
+ process.exit = keep.exit;
4522
+ console.log = keep.log;
4523
+ console.error = keep.error;
4524
+ console.warn = keep.warn;
4525
+ args.splice(0, args.length, ...saved);
4526
+ }
4527
+ let obj = cap.obj;
4528
+ if (!obj && cap.text.length) {
4529
+ try {
4530
+ obj = JSON.parse(cap.text.join("\n"));
4531
+ } catch (_) {}
4532
+ }
4533
+ if (!obj && !error) error = String(cap.err.find((l) => l.trim()) || "no answer").trim().split("\n")[0];
4534
+ return { exit: code, obj, error };
4535
+ }
4536
+
4537
+ function laneProbes(lane, full) {
4538
+ const out = {};
4539
+ for (const id of laneProbeIds(lane)) {
4540
+ const def = PROBE_DEFS[id];
4541
+ const r = runProbe(def.argv(lane), def.run);
4542
+ if (r.error || !r.obj) {
4543
+ out[id] = { exit: r.exit, error: r.error };
4544
+ continue;
4545
+ }
4546
+ const j = r.obj;
4547
+ let line;
4548
+ try {
4549
+ line = def.line(j);
4550
+ } catch (e) {
4551
+ line = null;
4552
+ }
4553
+ const e = { exit: r.exit, line };
4554
+ for (const k of def.pick) if (Object.prototype.hasOwnProperty.call(j, k)) e[k] = j[k];
4555
+ if (def.derive) Object.assign(e, def.derive(j));
4556
+ if (full) e.full = j;
4557
+ out[id] = e;
4558
+ }
4559
+ return out;
4560
+ }
4561
+
4170
4562
  // tier + packet cadence per trace-owning lane. This is the same data the tier
4171
4563
  // table in \`_shared/phases/trace.md\` renders for a human; the lint asserts the
4172
4564
  // two agree, so a lane cannot be in one and absent from the other.
4173
4565
  const LANE_TRACE = {
4174
- orc: { tier: "Build lanes", token: "orc" },
4175
- "orc-mini": { tier: "Build lanes", token: "mini" },
4566
+ // `spine_verbs` (v2.0.0 T1): the verbs a lane's own SPINE emits outside
4567
+ // any declared phase row — orc's ultra gates, and orc-mini, whose pipeline is
4568
+ // still in-spine. They join `trace_grammar` like a phase's `trace_verbs`.
4569
+ orc: { tier: "Build lanes", token: "orc", spine_verbs: ["ADVISE", "JUDGE"] },
4570
+ "orc-mini": {
4571
+ tier: "Build lanes",
4572
+ token: "mini",
4573
+ spine_verbs: ["PHASE", "GATE", "OUTCOME", "DRIFT", "TDD-RED", "TDD-GREEN", "FINDING", "FINDING-OUTCOME", "GRAPH-CONSULT", "GRAPH-UPDATE", "GRAPH-COMPLEXITY", "GRAPH-NOTES"],
4574
+ },
4176
4575
  "orc-fast": { tier: "Build lanes", token: "fast" },
4177
4576
  "orc-wiki": { tier: "Multi-dispatch", token: "wiki" },
4178
4577
  "orc-pr-driver": { tier: "Multi-dispatch", token: "prdriver" },
@@ -4200,12 +4599,96 @@ const LANE_TRACE = {
4200
4599
  "orc-export": { tier: "Single-dispatch", token: "export" },
4201
4600
  };
4202
4601
 
4602
+ // v2.0.0 T1 — the CLOSED trace verb set, as DATA (the `LANE_CALLS` shape).
4603
+ // `_shared/phases/trace-verbs.md` renders this table for a human, and
4604
+ // `bin/verify-contracts.js` holds the two together BOTH ways, plus every
4605
+ // `trace_verbs` / `spine_verbs` entry in the phase manifest: a verb a phase
4606
+ // lists that this registry lacks FAILS. `orc lane phases <lane> --json` hands a
4607
+ // lane `trace_grammar` = the grammar lines of exactly the verbs its phases emit
4608
+ // plus TRACE_ALWAYS_VERBS, so a run never reads the whole table.
4609
+ //
4610
+ // grammar — the line shape; ` · ` separates the sub-forms of one verb.
4611
+ // emitter — who produces it: `orc → writer` is a packet event, `hook` needs
4612
+ // no cooperation, `X→orc → writer` is a subagent-returned marker.
4613
+ // meaning — ONE line. The detail lives at `owner`.
4614
+ const TRACE_VERBS = {
4615
+ PHASE: { grammar: "PHASE <name> start|end", emitter: "orc → writer", meaning: "phase transition", owner: "_shared/phases/trace.md" },
4616
+ "PHASE-EDGE": { grammar: "PHASE-EDGE <role-family> :: first=<agent>", emitter: "hook", meaning: "deterministic phase inference: a SPAWN whose role family differs from the previous one opens an edge", owner: "hooks/orc-trace.js" },
4617
+ CONFIG: { grammar: "CONFIG <key=value …>", emitter: "orc → writer", meaning: "Phase 1 — the resolved config values this run will consume, ALWAYS with opus5_only", owner: "_shared/phases/trace-verbs.md" },
4618
+ "WIKI-CONSULT": { grammar: "WIKI-CONSULT <fresh|aging|stale|absent|empty> :: docs=<list|none>", emitter: "orc → writer", meaning: "project wiki consulted for grounding, with its staleness tier", owner: "_shared/phases/wiki-consult.md" },
4619
+ CROSSLINK: { grammar: "CROSSLINK <cached|configured-no-cache|none> :: boundaries=<n> peers=<names> · CROSSLINK inject task=<id> :: <boundary>", emitter: "orc → writer", meaning: "cross-repo peer-knowledge state at the consult point; `inject` when a slice receives a linked contract", owner: "_shared/phases/wiki-consult.md" },
4620
+ "GRAPH-CONSULT": { grammar: "GRAPH-CONSULT <fresh|updated|built|drifted|none|off> :: files=<n> symbols=<n>[ density=<n>[ thin=1]] · GRAPH-CONSULT card task=<id> :: targets=<…>", emitter: "orc → writer", meaning: "the code graph's preflight state, copied VERBATIM from the CLI `trace` field", owner: "_shared/code-graph.md" },
4621
+ "GRAPH-UPDATE": { grammar: "GRAPH-UPDATE <state> :: parsed=<n> reused=<n> deleted=<n> gen=<n> route=<n> ms=<n>", emitter: "orc → writer", meaning: "one graph update, the `trace` field of `orc graph update --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
4622
+ "GRAPH-MAP": { grammar: "GRAPH-MAP <repo|focused> :: files=<shown>/<total> [focus=<a,b>] gen=<n>", emitter: "orc → writer", meaning: "ONE line at the start of planning, the `trace` field of `orc graph map --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
4623
+ "GRAPH-CHANGES": { grammar: "GRAPH-CHANGES <found|none> :: symbols=<n> high=<n> medium=<n> low=<n> gen=<n>", emitter: "orc → writer", meaning: "one line at review, the `trace` field of `orc graph changes --json`, verbatim", owner: "_shared/phases/trace-verbs.md" },
4624
+ "GRAPH-COMPLEXITY": { grammar: "GRAPH-COMPLEXITY <mini-ok|recommend-orc> :: files=<n> callers=<n> caller_files=<n> maybe=<n> tests=<n> risk=<n> cochange=<n> gen=<n>", emitter: "orc → writer", meaning: "one line per /orc-mini run, the `trace` field of the `--complexity` impact call, verbatim", owner: "_shared/phases/trace-verbs.md" },
4625
+ "GRAPH-COCHANGE": { grammar: "GRAPH-COCHANGE <found|none> :: rows=<n> commits=<n>", emitter: "orc → writer", meaning: "one line per planning batch, for the file that produced the widest answer", owner: "_shared/phases/trace-verbs.md" },
4626
+ "GRAPH-HINT": { grammar: "GRAPH-HINT injected=<n> subagent_start=<n> read_notes=<n> updates=<n>", emitter: "orc → writer", meaning: "ONE line per phase close, the graph hook's counters; omitted when the counter file does not exist", owner: "_shared/phases/trace-verbs.md" },
4627
+ "GRAPH-GAIN": { grammar: "GRAPH-GAIN paid=<n> low=<n> high=<n> calls=<n>", emitter: "orc → writer", meaning: "ONE line per run at ship; `paid` is exact, `low`/`high` are an estimate never collapsed into one number", owner: "_shared/phases/trace-verbs.md" },
4628
+ "GRAPH-NOTES": { grammar: "GRAPH-NOTES <applied|below-min|none|deferred|off|skipped> :: <detail>", emitter: "orc → writer", meaning: "one notes batch; `applied` copies the noter's one-line return verbatim", owner: "_shared/phases/trace-verbs.md" },
4629
+ SPAWN: { grammar: "SPAWN <agent>", emitter: "hook", meaning: "an agent dispatch was observed (skeleton)", owner: "hooks/orc-trace.js" },
4630
+ RETURN: { grammar: "RETURN <agent> :: <desc> dur=<m>m<s>s [model=<id>]", emitter: "hook", meaning: "a subagent finished (skeleton) — not an orchestrator obligation; the model check is VERIFY", owner: "hooks/orc-trace.js" },
4631
+ DISPATCH: { grammar: "DISPATCH <agent> :: <task> expect=<model>/<effort>[ via=extra:<profile>]", emitter: "orc → writer", meaning: "orchestrator dispatched a named agent (the claim)", owner: "_shared/phases/trace-verbs.md" },
4632
+ SCORE: { grammar: "SCORE task=<id> score=<n> band=<range> model=<m> facets=<compact-vector>[ via=extra:<profile>] :: <reason>", emitter: "orc → writer", meaning: "scoring decision (tunes the rubric); a fix cycle emits `task=fix-<n>`", owner: "_shared/phases/scoring.md" },
4633
+ VERIFY: { grammar: "VERIFY <task> actual=<model>/<effort> ✅ MATCH · VERIFY <task> actual=<model>/<effort> ⛔ DOWNGRADE expected=<m>/<e>", emitter: "orc → writer", meaning: "claimed-vs-actual model check; surface a downgrade to the user, not just the trace", owner: "_shared/return-validation.md" },
4634
+ ASK: { grammar: "ASK <qid> :: offered=<o1|o2|…> rec=<o|none> chose=<o|other> by=<user|ledger|learned|config|default> [pre=<o>] [ctx=<k=v,…>]", emitter: "orc → writer", meaning: "one answered decision point — what `orc habit` learns from; the free text of an `other` answer is never stored", owner: "_shared/phases/trace-verbs.md" },
4635
+ QUESTION: { grammar: "QUESTION count=<n> :: <topic>", emitter: "subagent→orc → writer", meaning: "stopped to ask the user", owner: "_shared/phases/trace-verbs.md" },
4636
+ "CONTEXT-GAP": { grammar: "CONTEXT-GAP :: <what was already known>", emitter: "subagent→orc → writer", meaning: "asked/re-derived something already in context", owner: "_shared/phases/trace-verbs.md" },
4637
+ REPLAN: { grammar: "REPLAN wave=<n> :: <reason>", emitter: "orc → writer", meaning: "re-planned after a conflict/failure", owner: "_shared/phases/trace-verbs.md" },
4638
+ GATE: { grammar: "GATE <grounding|coverage|graph|evidence|derivation|facet|schema|judgment|wave-boundary|budget|stack-gate|stack-certainty|layer-green> pass|bounce|escalate :: <detail>", emitter: "orc → writer", meaning: "exit-gate result; `escalate` is judgment-only; bounce detail lists the misses", owner: "_shared/phases/trace-verbs.md" },
4639
+ ADVISE: { grammar: "ADVISE :: brief=<path> questions=<n>", emitter: "orc → writer", meaning: "ultra Phase U0 — advisor brief received, clarification round relayed", owner: "orc/references/ultra-mode.md" },
4640
+ JUDGE: { grammar: "JUDGE <analysis|plan|implementation> <verdict> round=<n> blocking=<n> advisory=<n> downgraded=<n>", emitter: "orc → writer", meaning: "ultra judgment verdict", owner: "orc/references/ultra-mode.md" },
4641
+ OUTCOME: { grammar: "OUTCOME task=<id> score=<n> band=<range> model=<m> retries=<n> requeues=<n> needs_context=<n> unmet=<n>", emitter: "orc → writer", meaning: "task closed — links the scoring band to what it actually took", owner: "_shared/phases/execution.md" },
4642
+ FINDING: { grammar: "FINDING p0=<n> p1=<n> p2=<n> p3=<n>[ pre=<n> suppressed=<n> folded=<n>]", emitter: "reviewer→orc → writer", meaning: "review outcome (P0–P3 severity ladder); the tail counts the after-filter's buckets", owner: "_shared/phases/review.md" },
4643
+ "FINDING-OUTCOME": { grammar: "FINDING-OUTCOME addressed=<n> disputed=<n> wontfix=<n> open=<n> pre=<n> suppressed=<n> :: <cat>:<addressed>/<total>,…", emitter: "orc → writer", meaning: "ONE line at review close — what became of each finding; `orc gotcha quality` reads it", owner: "_shared/phases/trace-verbs.md" },
4644
+ VERDICT: { grammar: "VERDICT pass|fail :: <detail>", emitter: "verifier→orc → writer", meaning: "verification outcome", owner: "_shared/phases/verify.md" },
4645
+ DRIFT: { grammar: "DRIFT loop=<n> :: <user description, compressed>", emitter: "orc → writer", meaning: "mock-example drift-recovery loop opened (hard cap 2 loops)", owner: "_shared/drift-recovery.md" },
4646
+ "TDD-RED": { grammar: "TDD-RED task=<id> iter=<n> :: <failing tests>", emitter: "executor→orc → writer", meaning: "TDD repair-loop iteration — the plan's acceptance tests still red", owner: "_shared/phases/trace-verbs.md" },
4647
+ "TDD-GREEN": { grammar: "TDD-GREEN task=<id> iter=<n>", emitter: "executor→orc → writer", meaning: "the task's TDD acceptance tests pass", owner: "_shared/phases/trace-verbs.md" },
4648
+ REPRO: { grammar: "REPRO red|green :: <cmd> exit=<n> · REPRO none :: <reason>", emitter: "executor→orc → writer", meaning: "a DEFECT task's reproduction: RED before the fix, GREEN after", owner: "_shared/return-validation.md" },
4649
+ NOTE: { grammar: "NOTE :: <decisions>", emitter: "writer", meaning: "the packet's `decisions` field — the WHY layer; one line, only when non-empty", owner: "agents/orc-trace-writer-haiku-4-5.md" },
4650
+ STATS: { grammar: "STATS lane=<l> slug=<s> dispatches=<n> waves=<n> tasks=<n> bands=<h:n,m:n,l:n> downgrades=<n> duration_ms=<n>", emitter: "orc → writer", meaning: "ONE summary line per run, in the FINISH packet, immediately BEFORE the FINISH line", owner: "_shared/phases/trace-verbs.md" },
4651
+ PACT: { grammar: "PACT <state> :: <ids> · PACT inject task=<id> :: <PACT-id> · PACT recheck pass|fail :: <ids>", emitter: "orc → writer", meaning: "invariant-ledger state at the Phase-1 probe, a promise injected into a task, the Phase-6 recheck", owner: "orc-pact/references/gate.md" },
4652
+ BOUNDARY: { grammar: "BOUNDARY <EXECUTE|ESCALATE|REFUSE|unknown> task=<id> :: <area> · BOUNDARY lift task=<id> :: <area>", emitter: "orc → writer", meaning: "per-task boundary verdict; an uncarded area is `unknown`, never REFUSE", owner: "orc-boundary/references/gate.md" },
4653
+ CHALLENGE: { grammar: "CHALLENGE iter=<n> findings=P0:<n>/P1:<n>/P2:<n> coverage=<n>% verdict=PASS|FAIL · CHALLENGE accept|rebut :: <id> · CHALLENGE regoal|retemplate :: v<n>", emitter: "orc → writer", meaning: "one line per completed /orc-challenge iteration — `orc challenge record`'s `trace_line`, verbatim", owner: "orc-challenge/SKILL.md" },
4654
+ EXTRA: { grammar: "EXTRA <profile>/<model> engine=<api|claude-shim|cli> task=<id> band=[lo,hi) tok=in/cw/cr/out outcome=<done|partial|failed|fallback> dur=<m>m<s>s · EXTRA fallback task=<id> :: <reason> → <agent> · EXTRA substitution task=<id> :: requested=<m> reported=<m> · EXTRA reroute task=<id> :: <providers> · EXTRA resume task=<id> attempt=<n> :: from=<reason> attribution=<verdict> target=<extra:profile|agent> files_preexisting=<n> · EXTRA orphan task=<id> :: attempt=<n> lease-expired files_changed=<n> state=<state> · EXTRA demote run=<slug> :: profile=<p> reason=<consecutive-stall|stale-live-attempt|manual> n=<k> → <ladder>", emitter: "orc → writer", meaning: "one line per FOREIGN dispatch — `orc extra dispatch`'s `trace_line`, verbatim", owner: "_shared/extra-dispatch.md" },
4655
+ FINISH: { grammar: "FINISH :: <detail>", emitter: "orc → writer", meaning: "run ended — mandatory, even on an abort", owner: "_shared/phases/trace.md" },
4656
+ };
4657
+
4658
+ // The verbs EVERY trace-owning lane owes, whatever its phases list. Every lane
4659
+ // sends at least the ONE end-of-run packet, and trace.md makes each of these
4660
+ // part of it: DISPATCH + VERIFY (the main return is dispatched and checked),
4661
+ // NOTE (the packet's `decisions`), STATS (every trace-owning lane emits it, not
4662
+ // just `orc`) and FINISH (mandatory, even on an abort). PHASE and CONFIG are
4663
+ // NOT here: a single-dispatch lane closes no named phase, and CONFIG is the
4664
+ // build lanes' Phase-1 line — both come in through the phase rows that emit them.
4665
+ const TRACE_ALWAYS_VERBS = ["DISPATCH", "VERIFY", "NOTE", "STATS", "FINISH"];
4666
+
4667
+ // `trace_grammar` for one lane: TRACE_VERBS order, hook verbs left out (a lane
4668
+ // never emits them), `null` for a lane that owns no trace.
4669
+ function laneTraceGrammar(lane) {
4670
+ const t = LANE_TRACE[lane];
4671
+ if (!t) return null;
4672
+ const want = new Set(TRACE_ALWAYS_VERBS);
4673
+ for (const id of LANE_PHASE_ORDER[lane] || PHASE_ORDER)
4674
+ if ((LANE_PHASES[id] || []).includes(lane)) for (const v of PHASE_FILES[id].trace_verbs || []) want.add(v);
4675
+ for (const p of LANE_OWN_PHASES[lane] || []) for (const v of p.trace_verbs || []) want.add(v);
4676
+ for (const v of t.spine_verbs || []) want.add(v);
4677
+ const out = {};
4678
+ for (const [v, d] of Object.entries(TRACE_VERBS)) if (want.has(v) && d.emitter !== "hook") out[v] = d.grammar;
4679
+ return out;
4680
+ }
4681
+
4203
4682
  // Which lanes run which SHARED phase. A lane absent from a row does not run
4204
4683
  // that phase — \`/orc-retro\` is in no trace row because it mines traces and
4205
4684
  // writes none (its hard rule 4), and \`context-combiner\` is in none because it
4206
4685
  // is a PHASE of the analyze run, not a lane (v0.42.0).
4207
4686
  const LANE_PHASES = {
4208
4687
  trace: Object.keys(LANE_TRACE),
4688
+ // v2.0.0 T1 — the verb table, ON DEMAND. Every lane that traces may need a
4689
+ // verb its `trace_grammar` does not carry, and it reaches the table through
4690
+ // trace.md (`via` in PHASE_FILES), never through its own spine.
4691
+ "trace-verbs": Object.keys(LANE_TRACE),
4209
4692
  preflight: [
4210
4693
  "orc",
4211
4694
  "orc-boundary",
@@ -4260,6 +4743,7 @@ const LANE_PHASES = {
4260
4743
  const PHASE_ORDER = [
4261
4744
  "preflight",
4262
4745
  "trace",
4746
+ "trace-verbs",
4263
4747
  "intake",
4264
4748
  "plan-handoff",
4265
4749
  "wiki-consult",
@@ -4294,6 +4778,7 @@ const PHASE_ORDER = [
4294
4778
  const LANE_PHASE_ORDER = {
4295
4779
  "orc-diy": [
4296
4780
  "trace",
4781
+ "trace-verbs",
4297
4782
  "intake",
4298
4783
  "planning",
4299
4784
  "wave-grouping",
@@ -4318,7 +4803,7 @@ const LANE_PHASE_ORDER = {
4318
4803
  // A row names a FILE and, when the phase still lives in that lane's spine, the
4319
4804
  // HEADING inside it (plan rule: a manifest names a file, a layer and at most a
4320
4805
  // heading — never a line number, /orc-doc rule 2). `read: "section"` is the
4321
- // partial-read declaration registered in `_shared/read-ladder.md`.
4806
+ // partial-read declaration registered in `_shared/phases/README.md`.
4322
4807
  //
4323
4808
  // WHY MOST ROWS NAME A HEADING RATHER THAN A FILE (v1.0.0 W14, measured):
4324
4809
  // moving a phase into its own file pays only when a run does NOT reach it —
@@ -4461,7 +4946,7 @@ const LANE_OWN_PHASES = {
4461
4946
  { ord: 0, id: "q0", file: "orc-quick/SKILL.md", heading: "## Q0 — Preflight (ONE time per session, silent, nothing can stop the run)", read: "section", trace_verbs: ["GATE", "GRAPH-CONSULT"] },
4462
4947
  { ord: 1, id: "q1", file: "orc-quick/SKILL.md", heading: "## Q1 — LOOK (silent — no questions here)", read: "section", trace_verbs: ["GATE", "WIKI-CONSULT", "GRAPH-CONSULT", "GRAPH-MAP"] },
4463
4948
  { ord: 2, id: "q2", file: "orc-quick/SKILL.md", heading: "## Q2 — ASK (ONE user turn: questions + the gate together)", read: "section", trace_verbs: [] },
4464
- { ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["DISPATCH", "VERIFY", "REPRO", "GRAPH-CHANGES", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-GAIN", "OUTCOME", "FINISH"] },
4949
+ { ord: 3, id: "q3", file: "orc-quick/SKILL.md", heading: "## Q3 — DO (dispatch → build/test → write the doc → offer)", read: "section", trace_verbs: ["DISPATCH", "VERIFY", "REPRO", "GRAPH-CHANGES", "GRAPH-UPDATE", "GRAPH-NOTES", "GRAPH-GAIN", "OUTCOME", "FINDING", "FINDING-OUTCOME", "FINISH"] },
4465
4950
  ],
4466
4951
  "orc-wiki": [
4467
4952
  { ord: 0, id: "phase-0", file: "orc-wiki/references/phases/phase-0.md", layers: ["full"], trace_verbs: [] },
@@ -4491,20 +4976,22 @@ function lanePhaseRows(lane) {
4491
4976
  file: def.file,
4492
4977
  layers,
4493
4978
  read: "whole",
4494
- // `always` must be justified (read-ladder.md, W10): a lane reads the
4979
+ // `always` must be justified (phases/README.md, W10): a lane reads the
4495
4980
  // config resolver and opens its trace pointer before it can do anything
4496
4981
  // at all. Every other phase is `on-phase` — most runs skip most phases.
4497
4982
  // `compile-time` is orc-diy and only orc-diy: it opens none of these
4498
4983
  // during a run. `orc diy compile` reads the `composed` layer once and
4499
4984
  // stitches it into FLOW-COMPILED.md, which is the only spine that run
4500
4985
  // follows. Calling that `on-phase` would describe a read that never
4501
- // happens (read-ladder.md rule 5).
4986
+ // happens (phases/README.md rule 5).
4502
4987
  when:
4503
4988
  lane === "orc-diy" && (PHASE_FILES[id].lane_layers || {})["orc-diy"]
4504
4989
  ? "compile-time"
4505
4990
  : id === "preflight" || id === "trace"
4506
4991
  ? "always"
4507
- : "on-phase",
4992
+ : PHASE_FILES[id].via
4993
+ ? "on-demand"
4994
+ : "on-phase",
4508
4995
  optional_when: null,
4509
4996
  // v1.7.0 — `rules` names its own call for the same reason `preflight`
4510
4997
  // does: the phase IS that command, and a phase row reading "none
@@ -4652,8 +5139,10 @@ function laneRailsManifest() {
4652
5139
  // is an answer, not a gap.
4653
5140
  if (!t || !t.token) continue;
4654
5141
  const rows = [];
5142
+ // An `on-demand` row is a reference a lane MAY open, never a phase a run
5143
+ // is IN — a rail step for it would be a phase the statusline can never show.
4655
5144
  for (const p of lanePhaseRows(lane))
4656
- rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
5145
+ if (p.when !== "on-demand") rows.push({ id: p.id, label: railLabel(p), kind: railKind(p), verbs: [] });
4657
5146
  for (const p of LANE_OWN_PHASES[lane] || [])
4658
5147
  rows.push({
4659
5148
  id: p.id,
@@ -4698,6 +5187,9 @@ function lanePhasesCmd(lane, claudeDir) {
4698
5187
  lane: l,
4699
5188
  trace_tier: t ? t.tier : null,
4700
5189
  trace_token: t ? t.token : null,
5190
+ // v2.0.0 T1 — the grammar line of every verb this lane's phases emit,
5191
+ // plus TRACE_ALWAYS_VERBS. A verb not here → `_shared/phases/trace-verbs.md`.
5192
+ trace_grammar: laneTraceGrammar(l),
4701
5193
  phases,
4702
5194
  shared_phase_count: phases.length,
4703
5195
  // `null` is NOT `[]`: a lane whose own pipeline is still declared in its
@@ -4728,6 +5220,11 @@ function lanePhasesCmd(lane, claudeDir) {
4728
5220
  ? ui.color.gray(`tier ${r.trace_tier} · lane token \`${r.trace_token}\``)
4729
5221
  : ui.color.gray("this lane owns no trace (it is not a run entry point)"))
4730
5222
  );
5223
+ if (r.trace_grammar)
5224
+ console.log(
5225
+ " trace grammar " +
5226
+ ui.color.gray(`${Object.keys(r.trace_grammar).length} verbs: ${Object.keys(r.trace_grammar).join(" · ")} (--json → trace_grammar)`)
5227
+ );
4731
5228
  if (!r.phases.length) {
4732
5229
  // An empty answer is an ANSWER: this lane runs no SHARED phase yet.
4733
5230
  console.log(
@@ -4798,6 +5295,9 @@ function laneConfigCmd(lane, claudeDir) {
4798
5295
  return 2;
4799
5296
  }
4800
5297
  const d = laneConfig(lane, claudeDir);
5298
+ // v2.0.0 T20 — the preflight probes' answers ride on this one call. LAST, so
5299
+ // every 1.9.2 field keeps its position.
5300
+ if (!args.includes("--no-probes")) d.probes = laneProbes(lane, args.includes("--probes-full"));
4801
5301
  if (wantsJson()) {
4802
5302
  emitJson({ ok: true, ...d });
4803
5303
  return 0;
@@ -4814,6 +5314,31 @@ function laneConfigCmd(lane, claudeDir) {
4814
5314
  console.log("");
4815
5315
  }
4816
5316
 
5317
+ // v2.0.0 T20 — the probes this lane makes at start, one line each.
5318
+ if (d.probes && Object.keys(d.probes).length) {
5319
+ console.log(ui.header("Probes (the preflight answers — exit code is data)"));
5320
+ const pw = Math.max(...Object.keys(d.probes).map((k) => k.length));
5321
+ for (const [id, p] of Object.entries(d.probes))
5322
+ console.log(` ${ui.color.cyan(id.padEnd(pw))} exit ${p.exit} ${p.error ? ui.color.yellow("error: " + p.error) : p.line}`);
5323
+ console.log("");
5324
+ }
5325
+
5326
+ // v2.0.0 W2 — only when `habits` is not off; off prints nothing new.
5327
+ if (d.habits) {
5328
+ const h = d.habits;
5329
+ console.log(ui.header("Habits"));
5330
+ console.log(" " + h.line);
5331
+ for (const l of h.learned) console.log(` learned ${l.key}: ${l.value} (${l.id}, ${l.count} of ${l.total}) undo: ${l.undo}`);
5332
+ for (const x of h.suggestions) console.log(` usual ${x.qid}: ${x.option} ${x.mark} (${x.id})`);
5333
+ if (h.proposal) {
5334
+ console.log(` proposal ${h.proposal.id} (${h.proposal.kind}) — show at the END of the run only:`);
5335
+ console.log(" " + h.proposal.line);
5336
+ console.log(ui.color.gray(` yes: ${h.proposal.yes}${h.proposal.keep ? " keep: " + h.proposal.keep : ""}${h.proposal.later ? " not now: " + h.proposal.later : ""} never: ${h.proposal.never}`));
5337
+ console.log(ui.color.gray(` trace: ${h.proposal.trace}`));
5338
+ }
5339
+ console.log("");
5340
+ }
5341
+
4817
5342
  console.log(ui.header(`Keys this lane reads (${d.keys.length})`));
4818
5343
  if (!d.keys.length) console.log(ui.color.gray(" none — this lane reads no config key."));
4819
5344
  const pad = d.keys.length ? Math.max(...d.keys.map((k) => k.key.length)) : 0;
@@ -4824,7 +5349,9 @@ function laneConfigCmd(lane, claudeDir) {
4824
5349
  ? ui.color.yellow(" shadowed")
4825
5350
  : k.is_overridden
4826
5351
  ? ui.color.green(" overridden")
4827
- : ui.color.gray(" default");
5352
+ : k.state === "learned"
5353
+ ? ui.color.cyan(" learned") + ui.color.gray(` (${k.source})`)
5354
+ : ui.color.gray(" default");
4828
5355
  console.log(` ${ui.color.cyan(k.key.padEnd(pad))} ${String(k.value).padEnd(24)} ${k.prio} ${k.family}${mark}`);
4829
5356
  if (k.inert_reason) console.log(ui.color.gray(` ↳ ${k.inert_reason}`));
4830
5357
  else if (k.shadow_reason) console.log(ui.color.gray(` ↳ ${k.shadow_reason}`));
@@ -4982,7 +5509,7 @@ const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
4982
5509
  // The tier-clip roster. It must contain EVERY agent the default table names —
4983
5510
  // `diyScoreTable()` looks each row's agent up here, so a table row naming an
4984
5511
  // agent this map does not know is a crash, not a fallback. v1.0.0 W4 added the
4985
- // two Opus 5 rows for exactly that reason.
5512
+ // two Opus 5.5 rows for exactly that reason.
4986
5513
  //
4987
5514
  // It also feeds `fixed_executor`'s option list, so every entry here is offerable
4988
5515
  // — which is what keeps the four agents no band names reachable (D14).
@@ -5014,10 +5541,10 @@ const DIY_TIERS = {
5014
5541
  "opus-4-8-high": { model: 5, effort: 2, modelId: "claude-opus-4-8", effortName: "high" },
5015
5542
  "opus-4-8-xhigh": { model: 5, effort: 3, modelId: "claude-opus-4-8", effortName: "xhigh" },
5016
5543
  "opus-4-8-max": { model: 5, effort: 4, modelId: "claude-opus-4-8", effortName: "max" },
5017
- "opus-5-med": { model: 6, effort: 1, modelId: "claude-opus-5", effortName: "medium" },
5018
- "opus-5-high": { model: 6, effort: 2, modelId: "claude-opus-5", effortName: "high" },
5019
- "opus-5-xhigh": { model: 6, effort: 3, modelId: "claude-opus-5", effortName: "xhigh" },
5020
- "opus-5-max": { model: 6, effort: 4, modelId: "claude-opus-5", effortName: "max" },
5544
+ "opus-5-med": { model: 6, effort: 1, modelId: "claude-opus-5-5", effortName: "medium" },
5545
+ "opus-5-high": { model: 6, effort: 2, modelId: "claude-opus-5-5", effortName: "high" },
5546
+ "opus-5-xhigh": { model: 6, effort: 3, modelId: "claude-opus-5-5", effortName: "xhigh" },
5547
+ "opus-5-max": { model: 6, effort: 4, modelId: "claude-opus-5-5", effortName: "max" },
5021
5548
  "fable-5-med": { model: 7, effort: 1, modelId: "claude-fable-5", effortName: "medium" },
5022
5549
  "fable-5-high": { model: 7, effort: 2, modelId: "claude-fable-5", effortName: "high" },
5023
5550
  "fable-5-xhigh": { model: 7, effort: 3, modelId: "claude-fable-5", effortName: "xhigh" },
@@ -5254,15 +5781,15 @@ function diyValidate(cfg) {
5254
5781
  if (cfg.autonomy === "hands-off" && (cfg.ship_mode === "commit" || cfg.ship_mode === "pr")) {
5255
5782
  warnings.push(`hands-off + ship_mode ${cfg.ship_mode}: git actions will run fully unattended`);
5256
5783
  }
5257
- // The pinned reviewer/verifier moved to claude-opus-5 (model rank 6) in
5784
+ // The pinned reviewer/verifier moved to claude-opus-5-5 (model rank 6) in
5258
5785
  // v0.34.0 — anything below that tier silently runs them at the session model.
5259
5786
  if (tier && tier.model < 6 && (cfg.review !== "off" || cfg.verify !== "off")) {
5260
5787
  // Careful wording (v0.34.7): `session_tier` is a DECLARATION, not the real
5261
5788
  // session model — a hook cannot read the model, only the effort. On a
5262
- // session that actually outranks the declared tier the pinned Opus 5 roles
5789
+ // session that actually outranks the declared tier the pinned Opus 5.5 roles
5263
5790
  // run at FULL pin (observed), so asserting they "will" downgrade is a false
5264
5791
  // alarm; what is certain is only that a LOWER real session caps them.
5265
- 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`);
5792
+ 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`);
5266
5793
  }
5267
5794
  return { errors, warnings };
5268
5795
  }
@@ -7646,7 +8173,7 @@ const WIKI_TIER_LADDER = [
7646
8173
  { id: "small-delta", tier: "light", why: "small delta, no new surface" },
7647
8174
  ];
7648
8175
 
7649
- // The POSITION each tier resolves to (v0.55.0). Two slots and ONE Opus 5 agent
8176
+ // The POSITION each tier resolves to (v0.55.0). Two slots and ONE Opus 5.5 agent
7650
8177
  // is not a contradiction: a slot names the POSITION, not the model, and the
7651
8178
  // ladder already collapses both tiers onto that agent while `opus5_only` is on.
7652
8179
  const WIKI_TIER_SLOT = { deep: "wiki-scanner-deep", light: "wiki-scanner-light" };
@@ -8402,6 +8929,11 @@ function gotchaStatus(claudeDir, verbose) {
8402
8929
  trigger: e.fields.trigger || null,
8403
8930
  fields: e.fields,
8404
8931
  })),
8932
+ // v2.0.0 W7 — the Behaviour panel's Gotchas tab: source, computed
8933
+ // status and a 7-week hits line per entry, plus the last sync state.
8934
+ // It READS the sync file; it never runs a sync.
8935
+ // `status` (a lane preflight) skips it: only `list` pays for the git call.
8936
+ ...(verbose ? { panel: require("./gotcha.js").panelView(claudeDir, gotchaDeps(), entries) } : {}),
8405
8937
  },
8406
8938
  entries.length ? 0 : 1
8407
8939
  );
@@ -9750,6 +10282,18 @@ ${line || ""}`;
9750
10282
  process.exit(1);
9751
10283
  }
9752
10284
 
10285
+ // The helpers `bin/gotcha.js` borrows: the v1 parser and paths stay HERE, so
10286
+ // `GOTCHA_HEAD` has one home and a 1.9.2 CLI still reads a 2.0 file.
10287
+ function gotchaDeps() {
10288
+ return { args, flag, positionals, emitJson, wantsJson, resolveClaudeDir, readOverride, repoRootOf, parseGotchas, gotchasPath, gotchasArchivePath, GOTCHA_HEAD };
10289
+ }
10290
+
10291
+ // An async importer that throws: one line, exit 1, never a stack in a lane.
10292
+ function asyncFail(e) {
10293
+ console.error("❌ " + ((e && e.message) || String(e)));
10294
+ process.exit(1);
10295
+ }
10296
+
9753
10297
  function gotcha() {
9754
10298
  if (flag("--global")) {
9755
10299
  console.error("❌ orc gotcha is project-scoped — the memory is this repo's. Run it from the project (or with --dir <path>).");
@@ -9763,7 +10307,8 @@ function gotcha() {
9763
10307
  gotchaStatus(claudeDir, false);
9764
10308
  break;
9765
10309
  case "list":
9766
- if (args.includes("--archived")) gotchaArchived(claudeDir);
10310
+ if (args.includes("--candidates")) require("./gotcha.js").gotchaCmd(gotchaDeps(), "list");
10311
+ else if (args.includes("--archived")) gotchaArchived(claudeDir);
9767
10312
  else gotchaStatus(claudeDir, true);
9768
10313
  break;
9769
10314
  case "show":
@@ -9775,13 +10320,35 @@ function gotcha() {
9775
10320
  if (args.includes("--dry-run")) gotchaPruneDryRun(claudeDir);
9776
10321
  else gotchaPrune(claudeDir);
9777
10322
  break;
10323
+ // v2.0.0 W4 — the gotchas engine (bin/gotcha.js): the CLI assigns ids,
10324
+ // dedupes, matches, builds the reviewer card and computes promotion.
10325
+ case "add":
10326
+ case "match":
10327
+ case "card":
10328
+ case "filter":
10329
+ case "observe":
10330
+ case "accept":
10331
+ case "quality":
10332
+ case "why":
10333
+ case "export":
10334
+ require("./gotcha.js").gotchaCmd(gotchaDeps(), pos[1]);
10335
+ break;
10336
+ // v2.0.0 W6a — the importers and the review-step sync (bin/gotcha-import.js).
10337
+ // They FEED `observe`; they never write a gotchas.md entry themselves.
10338
+ case "import":
10339
+ require("./gotcha-import.js").importCmd(gotchaDeps()).catch(asyncFail);
10340
+ break;
10341
+ case "sync":
10342
+ require("./gotcha-import.js").syncCmd(gotchaDeps()).catch(asyncFail);
10343
+ break;
9778
10344
  default:
9779
10345
  console.error(
9780
10346
  `Unknown: orc gotcha ${pos[1]}\n` +
9781
10347
  "Usage: orc gotcha status | list [--archived] repair memory (exit 0 = entries, 1 = none)\n" +
9782
10348
  " orc gotcha show <id> ONE entry, every field (exit 0 / 3 unknown id)\n" +
9783
10349
  " orc gotcha prune [--dry-run] archive the low-value tail down to gotchas_max.\n" +
9784
- " --dry-run NAMES every entry and writes nothing"
10350
+ " --dry-run NAMES every entry and writes nothing\n" +
10351
+ require("./gotcha.js").USAGE.replace(/^Usage: /, " ")
9785
10352
  );
9786
10353
  process.exit(1);
9787
10354
  }
@@ -10225,9 +10792,12 @@ function pr(alias) {
10225
10792
  // alias `orc pr-stack-template [<slug>]`.
10226
10793
  if (alias === "pr-stack-template") return stackTemplate(claudeDir, pos[1]);
10227
10794
  if (alias === "pr-stack-status") return stackStatus(claudeDir, pos[1]);
10795
+ // v2.0.0 W6a (Q1) — the unresolved review threads, read-only, via `gh`.
10796
+ if (pos[1] === "threads") return require("./gotcha-import.js").prThreadsCmd(gotchaDeps());
10228
10797
  const usage =
10229
10798
  "Usage: orc pr stack template [<slug>] write a fill-in stack-plan skeleton\n" +
10230
10799
  " orc pr stack status [<slug>] is a plan READY? (exit 0 ready / 1 absent-or-unfilled)\n" +
10800
+ " orc pr threads <n> [--all] the unresolved review threads (read-only; exit 0 · 1 none open · 5 gh missing/not authed)\n" +
10231
10801
  " (aliases: orc pr-stack-template, orc pr-stack-status)";
10232
10802
  if (pos[1] !== "stack") {
10233
10803
  console.error(`Unknown: orc ${pos.slice(1).join(" ") || ""}\n${usage}`);
@@ -11236,7 +11806,7 @@ function resume() {
11236
11806
  // assumed the opposite, so an interrupted turn produced a SECOND agent on the
11237
11807
  // same task while the first was still working. A graded run put THREE
11238
11808
  // `orc-executor-opus-5-low` agents on one task for 50m19s + 115m22s + 100m53s
11239
- // — 266 minutes of Opus 5 for one authorised dispatch, all editing the same
11809
+ // — 266 minutes of Opus 5.5 for one authorised dispatch, all editing the same
11240
11810
  // files. The hook already recorded every one of them; nothing ever READ it.
11241
11811
  //
11242
11812
  // The pending sidecar (`<trace>.pending.json`, written by orc-trace.js on every
@@ -11384,6 +11954,9 @@ function runCmd() {
11384
11954
  // Read-only, and the ONE reader of the pending sidecar. 0 clear / 1 in-flight / 2 unknown.
11385
11955
  if (sub === "inflight") return runInflightCmd(claudeDir);
11386
11956
 
11957
+ // v2.0.0 W6b (Q2)  the pre-run tree, so `orc undo` can never eat a user edit.
11958
+ if (sub === "snapshot") return require("./run-undo.js").snapshotCmd(runUndoDeps());
11959
+
11387
11960
  if (sub === "show") {
11388
11961
  const arg = pos[2];
11389
11962
  const pick = /^\d+$/.test(String(arg)) ? runs[Number(arg) - 1] : runs.find((r) => r.slug === arg);
@@ -11471,7 +12044,7 @@ function runCmd() {
11471
12044
  if (sub !== "list") {
11472
12045
  console.error(
11473
12046
  `Unknown subcommand: orc run ${sub}\n` +
11474
- `Try: orc run list | orc run show <slug|n> | orc run close <slug> --reason "<why>" | orc run reopen <slug>`
12047
+ `Try: orc run list | orc run show <slug|n> | orc run close <slug> --reason "<why>" | orc run reopen <slug> | orc run snapshot --run <slug>`
11475
12048
  );
11476
12049
  process.exit(1);
11477
12050
  }
@@ -11651,7 +12224,7 @@ function stats() {
11651
12224
  const empty = (msg) => {
11652
12225
  if (asJson)
11653
12226
  emitJson(
11654
- { log_dir: dir, runs: 0, from: null, to: null, lanes: {}, agents: {}, dispatches: 0, downgrades: 0, unfinished: 0, unknown_lane: 0, graph: statsGraph(claudeDir) },
12227
+ { log_dir: dir, runs: 0, from: null, to: null, lanes: {}, agents: {}, dispatches: 0, downgrades: 0, unfinished: 0, unknown_lane: 0, graph: statsGraph(claudeDir), questions: null },
11655
12228
  1
11656
12229
  );
11657
12230
  console.log(msg);
@@ -11713,6 +12286,13 @@ function stats() {
11713
12286
 
11714
12287
  const laneRows = [...lanes.entries()].sort((a, b) => b[1] - a[1]);
11715
12288
  const agentRows = [...agents.entries()].sort((a, b) => b[1] - a[1]);
12289
+ // v2.0.0 W6b (Q7) — questions per run, from the habit engine's ONE pass over
12290
+ // the ASK / QUESTION lines. null = it could not be computed, never a guess.
12291
+ let questions = null;
12292
+ try {
12293
+ const H = require("./habit.js");
12294
+ questions = H.questionStats(H.collect(claudeDir, habitDeps()), { since: typeof since === "string" && since ? since : null });
12295
+ } catch (_) {}
11716
12296
 
11717
12297
  if (asJson) {
11718
12298
  console.log(
@@ -11729,6 +12309,7 @@ function stats() {
11729
12309
  unfinished,
11730
12310
  unknown_lane: unknownLane,
11731
12311
  graph: statsGraph(claudeDir),
12312
+ questions,
11732
12313
  },
11733
12314
  null,
11734
12315
  2
@@ -11766,6 +12347,20 @@ function stats() {
11766
12347
  ["retrieval probably kept out", `~${kTok(gr.avoided_low)} – ${kTok(gr.avoided_high)} (an ESTIMATE — see \`orc graph gain\`)`],
11767
12348
  ]));
11768
12349
  }
12350
+ // Q7 — "fewer questions", measured. Every field --json carries is printed.
12351
+ if (questions && questions.runs) {
12352
+ console.log("\n" + ui.color.bold("Questions") + ` ${questions.asked} asked · ${questions.answered_for_you} answered for you`);
12353
+ console.log(ui.kv([
12354
+ ["per run (median)", String(questions.per_run_p50)],
12355
+ ["asked by a subagent", String(questions.subagent_questions)],
12356
+ ["how each was answered", Object.entries(questions.by).map(([k, v]) => `${k} ${v}`).join(" · ")],
12357
+ ["last runs, oldest first", questions.per_run_trend.map((r) => r.asked).join(" ")],
12358
+ ]));
12359
+ for (const [l, r] of Object.entries(questions.by_lane).sort((a, b) => b[1].asked - a[1].asked))
12360
+ console.log(` /${l.padEnd(18)} ${String(r.asked).padStart(3)} asked in ${plural(r.runs, "run")} · median ${r.per_run_p50} · ${r.answered_for_you} answered for you`);
12361
+ const pts = Object.entries(questions.by_point).sort((a, b) => b[1].asked - a[1].asked);
12362
+ for (const [q, r] of pts) console.log(ui.color.gray(` ${q.padEnd(34)} ${r.asked} asked · ${r.answered_for_you} answered for you`));
12363
+ }
11769
12364
  console.log("\n" + ui.color.bold("Health"));
11770
12365
  console.log(ui.kv([
11771
12366
  ["model downgrades", String(downgrades)],
@@ -21240,7 +21835,7 @@ function docForecastCmd(claudeDir, slugArg) {
21240
21835
  for (const part of roleParts) {
21241
21836
  const vec = vecFor(part.agent, part.n, pct);
21242
21837
  if (!part.target) {
21243
- const m = priceVector(claudeDir, vec, "claude-opus-5");
21838
+ const m = priceVector(claudeDir, vec, "claude-opus-5-5");
21244
21839
  if (m.usd === null) priceable = false;
21245
21840
  else total += m.usd;
21246
21841
  continue;
@@ -21264,8 +21859,8 @@ function docForecastCmd(claudeDir, slugArg) {
21264
21859
  };
21265
21860
  const usd50 = usdAt("p50");
21266
21861
  const usd90 = usdAt("p90");
21267
- const money50 = priceVector(claudeDir, p50, "claude-opus-5");
21268
- const money90 = priceVector(claudeDir, p90, "claude-opus-5");
21862
+ const money50 = priceVector(claudeDir, p50, "claude-opus-5-5");
21863
+ const money90 = priceVector(claudeDir, p90, "claude-opus-5-5");
21269
21864
  const extraView = {
21270
21865
  resolved: docExtra.resolved,
21271
21866
  why: docExtra.why,
@@ -21484,7 +22079,7 @@ function docCostCmd(claudeDir, slugArg) {
21484
22079
  runRows.push({ trace: r.name, lane: r.lane, date: r.date, tokens: runVec, weighted: weightedTokens(runVec) });
21485
22080
  }
21486
22081
 
21487
- const money = priceVector(claudeDir, total, "claude-opus-5");
22082
+ const money = priceVector(claudeDir, total, "claude-opus-5-5");
21488
22083
  const sections = Object.values(bySection).map((s) => ({
21489
22084
  id: s.id,
21490
22085
  heading: s.heading,
@@ -21493,7 +22088,7 @@ function docCostCmd(claudeDir, slugArg) {
21493
22088
  joined: s.joined > 0,
21494
22089
  tokens: s.joined ? s.vec : null,
21495
22090
  weighted: s.joined ? weightedTokens(s.vec) : null,
21496
- usd: s.joined ? priceVector(claudeDir, s.vec, "claude-opus-5").usd : null,
22091
+ usd: s.joined ? priceVector(claudeDir, s.vec, "claude-opus-5-5").usd : null,
21497
22092
  }));
21498
22093
  const unjoined = sections.filter((s) => !s.joined).length;
21499
22094
  const code = joined === 0 ? 3 : unjoined ? 1 : 0;
@@ -25827,8 +26422,8 @@ function extraResolveFor(claudeDir, score, opts) {
25827
26422
  //
25828
26423
  // `claude` is an ARRAY because `quick-executor` has two: a menu is what that
25829
26424
  // lane is. `claude_opus5` is the variant `opus5_only` would have used, derived
25830
- // from the shipped ladders — NULL where the agent is already Opus 5, so two
25831
- // wiki slots collapsing onto one Opus 5 scanner is not a contradiction: the
26425
+ // from the shipped ladders — NULL where the agent is already Opus 5.5, so two
26426
+ // wiki slots collapsing onto one Opus 5.5 scanner is not a contradiction: the
25832
26427
  // slot names the POSITION, not the model. NO AGENT IS ADDED by this release
25833
26428
  // and no pair is needed (the floor stays 51).
25834
26429
  const EXTRA_SLOTS = [
@@ -34469,6 +35064,37 @@ function doctor() {
34469
35064
  }
34470
35065
  } catch (_) {}
34471
35066
 
35067
+ // 5a-bis2) the session hook (v2.0.0 Q8/Q9). TWO entries: `Stop` (the bell,
35068
+ // armed only by `notify: bell`) and `SessionStart` on `compact` (the run
35069
+ // pointer after a compaction, always armed). One finding when either is missing.
35070
+ try {
35071
+ const cfg = resolvedConfig(claudeDir);
35072
+ let hk = {};
35073
+ try {
35074
+ hk = (JSON.parse(fs.readFileSync(path.join(claudeDir, "settings.json"), "utf8")) || {}).hooks || {};
35075
+ } catch (_) {}
35076
+ const wiredOn = (event, matcher) =>
35077
+ Array.isArray(hk[event]) &&
35078
+ hk[event].some((e) => (e.matcher || null) === (matcher || null) && (e.hooks || []).some((h) => String(h.command || "").includes("orc-session-hook")));
35079
+ const missing = [["Stop", null], ["SessionStart", "compact"], ["SubagentStart", null]].filter(([e, m]) => !wiredOn(e, m)).map(([e, m]) => (m ? `${e} ${m}` : e));
35080
+ const bell = String(cfg.notify || "off") === "bell";
35081
+ if (missing.length)
35082
+ warn(
35083
+ "session-hook-unwired",
35084
+ `session hook not wired on ${missing.join(", ")} — ` +
35085
+ [
35086
+ missing.includes("SessionStart compact") ? "a compacted session is not told which run is in flight" : null,
35087
+ missing.includes("Stop") ? (bell ? "notify is bell, but the bell never rings" : "the bell cannot ring if notify is set to bell") : null,
35088
+ missing.includes("SubagentStart") ? "a reviewer gets no gotcha card" : null,
35089
+ ]
35090
+ .filter(Boolean)
35091
+ .join("; ") +
35092
+ "; run `orc update`",
35093
+ { fixable: true, fix: "orc update", fix_command: "orc update" }
35094
+ );
35095
+ else ok(`session hook wired (Stop + SessionStart compact + SubagentStart) · notify ${bell ? "bell" : "off"}`);
35096
+ } catch (_) {}
35097
+
34472
35098
  // 5a-ter) the code graph (v1.8.0). ONE finding, and only while `code_graph` is
34473
35099
  // on — the read-gate rule. DRIFTED is the only state worth a line: NONE is
34474
35100
  // built by the next code lane's preflight, and a doctor that warns about a
@@ -40904,8 +41530,23 @@ Usage:
40904
41530
  --reason "<why>" moves RESUME.md aside (never deletes it) and records why,
40905
41531
  so it stops counting as waiting [--json]
40906
41532
  orc run reopen <slug|n> put it back — it is waiting again [--json]
41533
+ orc run snapshot --run <slug> at run start: what the tree looked like BEFORE the run
41534
+ (a git ref + the untracked files), so undo can tell your
41535
+ edits from the run's own [--json]
41536
+ orc undo --run <slug> [--files a,b] PRINTS the commands that revert ONLY the files this run
41537
+ [--apply] changed, back to the snapshot; --apply runs them. Never
41538
+ touches a file outside the run, keeps a run file you
41539
+ edited later (exit 4) [--json]
40907
41540
  orc stats [--since YYYY-MM-DD] [--json] how much you actually use each lane and agent, counted
40908
41541
  from the trace filenames — no model, instant, free
41542
+ orc habit show|log|points|why|accept|decline|forget|reset|doctor|export|purge your usual answers,
41543
+ learned from the ASK lines in your traces (config key
41544
+ \`habits\`, off by default). Never applied without your yes [--json]
41545
+ orc habit repo [accept|decline|forget <id>] soft preferences read from git history (commit,
41546
+ branch, test-file naming). An accepted one rides in
41547
+ \`orc rules slice\` as the LEARNED tier [--json]
41548
+ orc trace write --packet -|<file> write ONE phase packet into the run's trace pair (.txt +
41549
+ .jsonl). Exit 2 = a bad packet, nothing written [--json]
40909
41550
  orc wait the deterministic half of /orc-wait — a wait costs zero
40910
41551
  tokens, because a detached command does it, not a model
40911
41552
  orc wait lanes [--json] which lanes support a wait, what each one checkpoints,
@@ -43603,7 +44244,7 @@ function slEngine() {
43603
44244
  // panel really hands over — including `tokenCount`, which is the number v1.2.0
43604
44245
  // concluded could not be measured.
43605
44246
  const SL_TASK_FIXTURES = {
43606
- 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 },
44247
+ 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 },
43607
44248
  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 },
43608
44249
  empty: { id: "t3" },
43609
44250
  };
@@ -43612,7 +44253,7 @@ const SL_FIXTURES = {
43612
44253
  healthy: {
43613
44254
  label: "a healthy session mid-run",
43614
44255
  payload: {
43615
- model: { id: "claude-opus-5", display_name: "Opus 5" },
44256
+ model: { id: "claude-opus-5-5", display_name: "Opus 5.5" },
43616
44257
  effort: { level: "high" },
43617
44258
  version: "2.1.80",
43618
44259
  context_window: { used_percentage: 38, remaining_percentage: 62, context_window_size: 200000 },
@@ -44452,6 +45093,22 @@ const RULES_PRECEDENCE = [
44452
45093
  { rank: 3, layer: "ORC rules", scope: "everything", where: "<claude>/skills/_shared/rules/", beats: "nothing — it is the floor, and yours replaces it" },
44453
45094
  ];
44454
45095
 
45096
+ // v2.0.0 DE-8 — the LEARNED tier sits between your rules and the ORC rules.
45097
+ // It is named ONLY while `habits` is on: off = the ladder of the release before.
45098
+ function rulesPrecedence(claudeDir) {
45099
+ let on = false;
45100
+ try {
45101
+ on = require("./habit.js").habitsMode(readOverride(claudeDir).map) !== "off";
45102
+ } catch (_) {}
45103
+ if (!on) return RULES_PRECEDENCE;
45104
+ return [
45105
+ RULES_PRECEDENCE[0],
45106
+ RULES_PRECEDENCE[1],
45107
+ { rank: 3, layer: "learned", scope: "commit, branch and test-file naming", where: "<claude>/orc/habits-state.json (`orc habit repo`)", beats: "the ORC rules — and your rules beat it" },
45108
+ Object.assign({}, RULES_PRECEDENCE[2], { rank: 4 }),
45109
+ ];
45110
+ }
45111
+
44455
45112
  // Which packs ride in which lane. `ui` is absent from every row ON PURPOSE: it
44456
45113
  // is added PER TASK by the orchestrator when a task's declared files are
44457
45114
  // front-end. A UI rule in a backend slice is tokens paid on every spawn for a
@@ -44720,6 +45377,10 @@ function rulesLine(packs, blocks, overrides) {
44720
45377
  // which are what a rule's body is mostly made of. Nothing is hidden: the pack
44721
45378
  // file is named beside the rules, exactly as the light tiers already are.
44722
45379
  const RULES_COMPACT_LANES = new Set(["orc-quick"]);
45380
+ // v2.0.0 DE-17 — the SAME compact card for orc-mini / orc-fast, behind
45381
+ // `rules_card_compact` (default off). It flips only when eval E4 passes
45382
+ // (eval/results/2.0.0/E4.md). Off = the full card, byte for byte.
45383
+ const RULES_COMPACT_OPT_IN = new Set(["orc-mini", "orc-fast"]);
44723
45384
 
44724
45385
  // One line out of a rule body: the first paragraph, whitespace collapsed, cut
44725
45386
  // at a word boundary. A rule whose instruction does not survive that is a rule
@@ -44740,7 +45401,16 @@ function rulesSlice(claudeDir, lane, extraPacks) {
44740
45401
  const overridden = new Set(overrides.filter((o) => o.known).map((o) => o.id));
44741
45402
  const want = new Set([...(RULE_LANE_PACKS[lane] || []), ...(extraPacks || [])]);
44742
45403
  const chosen = packs.packs.filter((p) => want.has(p.id));
44743
- const compact = RULES_COMPACT_LANES.has(lane);
45404
+ let cfgMap = {};
45405
+ try {
45406
+ cfgMap = readOverride(claudeDir).map || {};
45407
+ } catch (_) {}
45408
+ const compact =
45409
+ RULES_COMPACT_LANES.has(lane) || (RULES_COMPACT_OPT_IN.has(lane) && /^(on|true)$/.test(String(cfgMap.rules_card_compact === undefined || cfgMap.rules_card_compact === null ? "on" : cfgMap.rules_card_compact)));
45410
+ // DE-8 — the LEARNED tier. `null` under `habits: off`: then the slice is
45411
+ // byte-identical to the release before, and the field is not even present.
45412
+ const learned = require("./habit.js").learnedRules(claudeDir, repoRootOf(claudeDir), cfgMap);
45413
+ const inForce = learned ? learned.filter((r) => r.state === "applied") : [];
44744
45414
 
44745
45415
  const L = [];
44746
45416
  if (!led.empty) {
@@ -44750,6 +45420,13 @@ function rulesSlice(claudeDir, lane, extraPacks) {
44750
45420
  if (body) L.push(pr, body, "");
44751
45421
  }
44752
45422
  }
45423
+ if (inForce.length) {
45424
+ L.push(
45425
+ "LEARNED PREFERENCES — computed from this repo's own history, and accepted by you. Your rules above win over these. These win over the ORC rules below.",
45426
+ ...inForce.map((r) => `${r.id} · ${r.rule}`),
45427
+ ""
45428
+ );
45429
+ }
44753
45430
  if (chosen.length) {
44754
45431
  L.push(
44755
45432
  `ORC RULES — the anti-slop baseline. Shipped with ORC, read-only. ${packs.count} rules, ${plural(chosen.length, "pack")} in this slice.`,
@@ -44793,6 +45470,9 @@ function rulesSlice(claudeDir, lane, extraPacks) {
44793
45470
  // `compact` is REPORTED, never asked for. A reader that cannot tell a short
44794
45471
  // card from a stripped one cannot trust either.
44795
45472
  compact,
45473
+ // Present ONLY when habits is on (DE-16: off = zero bytes). Stale rows are
45474
+ // listed with their reason and are NOT in `text`.
45475
+ ...(learned ? { learned } : {}),
44796
45476
  text: L.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\s+$/, ""),
44797
45477
  overrides,
44798
45478
  line: rulesLine(packs, led.blocks, overrides),
@@ -45069,7 +45749,7 @@ function rulesCmd() {
45069
45749
  },
45070
45750
  priorities: RULE_PRIORITIES,
45071
45751
  tiers: RULE_TIERS,
45072
- precedence: RULES_PRECEDENCE,
45752
+ precedence: rulesPrecedence(claudeDir),
45073
45753
  overrides,
45074
45754
  overrides_note:
45075
45755
  "counted from ORC rule ids you NAMED in your own rules. A conflict you did not name is found by the agent at dispatch and returned as rules_conflicts[] — the CLI cannot parse intent, so it does not pretend to.",
@@ -45082,7 +45762,7 @@ function rulesCmd() {
45082
45762
 
45083
45763
  console.log(`\norc rules — ${line}\n`);
45084
45764
  console.log(ui.color.bold(" Precedence") + ui.color.gray(" the order never changes"));
45085
- for (const r of RULES_PRECEDENCE)
45765
+ for (const r of rulesPrecedence(claudeDir))
45086
45766
  console.log(
45087
45767
  ` ${r.rank}. ${ui.color.cyan(r.layer.padEnd(12))} ${ui.color.gray(r.scope)}\n` +
45088
45768
  ` ${ui.color.gray(r.where)}\n` +
@@ -45245,7 +45925,7 @@ function lintScanFile(abs, rel, opts) {
45245
45925
  } catch (_) {
45246
45926
  return out;
45247
45927
  }
45248
- if (text.includes("")) return out; // binary
45928
+ if (text.includes("\u0000")) return out; // binary
45249
45929
  const lines = text.replace(/\r\n/g, "\n").split("\n");
45250
45930
 
45251
45931
  // THE CARVE-OUT, and it is not a convenience. A rule that bans a word has to
@@ -45588,6 +46268,16 @@ function jsonCrash(err) {
45588
46268
  case "graph":
45589
46269
  graphCmd();
45590
46270
  break;
46271
+ // v2.0.0 W2 — the habit engine. Every subcommand is a READ except accept,
46272
+ // decline, forget, reset and purge, which are habits-state.json's only writers.
46273
+ case "habit":
46274
+ require("./habit.js").habitCmd(habitDeps());
46275
+ break;
46276
+ // v2.0.0 T21 — the CLI holds the trace pen: one packet → the .txt + .jsonl
46277
+ // pair. The Haiku writer is the fallback when this exits ≠ 0.
46278
+ case "trace":
46279
+ require("./trace-write.js").traceCmd({ flag, positionals, emitJson, wantsJson, resolveClaudeDir, resolveLogDir, TRACE_VERBS, readOverride });
46280
+ break;
45591
46281
  case "mock":
45592
46282
  mock();
45593
46283
  break;
@@ -45677,12 +46367,20 @@ function jsonCrash(err) {
45677
46367
  case "pr-stack-status":
45678
46368
  pr(cmd);
45679
46369
  break;
46370
+ // v2.0.0 W6a (Q3) — the failing CI steps, read-only, flaky re-attempts named.
46371
+ case "ci":
46372
+ require("./gotcha-import.js").ciFailedCmd(gotchaDeps());
46373
+ break;
45680
46374
  case "resume":
45681
46375
  resume();
45682
46376
  break;
45683
46377
  case "run":
45684
46378
  runCmd();
45685
46379
  break;
46380
+ // v2.0.0 W6b (Q2)  revert ONLY what a run changed, back to its snapshot.
46381
+ case "undo":
46382
+ require("./run-undo.js").undoCmd(runUndoDeps());
46383
+ break;
45686
46384
  case "stats":
45687
46385
  stats();
45688
46386
  break;