harnery 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +16 -6
  2. package/dist/commander.d.ts +29 -0
  3. package/dist/commander.d.ts.map +1 -1
  4. package/dist/commander.js +4 -0
  5. package/dist/commands/agents.d.ts.map +1 -1
  6. package/dist/commands/agents.js +94 -33
  7. package/dist/commands/browse-ai.js +1 -1
  8. package/dist/commands/browse.d.ts.map +1 -1
  9. package/dist/commands/browse.js +41 -9
  10. package/dist/commands/cookies.js +1 -1
  11. package/dist/commands/decision.d.ts +4 -0
  12. package/dist/commands/decision.d.ts.map +1 -0
  13. package/dist/commands/decision.js +354 -0
  14. package/dist/commands/deinit.d.ts.map +1 -1
  15. package/dist/commands/deinit.js +4 -0
  16. package/dist/commands/devtools.d.ts +4 -0
  17. package/dist/commands/devtools.d.ts.map +1 -0
  18. package/dist/commands/devtools.js +239 -0
  19. package/dist/commands/docs.d.ts.map +1 -1
  20. package/dist/commands/docs.js +74 -2
  21. package/dist/commands/doctor.js +12 -4
  22. package/dist/commands/env.d.ts.map +1 -1
  23. package/dist/commands/env.js +3 -63
  24. package/dist/commands/fetch.js +1 -1
  25. package/dist/commands/init.d.ts +1 -0
  26. package/dist/commands/init.d.ts.map +1 -1
  27. package/dist/commands/init.js +54 -14
  28. package/dist/commands/scratch.js +1 -1
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +273 -62
  31. package/dist/commands/web-fetch.js +1 -1
  32. package/dist/core/agents/coord-client.d.ts.map +1 -1
  33. package/dist/core/agents/coord-client.js +32 -8
  34. package/dist/core/agents/events/consume.d.ts +25 -2
  35. package/dist/core/agents/events/consume.d.ts.map +1 -1
  36. package/dist/core/agents/events/consume.js +55 -7
  37. package/dist/core/agents/events/emit.d.ts +2 -1
  38. package/dist/core/agents/events/emit.d.ts.map +1 -1
  39. package/dist/core/agents/events/emit.js +6 -1
  40. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  41. package/dist/core/agents/rules/claim-conflict.js +16 -5
  42. package/dist/core/agents/state/scratch.d.ts +1 -1
  43. package/dist/core/agents/state/scratch.js +2 -2
  44. package/dist/core/config.d.ts +10 -0
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +13 -0
  47. package/dist/core/hooks/cli.js +3 -3
  48. package/dist/core/hooks/effects/index.d.ts +11 -7
  49. package/dist/core/hooks/effects/index.d.ts.map +1 -1
  50. package/dist/core/hooks/effects/index.js +15 -17
  51. package/dist/core/hooks/events/emit.d.ts.map +1 -1
  52. package/dist/core/hooks/events/emit.js +4 -0
  53. package/dist/core/hooks/events/rotate.d.ts +43 -0
  54. package/dist/core/hooks/events/rotate.d.ts.map +1 -0
  55. package/dist/core/hooks/events/rotate.js +142 -0
  56. package/dist/core/hooks/harness/events.d.ts +11 -1
  57. package/dist/core/hooks/harness/events.d.ts.map +1 -1
  58. package/dist/core/hooks/harness/events.js +22 -3
  59. package/dist/core/hooks/harness/wiring.d.ts +8 -0
  60. package/dist/core/hooks/harness/wiring.d.ts.map +1 -1
  61. package/dist/core/hooks/harness/wiring.js +34 -5
  62. package/dist/core/scratch/index.d.ts.map +1 -0
  63. package/dist/{lib → core}/scratch/index.js +2 -2
  64. package/dist/lib/agent-browser/client.js +1 -1
  65. package/dist/lib/browser/client.d.ts +14 -0
  66. package/dist/lib/browser/client.d.ts.map +1 -1
  67. package/dist/lib/browser/client.js +20 -0
  68. package/dist/lib/browser/index.d.ts +1 -0
  69. package/dist/lib/browser/index.d.ts.map +1 -1
  70. package/dist/lib/browser/runts.d.ts +44 -0
  71. package/dist/lib/browser/runts.d.ts.map +1 -0
  72. package/dist/lib/browser/runts.js +193 -0
  73. package/dist/lib/completion/walk.js +1 -1
  74. package/dist/lib/cookies/client.d.ts +1 -1
  75. package/dist/lib/cookies/client.d.ts.map +1 -1
  76. package/dist/lib/cookies/client.js +1 -1
  77. package/dist/lib/decision/index.d.ts +212 -0
  78. package/dist/lib/decision/index.d.ts.map +1 -0
  79. package/dist/lib/decision/index.js +523 -0
  80. package/dist/lib/devtools.d.ts +178 -0
  81. package/dist/lib/devtools.d.ts.map +1 -0
  82. package/dist/lib/devtools.js +1328 -0
  83. package/dist/lib/docs-frontmatter-migrate.d.ts +33 -0
  84. package/dist/lib/docs-frontmatter-migrate.d.ts.map +1 -0
  85. package/dist/lib/docs-frontmatter-migrate.js +364 -0
  86. package/dist/lib/docs-frontmatter.d.ts +33 -0
  87. package/dist/lib/docs-frontmatter.d.ts.map +1 -0
  88. package/dist/lib/docs-frontmatter.js +130 -0
  89. package/dist/lib/docs-index.d.ts +1 -0
  90. package/dist/lib/docs-index.d.ts.map +1 -1
  91. package/dist/lib/docs-index.js +4 -5
  92. package/dist/lib/docs-lint.d.ts +3 -0
  93. package/dist/lib/docs-lint.d.ts.map +1 -1
  94. package/dist/lib/docs-lint.js +67 -12
  95. package/dist/lib/docs-meta.d.ts +14 -0
  96. package/dist/lib/docs-meta.d.ts.map +1 -0
  97. package/dist/lib/docs-meta.js +34 -0
  98. package/dist/lib/docs-sweep.d.ts +12 -0
  99. package/dist/lib/docs-sweep.d.ts.map +1 -1
  100. package/dist/lib/docs-sweep.js +98 -103
  101. package/dist/lib/format.js +2 -2
  102. package/dist/lib/http/index.d.ts +1 -0
  103. package/dist/lib/http/index.d.ts.map +1 -1
  104. package/dist/lib/http/index.js +1 -0
  105. package/dist/lib/http/request.d.ts +77 -0
  106. package/dist/lib/http/request.d.ts.map +1 -0
  107. package/dist/lib/http/request.js +105 -0
  108. package/dist/lib/instructions/apply.d.ts +63 -0
  109. package/dist/lib/instructions/apply.d.ts.map +1 -0
  110. package/dist/lib/instructions/apply.js +255 -0
  111. package/dist/lib/instructions/splice.d.ts +73 -0
  112. package/dist/lib/instructions/splice.d.ts.map +1 -0
  113. package/dist/lib/instructions/splice.js +118 -0
  114. package/dist/lib/instructions/templates.d.ts +45 -0
  115. package/dist/lib/instructions/templates.d.ts.map +1 -0
  116. package/dist/lib/instructions/templates.js +258 -0
  117. package/dist/lib/tunnel/gate.d.ts +1 -0
  118. package/dist/lib/tunnel/gate.d.ts.map +1 -1
  119. package/dist/lib/tunnel/gate.js +15 -10
  120. package/dist/lib/tunnel/state.d.ts +11 -1
  121. package/dist/lib/tunnel/state.d.ts.map +1 -1
  122. package/dist/lib/tunnel/state.js +8 -3
  123. package/package.json +9 -6
  124. package/src/commander.ts +35 -0
  125. package/src/commands/agents.ts +97 -29
  126. package/src/commands/browse-ai.ts +1 -1
  127. package/src/commands/browse.ts +63 -8
  128. package/src/commands/cookies.ts +1 -1
  129. package/src/commands/decision.ts +438 -0
  130. package/src/commands/deinit.ts +5 -0
  131. package/src/commands/devtools.ts +284 -0
  132. package/src/commands/docs.ts +86 -2
  133. package/src/commands/doctor.ts +13 -4
  134. package/src/commands/env.ts +11 -77
  135. package/src/commands/fetch.ts +1 -1
  136. package/src/commands/init.ts +66 -15
  137. package/src/commands/scratch.ts +1 -1
  138. package/src/commands/tunnel.ts +316 -65
  139. package/src/commands/web-fetch.ts +1 -1
  140. package/src/core/agents/coord-client.ts +34 -7
  141. package/src/core/agents/events/consume.ts +65 -7
  142. package/src/core/agents/events/emit.ts +7 -1
  143. package/src/core/agents/rules/claim-conflict.ts +17 -6
  144. package/src/core/agents/state/scratch.ts +2 -2
  145. package/src/core/config.ts +15 -1
  146. package/src/core/hooks/cli.ts +3 -3
  147. package/src/core/hooks/effects/index.ts +23 -16
  148. package/src/core/hooks/events/emit.ts +5 -0
  149. package/src/core/hooks/events/rotate.ts +151 -0
  150. package/src/core/hooks/harness/events.ts +30 -3
  151. package/src/core/hooks/harness/wiring.ts +46 -5
  152. package/src/{lib → core}/scratch/index.ts +2 -2
  153. package/src/lib/agent-browser/client.ts +1 -1
  154. package/src/lib/browser/client.ts +28 -0
  155. package/src/lib/browser/index.ts +4 -0
  156. package/src/lib/browser/runts.ts +218 -0
  157. package/src/lib/completion/walk.ts +1 -1
  158. package/src/lib/cookies/client.ts +2 -2
  159. package/src/lib/decision/index.ts +685 -0
  160. package/src/lib/devtools.ts +1653 -0
  161. package/src/lib/docs-frontmatter-migrate.ts +427 -0
  162. package/src/lib/docs-frontmatter.ts +151 -0
  163. package/src/lib/docs-index.ts +4 -5
  164. package/src/lib/docs-lint.ts +61 -11
  165. package/src/lib/docs-meta.ts +44 -0
  166. package/src/lib/docs-sweep.ts +104 -102
  167. package/src/lib/format.ts +2 -2
  168. package/src/lib/http/index.ts +1 -0
  169. package/src/lib/http/request.ts +154 -0
  170. package/src/lib/instructions/apply.ts +318 -0
  171. package/src/lib/instructions/splice.ts +148 -0
  172. package/src/lib/instructions/templates.ts +295 -0
  173. package/src/lib/tunnel/gate.ts +15 -10
  174. package/src/lib/tunnel/state.ts +19 -4
  175. package/dist/lib/scratch/index.d.ts.map +0 -1
  176. /package/dist/{lib → core}/scratch/index.d.ts +0 -0
