@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
@@ -0,0 +1,657 @@
1
+ "use strict";
2
+ // v2.0.0 T21 — `orc trace write --packet -`: the CLI holds the trace pen.
3
+ //
4
+ // The orchestrator already builds a PHASE PACKET at every phase close
5
+ // (`_shared/phases/trace.md`). This module renders that ONE packet into BOTH
6
+ // halves of the trace pair — the `.txt` line and its `.jsonl` row — so the two
7
+ // can never disagree about an event. W0 measured the Haiku writer at 92.4 %
8
+ // (whole phase blocks missing from the `.jsonl`); here parity is by
9
+ // construction. The writer agent `orc-trace-writer-haiku-4-5` stays as the
10
+ // FALLBACK: a lane dispatches it only when this command exits ≠ 0.
11
+ //
12
+ // The formats are the writer's, not new ones (`agents/orc-trace-writer-haiku-4-5.md`):
13
+ // .txt [<ts>] <actor padded to 8> <VERB> :: <tail>
14
+ // .jsonl {"ts","actor","phase","verb","tail", …verbatim extra fields}
15
+ // NOTE one line from `decisions`, actor `writer`, the last event's ts
16
+ // and so are the file rules: the first (`run_meta`) packet renames a
17
+ // hook-bootstrapped `run-<DDMMYY>-<HHMMSS>.txt` (a MOVE of the .txt and its
18
+ // sidecars, reusing the bootstrap stamp); a later packet finds its file through
19
+ // `log_dir/.current`, so the FINISH packet must go out BEFORE `.current` is
20
+ // deleted. Nothing is written unless the WHOLE packet validates. Appends only —
21
+ // one append per file per block; an earlier line is never rewritten.
22
+ //
23
+ // Exit codes: 0 written · 1 usage / unreadable input · 2 invalid packet
24
+ // (unknown verb, bad ts, parse error — nothing written) · 3 trace-file state
25
+ // (no `.current` on a later packet, a rename target in the way).
26
+
27
+ const fs = require("fs");
28
+ const path = require("path");
29
+
30
+ const TS_RE = /^\d{6} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$/;
31
+ const GENERIC_RE = /^run-(\d{6})-(\d{6})\.txt$/;
32
+ const CORE_FIELDS = ["ts", "actor", "verb", "tail"];
33
+
34
+ class PacketError extends Error {}
35
+
36
+ // ── the packet parser: JSON, or the small plain-YAML shape trace.md shows ──
37
+
38
+ function stripComment(s) {
39
+ // A ` #` outside quotes starts a comment (YAML plain-scalar rule).
40
+ let q = null;
41
+ for (let i = 0; i < s.length; i++) {
42
+ const c = s[i];
43
+ if (q) {
44
+ if (c === "\\" && q === '"') i++;
45
+ else if (c === q) q = null;
46
+ } else if (c === '"' || c === "'") {
47
+ if (i === 0 || /[\s:{,\[]/.test(s[i - 1])) q = c;
48
+ } else if (c === "#" && (i === 0 || /\s/.test(s[i - 1]))) return s.slice(0, i);
49
+ }
50
+ return s;
51
+ }
52
+
53
+ // Read one quoted scalar starting at s[i] (a quote). → [value, nextIndex]
54
+ function readQuoted(s, i) {
55
+ const q = s[i];
56
+ let out = "";
57
+ for (let j = i + 1; j < s.length; j++) {
58
+ const c = s[j];
59
+ if (q === "'") {
60
+ if (c === "'") {
61
+ if (s[j + 1] === "'") {
62
+ out += "'";
63
+ j++;
64
+ continue;
65
+ }
66
+ return [out, j + 1];
67
+ }
68
+ out += c;
69
+ continue;
70
+ }
71
+ if (c === "\\") {
72
+ const n = s[++j];
73
+ if (n === "n") out += "\n";
74
+ else if (n === "t") out += "\t";
75
+ else if (n === "r") out += "\r";
76
+ else if (n === "u") {
77
+ out += String.fromCharCode(parseInt(s.slice(j + 1, j + 5), 16));
78
+ j += 4;
79
+ } else out += n === undefined ? "" : n;
80
+ continue;
81
+ }
82
+ if (c === '"') return [out, j + 1];
83
+ out += c;
84
+ }
85
+ throw new PacketError(`unterminated ${q === '"' ? "double" : "single"}-quoted string`);
86
+ }
87
+
88
+ function scalar(raw) {
89
+ const s = stripComment(String(raw)).trim();
90
+ if (s === "" || s === "~" || s === "null") return undefined;
91
+ if (s[0] === '"' || s[0] === "'") return readQuoted(s, 0)[0];
92
+ return s;
93
+ }
94
+
95
+ // Parse a flow map `{k: v, k: "v, with comma"}` (one level; a nested value is
96
+ // kept as its raw text). → object
97
+ function flowMap(s) {
98
+ s = s.trim();
99
+ if (s[0] !== "{") throw new PacketError(`expected a {…} map, got: ${s.slice(0, 40)}`);
100
+ const out = {};
101
+ let i = 1;
102
+ const ws = () => {
103
+ while (i < s.length && /\s/.test(s[i])) i++;
104
+ };
105
+ for (;;) {
106
+ ws();
107
+ if (s[i] === "}") return out;
108
+ if (i >= s.length) throw new PacketError("unterminated {…} map");
109
+ let key;
110
+ if (s[i] === '"' || s[i] === "'") [key, i] = readQuoted(s, i);
111
+ else {
112
+ const k0 = i;
113
+ while (i < s.length && s[i] !== ":" && s[i] !== "," && s[i] !== "}") i++;
114
+ key = s.slice(k0, i).trim();
115
+ }
116
+ ws();
117
+ if (s[i] !== ":") throw new PacketError(`map key "${key}" has no value`);
118
+ i++;
119
+ ws();
120
+ let val;
121
+ if (s[i] === '"' || s[i] === "'") [val, i] = readQuoted(s, i);
122
+ else if (s[i] === "{" || s[i] === "[") {
123
+ const open = s[i], close = open === "{" ? "}" : "]";
124
+ let depth = 0;
125
+ const v0 = i;
126
+ for (; i < s.length; i++) {
127
+ if (s[i] === open) depth++;
128
+ else if (s[i] === close && --depth === 0) {
129
+ i++;
130
+ break;
131
+ }
132
+ }
133
+ val = s.slice(v0, i);
134
+ } else {
135
+ const v0 = i;
136
+ while (i < s.length && s[i] !== "," && s[i] !== "}") i++;
137
+ val = s.slice(v0, i).trim();
138
+ if (val === "" || val === "~" || val === "null") val = undefined;
139
+ }
140
+ if (key) out[key] = val;
141
+ ws();
142
+ if (s[i] === ",") {
143
+ i++;
144
+ continue;
145
+ }
146
+ if (s[i] === "}") return out;
147
+ throw new PacketError(`expected "," or "}" after "${key}"`);
148
+ }
149
+ }
150
+
151
+ const indentOf = (l) => l.length - l.replace(/^ +/, "").length;
152
+ const KEY_RE = /^([A-Za-z_][\w-]*)\s*:(?:\s+(.*)|\s*)$/;
153
+
154
+ // Collect a flow map that may span lines, starting at lines[i] (text from col).
155
+ function takeFlow(lines, i, first) {
156
+ let buf = first;
157
+ const bal = (t) => (t.match(/\{/g) || []).length - (t.match(/\}/g) || []).length;
158
+ while (bal(stripComment(buf)) > 0 && i + 1 < lines.length) buf += " " + lines[++i].trim();
159
+ return [flowMap(stripComment(buf)), i];
160
+ }
161
+
162
+ function blockScalar(lines, i, style, parentIndent) {
163
+ const body = [];
164
+ let j = i + 1;
165
+ for (; j < lines.length; j++) {
166
+ const l = lines[j];
167
+ if (l.trim() !== "" && indentOf(l) <= parentIndent) break;
168
+ body.push(l);
169
+ }
170
+ while (body.length && body[body.length - 1].trim() === "") body.pop();
171
+ const ind = Math.min(...body.filter((l) => l.trim()).map(indentOf).concat([Infinity]));
172
+ const rows = body.map((l) => (l.trim() === "" ? "" : l.slice(ind === Infinity ? 0 : ind)));
173
+ let text;
174
+ if (style[0] === "|") text = rows.join("\n");
175
+ else {
176
+ text = "";
177
+ for (const r of rows) text += r === "" ? "\n" : (text && !text.endsWith("\n") ? " " : "") + r;
178
+ }
179
+ return [text, j - 1];
180
+ }
181
+
182
+ function parseYaml(text) {
183
+ const lines = text.split("\n");
184
+ const out = {};
185
+ for (let i = 0; i < lines.length; i++) {
186
+ const line = lines[i];
187
+ if (line.trim() === "" || line.trim()[0] === "#") continue;
188
+ if (indentOf(line) !== 0) throw new PacketError(`line ${i + 1}: unexpected indentation`);
189
+ const m = KEY_RE.exec(line);
190
+ if (!m) throw new PacketError(`line ${i + 1}: expected "key: value"`);
191
+ const key = m[1];
192
+ const rest = stripComment(m[2] || "").trim();
193
+ if (/^[>|][+-]?$/.test(rest)) {
194
+ [out[key], i] = blockScalar(lines, i, rest, 0);
195
+ continue;
196
+ }
197
+ if (rest[0] === "{") {
198
+ [out[key], i] = takeFlow(lines, i, rest);
199
+ continue;
200
+ }
201
+ if (rest !== "") {
202
+ out[key] = scalar(m[2]);
203
+ continue;
204
+ }
205
+ // A nested block: a sequence (`- …`) or a one-level map.
206
+ let j = i + 1;
207
+ while (j < lines.length && (lines[j].trim() === "" || lines[j].trim()[0] === "#")) j++;
208
+ if (j >= lines.length || (indentOf(lines[j]) === 0 && !/^- /.test(lines[j]))) {
209
+ out[key] = undefined;
210
+ continue;
211
+ }
212
+ if (/^\s*- /.test(lines[j]) || /^\s*-$/.test(lines[j])) {
213
+ const seq = [];
214
+ let k = j;
215
+ for (; k < lines.length; k++) {
216
+ const l = lines[k];
217
+ if (l.trim() === "" || l.trim()[0] === "#") continue;
218
+ if (indentOf(l) === 0 && !/^- /.test(l)) break;
219
+ const d = /^(\s*)- ?(.*)$/.exec(l);
220
+ if (!d) throw new PacketError(`line ${k + 1}: expected a "- " item`);
221
+ const itemIndent = d[1].length;
222
+ const body = d[2].trim();
223
+ if (body[0] === "{") {
224
+ let v;
225
+ [v, k] = takeFlow(lines, k, body);
226
+ seq.push(v);
227
+ continue;
228
+ }
229
+ const km = KEY_RE.exec(body);
230
+ if (!km) {
231
+ seq.push(scalar(body));
232
+ continue;
233
+ }
234
+ // A block-map item: this line's pair plus every deeper-indented pair.
235
+ const item = {};
236
+ const put = (kk, vv, at) => {
237
+ const r = stripComment(vv || "").trim();
238
+ if (/^[>|][+-]?$/.test(r)) {
239
+ let t;
240
+ [t, k] = blockScalar(lines, at, r, itemIndent + 2);
241
+ item[kk] = t;
242
+ } else item[kk] = scalar(vv || "");
243
+ };
244
+ put(km[1], km[2], k);
245
+ while (k + 1 < lines.length) {
246
+ const n = lines[k + 1];
247
+ if (n.trim() === "") {
248
+ k++;
249
+ continue;
250
+ }
251
+ if (indentOf(n) <= itemIndent) break;
252
+ const nm = KEY_RE.exec(n.trim());
253
+ if (!nm) throw new PacketError(`line ${k + 2}: expected "key: value" inside an item`);
254
+ k++;
255
+ put(nm[1], nm[2], k);
256
+ }
257
+ seq.push(item);
258
+ }
259
+ out[key] = seq;
260
+ i = k - 1;
261
+ continue;
262
+ }
263
+ const map = {};
264
+ let k = j;
265
+ for (; k < lines.length; k++) {
266
+ const l = lines[k];
267
+ if (l.trim() === "" || l.trim()[0] === "#") continue;
268
+ if (indentOf(l) === 0) break;
269
+ const nm = KEY_RE.exec(l.trim());
270
+ if (!nm) throw new PacketError(`line ${k + 1}: expected "key: value" under ${key}`);
271
+ map[nm[1]] = scalar(nm[2] || "");
272
+ }
273
+ out[key] = map;
274
+ i = k - 1;
275
+ }
276
+ return out;
277
+ }
278
+
279
+ function parsePacket(text) {
280
+ const t = String(text || "").replace(/^/, "").replace(/\r\n?/g, "\n");
281
+ if (!t.trim()) throw new PacketError("the packet is empty");
282
+ if (t.trim()[0] === "{") {
283
+ try {
284
+ return JSON.parse(t);
285
+ } catch (e) {
286
+ throw new PacketError(`the packet is not valid JSON: ${e.message}`);
287
+ }
288
+ }
289
+ return parseYaml(t);
290
+ }
291
+
292
+ // ── validation + rendering (pure: the golden pair tests this half) ──
293
+
294
+ const oneLine = (v) =>
295
+ String(v)
296
+ .replace(/\r/g, "")
297
+ .split("\n")
298
+ .map((s) => s.trim())
299
+ .filter(Boolean)
300
+ .join(" · ");
301
+
302
+ function kebab(slug) {
303
+ return String(slug || "")
304
+ .toLowerCase()
305
+ .replace(/[^a-z0-9]+/g, "-")
306
+ .replace(/^-+/, "")
307
+ .slice(0, 32)
308
+ .replace(/-+$/, "");
309
+ }
310
+
311
+ // The head shape of a verb, read from its grammar in TRACE_VERBS (the first form):
312
+ // `sep` = the grammar has a ` :: ` between head and tail; `args` = it names head
313
+ // arguments after the verb (a `[…]` part is optional and does not count).
314
+ const TAIL_ALIASES = ["note", "detail", "text"];
315
+ function headShape(grammar) {
316
+ const form = String(grammar || "").split(" · ")[0];
317
+ const sep = form.includes(" :: ");
318
+ const head = (sep ? form.slice(0, form.indexOf(" :: ")) : form).replace(/\[[^\]]*\]/g, " ");
319
+ const rest = head.replace(/<[^>]*>/g, "<x>").trim().split(/\s+/).slice(1).filter(Boolean);
320
+ return { sep, args: rest.length > 0 };
321
+ }
322
+
323
+ // Put ONE event into the grammar's shape, or return an error. A lane may hand the
324
+ // detail over as `note`/`detail`/`text`, put ` :: ` inside `verb`, start the tail
325
+ // with `::`, or give a no-`::` verb only a tail — each is repaired, never guessed.
326
+ function shapeEvent(e, word, d) {
327
+ let verb = oneLine(e.verb == null ? "" : e.verb);
328
+ let tail = e.tail;
329
+ let alias = null;
330
+ if (tail == null || tail === "") for (const k of TAIL_ALIASES) if (e[k] != null && e[k] !== "") { tail = e[k]; alias = k; break; }
331
+ tail = tail == null ? "" : oneLine(tail);
332
+ const cut = verb.indexOf("::");
333
+ if (cut >= 0) { tail = [verb.slice(cut + 2).trim(), tail].filter(Boolean).join(" · "); verb = verb.slice(0, cut).trim(); }
334
+ tail = tail.replace(/^(?:::\s*)+/, "").trim();
335
+ const shape = d.grammar ? headShape(d.grammar) : null;
336
+ const bare = verb === word;
337
+ if (shape && !shape.sep && bare && tail) { verb = `${word} ${tail}`; tail = ""; }
338
+ else if (shape && shape.sep && shape.args && bare) {
339
+ const i = tail.indexOf(" :: ");
340
+ if (i > 0) { verb = `${word} ${tail.slice(0, i).trim()}`; tail = tail.slice(i + 4).trim(); }
341
+ else return { error: `${word} needs its head arguments before "::" — grammar: ${d.grammar.split(" · ")[0]} — put the whole head in \`verb\` (e.g. "${d.grammar.split(" :: ")[0].split(" · ")[0]}")` };
342
+ }
343
+ return { verb, tail: tail || undefined, alias };
344
+ }
345
+
346
+ // → { phase, run_meta, events: [{ts, actor, verb, tail, extra}], note } or throws.
347
+ function validatePacket(pk, traceVerbs) {
348
+ if (!pk || typeof pk !== "object" || Array.isArray(pk)) throw new PacketError("the packet is not a map");
349
+ const errs = [];
350
+ const events = Array.isArray(pk.events) ? pk.events : [];
351
+ if (pk.events !== undefined && !Array.isArray(pk.events)) errs.push("events must be a list");
352
+ if (!events.length) errs.push("the packet has no events — zero new trace lines is a protocol violation");
353
+ const out = [];
354
+ events.forEach((e, n) => {
355
+ const at = `events[${n}]`;
356
+ if (!e || typeof e !== "object") return errs.push(`${at} is not a {ts, verb, tail} map`);
357
+ const verb = oneLine(e.verb == null ? "" : e.verb);
358
+ const word = verb.split(/\s+/)[0];
359
+ if (!word) return errs.push(`${at} has no verb`);
360
+ const d = Object.prototype.hasOwnProperty.call(traceVerbs, word) ? traceVerbs[word] : null;
361
+ if (!d) return errs.push(`${at}: unknown verb ${word} — not in the CLOSED set (trace-verbs.md)`);
362
+ if (d.emitter === "hook") return errs.push(`${at}: ${word} is written by the hook, never by a packet`);
363
+ const ts = e.ts == null ? "" : String(e.ts).trim();
364
+ if (!TS_RE.test(ts)) errs.push(`${at} (${word}): ts "${ts}" is not DDMMYY HH:MM:SS.mmm — the event's REAL time, never "now"`);
365
+ const actor = e.actor == null || e.actor === "" ? "orc" : String(e.actor).trim();
366
+ if (/\s/.test(actor)) errs.push(`${at} (${word}): actor "${actor}" has a space`);
367
+ const shaped = shapeEvent(e, word, d);
368
+ if (shaped.error) return errs.push(`${at}: ${shaped.error}`);
369
+ const extra = {};
370
+ for (const [k, v] of Object.entries(e)) {
371
+ if (CORE_FIELDS.includes(k) || k === "phase" || k === shaped.alias || v === undefined) continue;
372
+ extra[k] = typeof v === "string" && /^-?\d+(?:\.\d+)?$/.test(v) ? Number(v) : v;
373
+ }
374
+ out.push({ ts, actor, verb: shaped.verb, tail: shaped.tail, extra });
375
+ });
376
+ const rm = pk.run_meta;
377
+ if (rm !== undefined && rm !== null) {
378
+ if (typeof rm !== "object") errs.push("run_meta must be a map");
379
+ else {
380
+ if (!rm.lane || !/^[a-z0-9]+$/.test(String(rm.lane))) errs.push(`run_meta.lane "${rm.lane || ""}" is not a lane token`);
381
+ if (!kebab(rm.slug)) errs.push("run_meta.slug is missing");
382
+ }
383
+ }
384
+ if (errs.length) {
385
+ const err = new PacketError(errs.join("; "));
386
+ err.errors = errs;
387
+ throw err;
388
+ }
389
+ const decisions = pk.decisions == null ? "" : oneLine(pk.decisions);
390
+ return {
391
+ phase: pk.phase == null ? undefined : oneLine(pk.phase),
392
+ run: typeof pk.run === "string" && /^run-[a-z0-9]+-[a-z0-9-]+-\d{6}-\d{6}(\.txt)?$/.test(pk.run) ? pk.run : null,
393
+ run_meta: rm || null,
394
+ events: out,
395
+ note: decisions ? { ts: out[out.length - 1].ts, actor: "writer", verb: "NOTE", tail: decisions, extra: {} } : null,
396
+ };
397
+ }
398
+
399
+ // → { txt: [lines], jsonl: [lines] } — SAME order and SAME count.
400
+ function renderBlock(v) {
401
+ const all = v.events.concat(v.note ? [v.note] : []);
402
+ const txt = [];
403
+ const jsonl = [];
404
+ for (const e of all) {
405
+ txt.push(`[${e.ts}] ${e.actor.padEnd(8)} ${e.verb}` + (e.tail ? ` :: ${e.tail}` : ""));
406
+ const row = { ts: e.ts, actor: e.actor };
407
+ if (v.phase !== undefined) row.phase = v.phase;
408
+ row.verb = e.verb;
409
+ if (e.tail !== undefined) row.tail = e.tail;
410
+ jsonl.push(JSON.stringify(Object.assign(row, e.extra)));
411
+ }
412
+ return { txt, jsonl };
413
+ }
414
+
415
+ // ── the file half ──
416
+
417
+ const countLines = (f) => {
418
+ try {
419
+ const t = fs.readFileSync(f, "utf8");
420
+ return (t.match(/\n/g) || []).length + (t && !t.endsWith("\n") ? 1 : 0);
421
+ } catch (_) {
422
+ return 0;
423
+ }
424
+ };
425
+ const exists = (f) => {
426
+ try {
427
+ fs.statSync(f);
428
+ return true;
429
+ } catch (_) {
430
+ return false;
431
+ }
432
+ };
433
+
434
+ // One append for the whole block. A file whose last line has no newline gets
435
+ // one first, so a block never glues onto an earlier line (never rewrites it).
436
+ function appendBlock(file, rows) {
437
+ if (!rows.length) return;
438
+ let lead = "";
439
+ try {
440
+ const st = fs.statSync(file);
441
+ if (st.size > 0) {
442
+ const fd = fs.openSync(file, "r");
443
+ const b = Buffer.alloc(1);
444
+ fs.readSync(fd, b, 0, 1, st.size - 1);
445
+ fs.closeSync(fd);
446
+ if (b[0] !== 0x0a) lead = "\n";
447
+ }
448
+ } catch (_) {}
449
+ fs.appendFileSync(file, lead + rows.join("\n") + "\n");
450
+ }
451
+
452
+ class StateError extends Error {}
453
+
454
+ // Decide the file against DISK (the writer's rename rule), then append.
455
+ function writePacket(claudeDir, logDir, v, block) {
456
+ const root = path.dirname(claudeDir);
457
+ fs.mkdirSync(logDir, { recursive: true });
458
+ const ptrFile = path.join(logDir, ".current");
459
+ let ptr = null;
460
+ try {
461
+ ptr = fs.readFileSync(ptrFile, "utf8").trim() || null;
462
+ } catch (_) {}
463
+ let target;
464
+ let renamed = false;
465
+ let repointed = false;
466
+ if (v.run_meta) {
467
+ const rm = v.run_meta;
468
+ const given = rm.trace_path ? (path.isAbsolute(rm.trace_path) ? rm.trace_path : path.join(root, rm.trace_path)) : null;
469
+ const g = ptr && GENERIC_RE.exec(ptr);
470
+ if (g && exists(path.join(logDir, ptr)) && (!given || path.basename(given) !== ptr)) {
471
+ // The clobber signature: a rich packet name beside a generic bootstrap
472
+ // pointer. MOVE the bootstrap file and its sidecars; reuse its stamp.
473
+ const name = `run-${rm.lane}-${kebab(rm.slug)}-${g[1]}-${g[2]}.txt`;
474
+ const dest = path.join(logDir, name);
475
+ if (exists(dest)) {
476
+ if (fs.statSync(dest).size > 0) throw new StateError(`rename target ${name} already exists and is not empty`);
477
+ fs.unlinkSync(dest);
478
+ }
479
+ for (const sfx of ["", ".pending.json", ".jsonl"]) {
480
+ const from = path.join(logDir, ptr + sfx);
481
+ if (exists(from)) fs.renameSync(from, path.join(logDir, name + sfx));
482
+ }
483
+ fs.writeFileSync(ptrFile, name + "\n");
484
+ target = dest;
485
+ renamed = true;
486
+ } else if (given) {
487
+ target = given;
488
+ } else if (ptr) {
489
+ target = path.join(logDir, ptr);
490
+ } else throw new StateError("run_meta has no trace_path and log_dir/.current is missing — nothing names the trace file");
491
+ // A pointer naming some OTHER existing file is repointed after the write
492
+ // (the file then exists); a missing pointer is never invented here.
493
+ if (ptr && !renamed && path.basename(target) !== ptr && path.dirname(path.resolve(target)) === path.resolve(logDir)) repointed = true;
494
+ } else {
495
+ // A late packet (the ASK answers after the habits nudge) may arrive after the
496
+ // lane removed `.current`: its `run:` names an EXISTING trace in log_dir.
497
+ const late = !ptr && v.run ? path.join(logDir, path.basename(v.run).replace(/(\.txt)?$/, ".txt")) : null;
498
+ if (late && exists(late)) target = late;
499
+ else if (!ptr)
500
+ throw new StateError("log_dir/.current is missing and this packet has no run_meta — the FINISH packet must be written BEFORE .current is deleted");
501
+ else target = path.join(logDir, ptr);
502
+ }
503
+ const jsonl = target + ".jsonl";
504
+ const before = [countLines(target), countLines(jsonl)];
505
+ appendBlock(target, block.txt);
506
+ appendBlock(jsonl, block.jsonl);
507
+ if (repointed) fs.writeFileSync(ptrFile, path.basename(target) + "\n");
508
+ return {
509
+ trace_path: path.relative(root, target).split(path.sep).join("/"),
510
+ lines_written: countLines(target) - before[0],
511
+ jsonl_written: countLines(jsonl) - before[1],
512
+ renamed,
513
+ repointed,
514
+ };
515
+ }
516
+
517
+ const USAGE = "usage: orc trace write --packet -|<file> [--json] (reads one phase packet; writes the .txt + .jsonl pair)";
518
+
519
+ // deps: { flag, positionals, emitJson, wantsJson, resolveClaudeDir, resolveLogDir, TRACE_VERBS }
520
+ // The habits capture check (eval E2, 27-09-2026): a live lane read habits.md at
521
+ // preflight and then wrote no ASK at the question it asked minutes later. So at the
522
+ // FINISH packet, with `habits` on and ZERO `ASK` lines in the run, the CLI names
523
+ // this lane's registered questions and asks for ONE more packet with the answers.
524
+ // It never blocks: the packet is already written, the exit stays 0.
525
+ function askNudge(deps, claudeDir, v, r) {
526
+ if (!deps.readOverride || !v.events.some((e) => e.verb.split(/\s+/)[0] === "FINISH")) return null;
527
+ let H;
528
+ try { H = require("./habit.js"); } catch (_) { return null; }
529
+ let mode = "off";
530
+ try { mode = H.habitsMode(deps.readOverride(claudeDir).map); } catch (_) {}
531
+ if (mode === "off") return null;
532
+ const file = path.join(path.dirname(claudeDir), r.trace_path);
533
+ let txt = "";
534
+ try { txt = fs.readFileSync(file, "utf8"); } catch (_) { return null; }
535
+ // The qids this run already recorded (a VALID ASK line — invalid ones never reach the file).
536
+ const done = new Set();
537
+ for (const m of txt.matchAll(/\] \S+\s+ASK (\S+) ::/g)) done.add(m[1]);
538
+ const tok = (/(?:^|\/)run-([a-z0-9]+)-/.exec(r.trace_path) || [])[1];
539
+ if (!tok) return null;
540
+ const lane = tok === "orc" || tok === "ultra" ? "orc" : `orc-${tok}`;
541
+ const qids = Object.keys(H.ASK_POINTS).filter((q) => (H.ASK_POINTS[q].lanes || []).includes(lane) && !done.has(q));
542
+ if (!qids.length) return null;
543
+ return {
544
+ qids,
545
+ line: `habits: ${mode} — ${done.size ? `this run recorded ASK for ${[...done].join(", ")} only` : "this run wrote NO ASK line"}. If you asked the user any question below, write ONE more packet now (with \`run: ${path.basename(r.trace_path)}\` — it works even after .current is removed): one ASK event per question you asked the user that is marked (H <qid>) — ${lane}: ${qids.map((q) => { const o = Object.keys(H.ASK_POINTS[q].options || {}); return o.length ? `${q} (${o.join("|")})` : `${q} (the agent names you offered)`; }).join(", ")}. Use THESE option ids exactly. verb "ASK <qid>", tail "offered=<o1|o2|…> rec=<o|none> chose=<o|other> by=<user|ledger|learned|config|default>". A question you did not ask this run → no event.`,
546
+ };
547
+ }
548
+
549
+ function traceCmd(deps) {
550
+ const pos = deps.positionals();
551
+ const json = deps.wantsJson();
552
+ const fail = (code, state, reason, extra) => {
553
+ if (json) return deps.emitJson(Object.assign({ ok: false, state, reason }, extra || {}), code);
554
+ console.error(`trace write: ${reason}`);
555
+ process.exit(code);
556
+ };
557
+ if (pos[1] !== "write") return fail(1, "usage", USAGE);
558
+ const src = deps.flag("--packet");
559
+ if (src === undefined) return fail(1, "usage", USAGE);
560
+ let text;
561
+ try {
562
+ text = src === true || src === "-" ? fs.readFileSync(0, "utf8") : fs.readFileSync(path.resolve(String(src)), "utf8");
563
+ } catch (e) {
564
+ return fail(1, "unreadable", `cannot read the packet (${e.code || e.message})`);
565
+ }
566
+ let v;
567
+ try {
568
+ v = validatePacket(parsePacket(text), deps.TRACE_VERBS);
569
+ } catch (e) {
570
+ if (!(e instanceof PacketError)) throw e;
571
+ return fail(2, "invalid", `${e.message} — nothing written`, { errors: e.errors || [e.message] });
572
+ }
573
+ // An ASK is checked against the habit registry AS IT ARRIVES (eval E2): a live lane
574
+ // wrote its own option names and extra text, and the habit engine then counted
575
+ // nothing. An invalid ASK is kept OUT of the trace and handed back with the option
576
+ // ids, so the lane resends it in the same turn; the other events are written.
577
+ const askRejected = [];
578
+ let H = null;
579
+ try { H = require("./habit.js"); } catch (_) {}
580
+ // The trace so far, to tell a FOLLOW-UP from a new answer: the same qid again
581
+ // with no new executor SPAWN (the hook writes those, deterministically) since
582
+ // its last ASK is the same entry's next decision (eval E2: `stop` at the commit
583
+ // offer after the review, recorded as quick.q3.offer.review).
584
+ let sofar = "";
585
+ let sofarName = null;
586
+ try {
587
+ const ld = deps.resolveLogDir(deps.resolveClaudeDir());
588
+ let cur = null;
589
+ try { cur = fs.readFileSync(path.join(ld, ".current"), "utf8").trim() || null; } catch (_) {}
590
+ const name = cur || (v.run ? path.basename(v.run).replace(/(\.txt)?$/, ".txt") : null);
591
+ sofarName = name;
592
+ if (name) sofar = fs.readFileSync(path.join(ld, name), "utf8");
593
+ } catch (_) {}
594
+ const followUp = (qid) => {
595
+ const lines = sofar.split(/\r?\n/);
596
+ let last = -1;
597
+ lines.forEach((l, i) => { if (new RegExp(`\\]\\s+\\S+\\s+ASK ${qid.replace(/\./g, "\\.")} ::`).test(l)) last = i; });
598
+ if (last < 0) return false;
599
+ return !lines.slice(last + 1).some((l) => /\]\s+hook\s+SPAWN orc-executor-/.test(l));
600
+ };
601
+ if (H) {
602
+ const seen = new Set();
603
+ v.events = v.events.filter((e) => {
604
+ if (e.verb.split(/\s+/)[0] !== "ASK") return true;
605
+ const p = H.parseAskLine(`${e.verb} :: ${e.tail || ""}`);
606
+ if (p && p.ok !== false && (followUp(p.qid) || seen.has(p.qid))) {
607
+ askRejected.push({ qid: p.qid, error: "a follow-up decision in the SAME entry (no new executor dispatch since this question was answered) — record no ASK for it", options: "none" });
608
+ return false;
609
+ }
610
+ // eval E2: a live lane invented `q3-review`, `q-exec` — an id the registry
611
+ // does not know counts for nothing, so it is refused with the real ids.
612
+ if (p && p.ok !== false && p.known === false && !(H.HABIT_META_QIDS || []).includes(p.qid)) {
613
+ const tok = (/(?:^|\/)run-([a-z0-9]+)-/.exec(sofarName || "") || [])[1];
614
+ const lane = !tok ? null : tok === "orc" || tok === "ultra" ? "orc" : `orc-${tok}`;
615
+ const ids = Object.keys(H.ASK_POINTS).filter((q) => !lane || (H.ASK_POINTS[q].lanes || []).includes(lane));
616
+ askRejected.push({ qid: p.qid, error: `unknown question id — use one of: ${ids.join(", ")}`, options: "see the id list" });
617
+ return false;
618
+ }
619
+ if (p && p.ok !== false) { seen.add(p.qid); return true; }
620
+ const qid = e.verb.split(/\s+/)[1] || "";
621
+ const pt = H.ASK_POINTS[qid];
622
+ const o = pt ? Object.keys(pt.options || {}) : [];
623
+ askRejected.push({ qid, error: (p && p.error) || "not an ASK line", options: o.length ? o.join("|") : "the agent names you offered (plain ids, no spaces)" });
624
+ return false;
625
+ });
626
+ }
627
+ if (askRejected.length && !v.events.length && !v.note) {
628
+ const line = `ASK rejected — nothing written. Resend each as verb "ASK <qid>", tail "offered=<a|b> rec=<o> chose=<o> by=user" with these ids: ${askRejected.map((a) => `${a.qid} (${a.options}) — ${a.error}`).join("; ")}`;
629
+ if (json) return deps.emitJson({ ask_rejected_line: line, ok: true, lines_written: 0, jsonl_written: 0, ask_rejected: askRejected }, 0);
630
+ console.log(line);
631
+ return;
632
+ }
633
+ const claudeDir = deps.resolveClaudeDir();
634
+ let r;
635
+ try {
636
+ r = writePacket(claudeDir, deps.resolveLogDir(claudeDir), v, renderBlock(v));
637
+ } catch (e) {
638
+ if (!(e instanceof StateError)) return fail(3, "error", `${e.message} — nothing written`);
639
+ return fail(3, "trace-state", `${e.message} — nothing written`);
640
+ }
641
+ const nudge = askNudge(deps, claudeDir, v, r);
642
+ // The messages go FIRST: a lane often pipes the answer through `head`.
643
+ const rejLine = askRejected.length
644
+ ? `ASK rejected (the other events were written) — resend each as verb "ASK <qid>", tail "offered=<a|b> rec=<o> chose=<o> by=user" with these ids: ${askRejected.map((a) => `${a.qid} (${a.options}) — ${a.error}`).join("; ")}`
645
+ : null;
646
+ const head = Object.assign(rejLine ? { ask_rejected_line: rejLine } : {}, nudge ? { ask_missing_line: nudge.line } : {});
647
+ if (json) return deps.emitJson(Object.assign(head, { ok: true }, r, rejLine ? { ask_rejected: askRejected } : {}, nudge ? { ask_missing: nudge } : {}), 0);
648
+ if (rejLine) console.log(rejLine);
649
+ if (nudge) console.log(nudge.line);
650
+ console.log(
651
+ `trace write: +${r.lines_written} lines, +${r.jsonl_written} jsonl → ${r.trace_path}` +
652
+ (r.renamed ? " (renamed the bootstrap file)" : "") +
653
+ (r.repointed ? " (.current repointed)" : "")
654
+ );
655
+ }
656
+
657
+ module.exports = { parsePacket, validatePacket, renderBlock, writePacket, traceCmd, kebab, PacketError, StateError, USAGE };