@@ -0,0 +1,438 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import type { Command } from "commander";
3
+ import type { EmitContext } from "../commander.ts";
4
+ import {
5
+ emitCanonical,
6
+ monorepoRoot,
7
+ normalizeHarness,
8
+ readHeartbeat,
9
+ resolveOwner,
10
+ } from "../core/agents/index.ts";
11
+ import { resolveBinName } from "../core/config.ts";
12
+ import {
13
+ archiveDecision,
14
+ claimDecision,
15
+ DECISION_STAKES,
16
+ type DecisionManifest,
17
+ type DecisionStakes,
18
+ type DecisionStatus,
19
+ type DecisionTier,
20
+ fileDecision,
21
+ isStakes,
22
+ isStatus,
23
+ isTier,
24
+ isVerdict,
25
+ listDecisions,
26
+ type ReviewVerdict,
27
+ reopenDecision,
28
+ resolveDecision,
29
+ reviewDecision,
30
+ searchDecisions,
31
+ showDecision,
32
+ supersedeDecision,
33
+ triageDecision,
34
+ wontfixDecision,
35
+ } from "../lib/decision/index.ts";
36
+
37
+ /**
38
+ * `harn decision`: the decision docket — a persistent queue of decisions an
39
+ * agent would otherwise route to a human, carried through triage → deliberation
40
+ * → an evidence-cited resolution → async review.
41
+ *
42
+ * The engine is generic: it stores `tier` (0/1/2) + `stakes` but never decides
43
+ * what belongs in which tier — that's host policy, applied by the filing agent
44
+ * (for a host, via its own decision skill + rubric).
45
+ */
46
+ let emit: EmitContext;
47
+
48
+ export function registerDecisionCommand(program: Command, emitParam: EmitContext): void {
49
+ emit = emitParam;
50
+ const bin = () => resolveBinName();
51
+ const root = program
52
+ .command("decision")
53
+ .alias("decisions")
54
+ .description(
55
+ "Decision docket: file a decision an agent would otherwise escalate, " +
56
+ "deliberate it, resolve with cited evidence, review async.",
57
+ );
58
+
59
+ // ── file ─────────────────────────────────────────────────────────────────
60
+ root
61
+ .command("file <question...>")
62
+ .description("File a decision into the docket. Tier/stakes are the filer's triage call.")
63
+ .option("--tier <0|1|2>", "Human-involvement tier (0 none, 1 review, 2 decide-with-brief)", "2")
64
+ .option("--stakes <small|medium|high>", "Reversal cost / blast radius", "medium")
65
+ .option("--context <text>", "Why it matters, what's blocked")
66
+ .option("--default-taken <text>", "What you proceeded with (always-proceed, tier 0/1)")
67
+ .option("--brief <path>", "Path to a markdown file with the long-form brief")
68
+ .option("--filed-by <name>", "Filer agent name (else resolved from heartbeat)")
69
+ .action((question: string[], opts: FileOpts) => {
70
+ const coordRoot = coordRootOrExit();
71
+ const tier = parseTier(opts.tier);
72
+ const stakes = parseStakes(opts.stakes);
73
+ let brief: string | undefined;
74
+ if (opts.brief) {
75
+ if (!existsSync(opts.brief)) {
76
+ emit.error({ code: "no_brief_file", message: `brief file not found: ${opts.brief}` });
77
+ process.exit(1);
78
+ }
79
+ brief = readFileSync(opts.brief, "utf8");
80
+ }
81
+ const owner = resolveOwner();
82
+ const hb = owner ? readHeartbeat(owner) : null;
83
+ const r = fileDecision(coordRoot, {
84
+ question: question.join(" "),
85
+ tier,
86
+ stakes,
87
+ context: opts.context,
88
+ defaultTaken: opts.defaultTaken,
89
+ brief,
90
+ filedBy: opts.filedBy ?? hb?.name ?? undefined,
91
+ filedById: owner ?? undefined,
92
+ });
93
+ if (!r.ok) return fail("file_failed", r.reason);
94
+ emitDecisionEvent("decision.filed", {
95
+ decision_id: r.manifest!.decision_id,
96
+ tier,
97
+ stakes,
98
+ });
99
+ emit.data(r.manifest);
100
+ });
101
+
102
+ // ── list ─────────────────────────────────────────────────────────────────
103
+ root
104
+ .command("list")
105
+ .description("List docket decisions (default: active only, newest first).")
106
+ .option("--status <status>", "Filter by status")
107
+ .option("--tier <0|1|2>", "Filter by tier")
108
+ .option("--stakes <small|medium|high>", "Filter by stakes")
109
+ .option("--open", "Only non-terminal decisions (the live queue)")
110
+ .option("--archived", "Include archived (graduated/terminal) decisions")
111
+ .action((opts: ListOpts) => {
112
+ const coordRoot = coordRootOrExit();
113
+ const status = opts.status ? parseStatus(opts.status) : undefined;
114
+ const tier = opts.tier !== undefined ? parseTier(opts.tier) : undefined;
115
+ const stakes = opts.stakes ? parseStakes(opts.stakes) : undefined;
116
+ const rows = listDecisions(coordRoot, {
117
+ status,
118
+ tier,
119
+ stakes,
120
+ openOnly: opts.open,
121
+ includeArchived: opts.archived,
122
+ }).map(summarize);
123
+ emit.data({
124
+ rows,
125
+ meta: {
126
+ total: rows.length,
127
+ filter: { status, tier, stakes, open: !!opts.open, archived: !!opts.archived },
128
+ },
129
+ });
130
+ });
131
+
132
+ // ── show ─────────────────────────────────────────────────────────────────
133
+ root
134
+ .command("show <id>")
135
+ .description("Show one decision: manifest + long-form bodies.")
136
+ .action((id: string) => {
137
+ const coordRoot = coordRootOrExit();
138
+ const detail = showDecision(coordRoot, id);
139
+ if (!detail) return fail("not_found", `no decision "${id}"`);
140
+ emit.data(detail);
141
+ });
142
+
143
+ // ── search ───────────────────────────────────────────────────────────────
144
+ root
145
+ .command("search <query...>")
146
+ .description("Substring search over questions, context, resolutions, bodies (incl. archive).")
147
+ .action((query: string[]) => {
148
+ const coordRoot = coordRootOrExit();
149
+ const hits = searchDecisions(coordRoot, query.join(" ")).map((h) => ({
150
+ ...summarize(h.manifest),
151
+ where: h.where,
152
+ snippet: h.snippet,
153
+ }));
154
+ emit.data({ rows: hits, meta: { total: hits.length, query: query.join(" ") } });
155
+ });
156
+
157
+ // ── claim ────────────────────────────────────────────────────────────────
158
+ root
159
+ .command("claim <id>")
160
+ .description("Claim a decision for deliberation (last-writer-wins).")
161
+ .option("--owner <id>", "Claim as this owner (else the current agent)")
162
+ .action((id: string, opts: { owner?: string }) => {
163
+ const coordRoot = coordRootOrExit();
164
+ const owner = opts.owner ?? resolveOwner();
165
+ if (!owner) {
166
+ return fail(
167
+ "no_owner",
168
+ `not in an agent session; pass --owner or run \`${bin()} agents whoami\` to check`,
169
+ );
170
+ }
171
+ const r = claimDecision(coordRoot, id, owner);
172
+ if (!r.ok) return fail("claim_failed", r.reason);
173
+ emit.data(r.manifest);
174
+ });
175
+
176
+ // ── resolve ──────────────────────────────────────────────────────────────
177
+ root
178
+ .command("resolve <id>")
179
+ .description("Resolve a decision. Evidence (≥1 citation) is required.")
180
+ .requiredOption("--recommendation <text>", "The recommendation")
181
+ .option(
182
+ "--evidence <text>",
183
+ "A cited fact (query run, file read, cost computed). Repeatable; ≥1 required.",
184
+ collect,
185
+ [] as string[],
186
+ )
187
+ .option("--confidence <text>", "Confidence in the recommendation")
188
+ .option("--reversal-cost <text>", "Cost to reverse if wrong")
189
+ .option("--wrong-if <text>", "What would make this wrong (pre-mortem)")
190
+ .option("--revisit-when <text>", "Revisit trigger")
191
+ .option("--resolved-by <name>", "Resolver (else the current agent)")
192
+ .action((id: string, opts: ResolveOpts) => {
193
+ const coordRoot = coordRootOrExit();
194
+ const owner = resolveOwner();
195
+ const resolvedBy = opts.resolvedBy ?? (owner ? readHeartbeat(owner)?.name : undefined);
196
+ if (!resolvedBy) {
197
+ return fail("no_resolver", "pass --resolved-by (no agent session to infer it from)");
198
+ }
199
+ const r = resolveDecision(coordRoot, id, {
200
+ recommendation: opts.recommendation,
201
+ evidence: opts.evidence,
202
+ confidence: opts.confidence,
203
+ reversal_cost: opts.reversalCost,
204
+ wrong_if: opts.wrongIf,
205
+ revisit_when: opts.revisitWhen,
206
+ resolved_by: resolvedBy,
207
+ });
208
+ if (!r.ok) return fail("resolve_failed", r.reason);
209
+ emitDecisionEvent("decision.resolved", {
210
+ decision_id: id,
211
+ tier: r.manifest!.tier,
212
+ stakes: r.manifest!.stakes,
213
+ confidence: opts.confidence ?? null,
214
+ });
215
+ emit.data(r.manifest);
216
+ });
217
+
218
+ // ── review ───────────────────────────────────────────────────────────────
219
+ root
220
+ .command("review <id>")
221
+ .description("Record a review verdict (calibration, not approval — work already proceeded).")
222
+ .requiredOption(
223
+ "--verdict <verdict>",
224
+ "ratified | overridden | wrong-tier-high | wrong-tier-low",
225
+ )
226
+ .option("--note <text>", "Reviewer note")
227
+ .action((id: string, opts: { verdict: string; note?: string }) => {
228
+ const coordRoot = coordRootOrExit();
229
+ if (!isVerdict(opts.verdict)) {
230
+ return fail(
231
+ "bad_verdict",
232
+ "verdict must be: ratified | overridden | wrong-tier-high | wrong-tier-low",
233
+ );
234
+ }
235
+ const r = reviewDecision(coordRoot, id, {
236
+ verdict: opts.verdict as ReviewVerdict,
237
+ note: opts.note,
238
+ });
239
+ if (!r.ok) return fail("review_failed", r.reason);
240
+ emitDecisionEvent("decision.reviewed", {
241
+ decision_id: id,
242
+ verdict: opts.verdict,
243
+ tier: r.manifest!.tier,
244
+ });
245
+ emit.data(r.manifest);
246
+ });
247
+
248
+ // ── triage ─────────────────────────────────────────────────────────────────
249
+ // Re-set tier/stakes on an already-filed decision (e.g. after a sweeper or
250
+ // reviewer flags a wrong-tier). Cheap enough to surface in phase 1.
251
+ root
252
+ .command("triage <id>")
253
+ .description("Adjust an already-filed decision's tier/stakes.")
254
+ .option("--tier <0|1|2>", "New tier")
255
+ .option("--stakes <small|medium|high>", "New stakes")
256
+ .action((id: string, opts: { tier?: string; stakes?: string }) => {
257
+ const coordRoot = coordRootOrExit();
258
+ if (opts.tier === undefined && opts.stakes === undefined) {
259
+ return fail("nothing_to_do", "pass --tier and/or --stakes");
260
+ }
261
+ const r = triageDecision(coordRoot, id, {
262
+ tier: opts.tier !== undefined ? parseTier(opts.tier) : undefined,
263
+ stakes: opts.stakes !== undefined ? parseStakes(opts.stakes) : undefined,
264
+ });
265
+ if (!r.ok) return fail("triage_failed", r.reason);
266
+ emit.data(r.manifest);
267
+ });
268
+
269
+ // ── archive ─────────────────────────────────────────────────────────────────
270
+ // The graduation exit: a reviewed decision's output has landed in a canonical
271
+ // home (an ADR, AGENTS.md, a code change), so the decision closes and moves to
272
+ // the archive, still searchable as precedent. `--graduated-to` records where.
273
+ root
274
+ .command("archive <id>")
275
+ .description("Archive a reviewed decision (terminal). Record where its output graduated.")
276
+ .option("--graduated-to <ref>", "Where the resolved output landed (e.g. docs/decisions.md#foo)")
277
+ .action((id: string, opts: { graduatedTo?: string }) => {
278
+ const coordRoot = coordRootOrExit();
279
+ const r = archiveDecision(coordRoot, id, opts.graduatedTo);
280
+ if (!r.ok) return fail("archive_failed", r.reason);
281
+ emitDecisionEvent("decision.archived", {
282
+ decision_id: id,
283
+ tier: r.manifest!.tier,
284
+ graduated_to: opts.graduatedTo ?? null,
285
+ });
286
+ emit.data(r.manifest);
287
+ });
288
+
289
+ // ── reopen ───────────────────────────────────────────────────────────────────
290
+ // The inverse of archive: pull an archived decision back to `reviewed`. The one
291
+ // sanctioned way out of the terminal archive (e.g. a fat-fingered graduated_to
292
+ // or the wrong decision archived). Clears graduated_to; a re-archive sets it fresh.
293
+ root
294
+ .command("reopen <id>")
295
+ .alias("unarchive")
296
+ .description("Reopen an archived decision back to reviewed (the inverse of archive).")
297
+ .action((id: string) => {
298
+ const coordRoot = coordRootOrExit();
299
+ const r = reopenDecision(coordRoot, id);
300
+ if (!r.ok) return fail("reopen_failed", r.reason);
301
+ emitDecisionEvent("decision.reopened", { decision_id: id, tier: r.manifest!.tier });
302
+ emit.data(r.manifest);
303
+ });
304
+
305
+ // ── supersede ────────────────────────────────────────────────────────────────
306
+ root
307
+ .command("supersede <id>")
308
+ .description("Mark a decision superseded by a newer one (terminal).")
309
+ .option("--by <id>", "The superseding decision's id")
310
+ .action((id: string, opts: { by?: string }) => {
311
+ const coordRoot = coordRootOrExit();
312
+ const r = supersedeDecision(coordRoot, id, opts.by);
313
+ if (!r.ok) return fail("supersede_failed", r.reason);
314
+ emitDecisionEvent("decision.superseded", {
315
+ decision_id: id,
316
+ superseded_by: opts.by ?? null,
317
+ });
318
+ emit.data(r.manifest);
319
+ });
320
+
321
+ // ── wontfix ──────────────────────────────────────────────────────────────────
322
+ root
323
+ .command("wontfix <id>")
324
+ .description("Close an un-deliberated decision without action (terminal).")
325
+ .option("--reason <text>", "Why it's being closed")
326
+ .action((id: string, opts: { reason?: string }) => {
327
+ const coordRoot = coordRootOrExit();
328
+ const r = wontfixDecision(coordRoot, id, opts.reason);
329
+ if (!r.ok) return fail("wontfix_failed", r.reason);
330
+ emitDecisionEvent("decision.wontfix", {
331
+ decision_id: id,
332
+ reason: opts.reason ?? null,
333
+ });
334
+ emit.data(r.manifest);
335
+ });
336
+ }
337
+
338
+ // ─── option types ────────────────────────────────────────────────────────────
339
+
340
+ interface FileOpts {
341
+ tier: string;
342
+ stakes: string;
343
+ context?: string;
344
+ defaultTaken?: string;
345
+ brief?: string;
346
+ filedBy?: string;
347
+ }
348
+ interface ListOpts {
349
+ status?: string;
350
+ tier?: string;
351
+ stakes?: string;
352
+ open?: boolean;
353
+ archived?: boolean;
354
+ }
355
+ interface ResolveOpts {
356
+ recommendation: string;
357
+ evidence: string[];
358
+ confidence?: string;
359
+ reversalCost?: string;
360
+ wrongIf?: string;
361
+ revisitWhen?: string;
362
+ resolvedBy?: string;
363
+ }
364
+
365
+ // ─── helpers ───────────────────────────────────────────────────────────────────
366
+
367
+ function coordRootOrExit(): string {
368
+ const root = monorepoRoot();
369
+ if (!root) {
370
+ emit.error({
371
+ code: "no_coord_root",
372
+ message: "not in a coord-aware repo (no .harnery/ found)",
373
+ });
374
+ process.exit(1);
375
+ }
376
+ return root;
377
+ }
378
+
379
+ function fail(code: string, message?: string): never {
380
+ emit.error({ code, message: message ?? code });
381
+ process.exit(1);
382
+ }
383
+
384
+ function parseTier(raw: string): DecisionTier {
385
+ const n = Number.parseInt(raw, 10);
386
+ if (!isTier(n)) fail("bad_tier", `tier must be 0, 1, or 2 (got "${raw}")`);
387
+ return n as DecisionTier;
388
+ }
389
+
390
+ function parseStakes(raw: string): DecisionStakes {
391
+ if (!isStakes(raw))
392
+ fail("bad_stakes", `stakes must be ${DECISION_STAKES.join(" | ")} (got "${raw}")`);
393
+ return raw as DecisionStakes;
394
+ }
395
+
396
+ function parseStatus(raw: string): DecisionStatus {
397
+ if (!isStatus(raw)) fail("bad_status", `unknown status "${raw}"`);
398
+ return raw as DecisionStatus;
399
+ }
400
+
401
+ function collect(value: string, previous: string[]): string[] {
402
+ return previous.concat([value]);
403
+ }
404
+
405
+ /** Compact row for list/search output — the full manifest is available via `show`. */
406
+ function summarize(m: DecisionManifest): Record<string, unknown> {
407
+ return {
408
+ decision_id: m.decision_id,
409
+ status: m.status,
410
+ tier: m.tier,
411
+ stakes: m.stakes,
412
+ question: m.question,
413
+ filed_by: m.filed_by ?? null,
414
+ filed_at: m.filed_at,
415
+ claimed_by: m.claimed_by ?? null,
416
+ resolved: !!m.resolution,
417
+ reviewed: !!m.review,
418
+ graduated_to: m.graduated_to ?? null,
419
+ };
420
+ }
421
+
422
+ /**
423
+ * Emit a canonical `decision.*` event. Soft: no-ops when there's no agent
424
+ * session to attribute it to (operator-side filing). Powers the docket metrics
425
+ * without new telemetry plumbing.
426
+ */
427
+ function emitDecisionEvent(type: string, data: Record<string, unknown>): void {
428
+ const owner = resolveOwner();
429
+ if (!owner) return;
430
+ const hb = readHeartbeat(owner);
431
+ emitCanonical({
432
+ type,
433
+ owner,
434
+ session: hb?.session_id ?? owner,
435
+ harness: normalizeHarness(hb?.platform),
436
+ data,
437
+ });
438
+ }
@@ -34,6 +34,7 @@ import type { Command } from "commander";
34
34
  import type { EmitContext } from "../commander.ts";
35
35
  import { DEFAULT_BIN_NAME } from "../core/config.ts";
36
36
  import { HARNESS_SPECS, type HarnessId } from "../core/hooks/harness/events.ts";
37
+ import { removeInstructions } from "../lib/instructions/apply.ts";
37
38
  import { type SettingsFile, unwireHooks } from "./init.ts";
38
39
 
39
40
  interface DeinitOpts {
@@ -133,6 +134,10 @@ export function registerDeinitCommand(program: Command, emit: EmitContext, binNa
133
134
  }
134
135
  }
135
136
 
137
+ // ── 1b. agent-facing instructions block + skills ───────────────────────
138
+ const removed = removeInstructions(projectRoot, { harness, dryRun });
139
+ actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
140
+
136
141
  // ── 2. coord root (opt-in; destructive) ────────────────────────────────
137
142
  if (purgeState) {
138
143
  if (!coordExists) {