safeword 0.67.0 → 0.68.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 (60) hide show
  1. package/dist/boundary-SBKG6B55.js +678 -0
  2. package/dist/boundary-SBKG6B55.js.map +1 -0
  3. package/dist/{check-IARKNMMO.js → check-U65IHMZM.js} +6 -5
  4. package/dist/{check-IARKNMMO.js.map → check-U65IHMZM.js.map} +1 -1
  5. package/dist/{chunk-ILDGIT74.js → chunk-4YWR3HPC.js} +2 -2
  6. package/dist/{chunk-ZPH6ECOQ.js → chunk-C3YBWOJF.js} +48 -32
  7. package/dist/chunk-C3YBWOJF.js.map +1 -0
  8. package/dist/chunk-CTHKK2XP.js +298 -0
  9. package/dist/chunk-CTHKK2XP.js.map +1 -0
  10. package/dist/{chunk-MZZLPZDQ.js → chunk-CWPBPI6A.js} +134 -33
  11. package/dist/chunk-CWPBPI6A.js.map +1 -0
  12. package/dist/{chunk-JQZCESMU.js → chunk-PRHPWVWN.js} +2 -2
  13. package/dist/{chunk-JQZCESMU.js.map → chunk-PRHPWVWN.js.map} +1 -1
  14. package/dist/{chunk-VDHXT7SV.js → chunk-RZQATAGI.js} +31 -158
  15. package/dist/chunk-RZQATAGI.js.map +1 -0
  16. package/dist/{chunk-I2C5SGFB.js → chunk-UVBYTXHG.js} +91 -9
  17. package/dist/chunk-UVBYTXHG.js.map +1 -0
  18. package/dist/cli.js +17 -11
  19. package/dist/cli.js.map +1 -1
  20. package/dist/{codify-QC6UXHI5.js → codify-YCMAMAJH.js} +5 -5
  21. package/dist/codify-YCMAMAJH.js.map +1 -0
  22. package/dist/{diff-452OHB47.js → diff-2KJUUMQ4.js} +2 -2
  23. package/dist/index.js +1 -1
  24. package/dist/{lint-gherkin-KFWEX4LL.js → lint-gherkin-OYFN6ZA3.js} +2 -2
  25. package/dist/presets/typescript/index.js +1 -1
  26. package/dist/{reset-2N55QL4F.js → reset-IP64AEPP.js} +3 -3
  27. package/dist/{reset-2N55QL4F.js.map → reset-IP64AEPP.js.map} +1 -1
  28. package/dist/{retro-7MVICR6B.js → retro-5NCR3MBL.js} +14 -10
  29. package/dist/retro-5NCR3MBL.js.map +1 -0
  30. package/dist/{setup-7SYJNJKR.js → setup-BIJAPRGL.js} +12 -12
  31. package/dist/setup-BIJAPRGL.js.map +1 -0
  32. package/dist/{sync-tracker-3S3HZMRX.js → sync-tracker-5HKABUN4.js} +7 -7
  33. package/dist/{upgrade-KEEDWKDK.js → upgrade-POITHI4A.js} +17 -10
  34. package/dist/upgrade-POITHI4A.js.map +1 -0
  35. package/package.json +3 -3
  36. package/templates/agents/safeword-retro-filer.md +6 -3
  37. package/templates/agents/safeword-retro-filer.toml +6 -3
  38. package/templates/commands/cleanup-zombies.md +7 -6
  39. package/templates/guides/self-report-filing.md +7 -4
  40. package/templates/guides/zombie-process-cleanup.md +11 -11
  41. package/templates/hooks/lib/done-gate.ts +9 -1
  42. package/templates/hooks/lib/lint.ts +49 -25
  43. package/templates/hooks/lib/phase-provenance.ts +2 -2
  44. package/templates/hooks/lib/retro-draft-spool.ts +46 -12
  45. package/templates/hooks/pre-tool-quality.ts +1 -1
  46. package/templates/scripts/cleanup-zombies.sh +31 -15
  47. package/templates/skills/cleanup-zombies/SKILL.md +7 -6
  48. package/templates/skills/retro/SKILL.md +3 -0
  49. package/dist/chunk-I2C5SGFB.js.map +0 -1
  50. package/dist/chunk-MZZLPZDQ.js.map +0 -1
  51. package/dist/chunk-VDHXT7SV.js.map +0 -1
  52. package/dist/chunk-ZPH6ECOQ.js.map +0 -1
  53. package/dist/codify-QC6UXHI5.js.map +0 -1
  54. package/dist/retro-7MVICR6B.js.map +0 -1
  55. package/dist/setup-7SYJNJKR.js.map +0 -1
  56. package/dist/upgrade-KEEDWKDK.js.map +0 -1
  57. /package/dist/{chunk-ILDGIT74.js.map → chunk-4YWR3HPC.js.map} +0 -0
  58. /package/dist/{diff-452OHB47.js.map → diff-2KJUUMQ4.js.map} +0 -0
  59. /package/dist/{lint-gherkin-KFWEX4LL.js.map → lint-gherkin-OYFN6ZA3.js.map} +0 -0
  60. /package/dist/{sync-tracker-3S3HZMRX.js.map → sync-tracker-5HKABUN4.js.map} +0 -0
@@ -0,0 +1,678 @@
1
+ import {
2
+ classifyAnnotation,
3
+ detectUnanchoredPhaseTransition,
4
+ evaluateTicketWrite,
5
+ frontmatterOf,
6
+ isValidSha,
7
+ isValidSkipReason,
8
+ parseCheckboxAnnotation,
9
+ scalar
10
+ } from "./chunk-CTHKK2XP.js";
11
+ import {
12
+ resolveTicketsDirectory
13
+ } from "./chunk-CYICTM6W.js";
14
+ import {
15
+ readFileSafe
16
+ } from "./chunk-X26EJNPZ.js";
17
+ import {
18
+ warn
19
+ } from "./chunk-O2ULQPXW.js";
20
+
21
+ // src/commands/boundary.ts
22
+ import { execFileSync as execFileSync2 } from "child_process";
23
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
24
+ import nodePath5 from "path";
25
+ import process2 from "process";
26
+
27
+ // templates/hooks/lib/ledger-git.ts
28
+ import { execFileSync } from "child_process";
29
+ function git(cwd, args, input) {
30
+ return execFileSync("git", args, {
31
+ cwd,
32
+ input,
33
+ encoding: "utf8",
34
+ stdio: input === void 0 ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"]
35
+ }).trim();
36
+ }
37
+ function resolveCommitSha(cwd, sha) {
38
+ try {
39
+ return git(cwd, ["rev-parse", "--verify", `${sha}^{commit}`]);
40
+ } catch {
41
+ return void 0;
42
+ }
43
+ }
44
+ function isAncestorOfHead(cwd, sha) {
45
+ try {
46
+ execFileSync("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
47
+ cwd,
48
+ stdio: "pipe"
49
+ });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ function patchIdForCommit(cwd, sha) {
56
+ try {
57
+ const patch = git(cwd, ["show", "--format=format:", "--patch", "--no-ext-diff", sha]);
58
+ const output = git(cwd, ["patch-id", "--stable"], patch);
59
+ return output.split(/\s+/)[0] || void 0;
60
+ } catch {
61
+ return void 0;
62
+ }
63
+ }
64
+ function allReachableCommits(cwd) {
65
+ try {
66
+ const output = git(cwd, ["rev-list", "--no-merges", "HEAD"]);
67
+ return output === "" ? [] : output.split("\n").filter(Boolean);
68
+ } catch {
69
+ return [];
70
+ }
71
+ }
72
+ function buildReachablePatchMap(cwd) {
73
+ const patchIdToCommits = /* @__PURE__ */ new Map();
74
+ for (const commit of allReachableCommits(cwd)) {
75
+ const patchId = patchIdForCommit(cwd, commit);
76
+ if (!patchId) continue;
77
+ const commits = patchIdToCommits.get(patchId) ?? [];
78
+ commits.push(commit);
79
+ patchIdToCommits.set(patchId, commits);
80
+ }
81
+ return patchIdToCommits;
82
+ }
83
+ function createLedgerShaResolver(cwd) {
84
+ let reachablePatchMap;
85
+ function patchMap() {
86
+ reachablePatchMap ??= buildReachablePatchMap(cwd);
87
+ return reachablePatchMap;
88
+ }
89
+ return (sha) => {
90
+ const fullSha = resolveCommitSha(cwd, sha);
91
+ if (!fullSha) return false;
92
+ if (isAncestorOfHead(cwd, fullSha)) return fullSha;
93
+ const stalePatchId = patchIdForCommit(cwd, fullSha);
94
+ if (!stalePatchId) return false;
95
+ const candidates = patchMap().get(stalePatchId) ?? [];
96
+ const [candidate] = candidates;
97
+ return candidates.length === 1 && candidate ? candidate : false;
98
+ };
99
+ }
100
+
101
+ // templates/hooks/lib/done-gate.ts
102
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
103
+ import nodePath4 from "path";
104
+
105
+ // templates/hooks/lib/dependency-readiness.ts
106
+ import { createHash } from "crypto";
107
+ import {
108
+ existsSync as existsSync2,
109
+ mkdirSync,
110
+ readdirSync,
111
+ readFileSync as readFileSync2,
112
+ statSync as statSync2,
113
+ writeFileSync
114
+ } from "fs";
115
+ import nodePath2 from "path";
116
+
117
+ // templates/hooks/lib/namespace-root.ts
118
+ import { existsSync, readFileSync, statSync } from "fs";
119
+ import nodePath from "path";
120
+
121
+ // templates/hooks/lib/test-runner.ts
122
+ import { execSync, spawnSync } from "child_process";
123
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
124
+ import nodePath3 from "path";
125
+ var projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
126
+
127
+ // templates/hooks/lib/done-gate.ts
128
+ var PR_SCOPE_LINE_PATTERN = /^\*\*PR Scope:\*\*\s*(?<status>.+)$/im;
129
+ function checkVerifyArtifact(content) {
130
+ const prScopeMatch = PR_SCOPE_LINE_PATTERN.exec(content);
131
+ if (!prScopeMatch?.groups?.status) {
132
+ return {
133
+ ok: false,
134
+ reason: "verify.md is missing PR scope evidence. Run /verify again so it records `**PR Scope:**` before marking done."
135
+ };
136
+ }
137
+ const status = prScopeMatch.groups.status;
138
+ const failed = status.includes("\u274C") || !status.includes("\u2705") && /piggybacked changes/i.test(status);
139
+ if (failed) {
140
+ return {
141
+ ok: false,
142
+ reason: "verify.md says PR scope failed. Revert or split the piggybacked work, or explicitly accept the scope change in the ticket artifacts before marking done."
143
+ };
144
+ }
145
+ return { ok: true };
146
+ }
147
+
148
+ // templates/hooks/lib/impl-plan.ts
149
+ var IMPL_PLAN_SECTIONS = [
150
+ "Approach",
151
+ "Decisions",
152
+ "Arch alignment",
153
+ "Known deviations",
154
+ "Assessment triggers"
155
+ ];
156
+ var STATUS_PREFIX = "**Status:**";
157
+ var SKIP_PREFIX = "skip:";
158
+ var SECTION_NAMES = new Map(
159
+ IMPL_PLAN_SECTIONS.map((name) => [name.toLowerCase(), name])
160
+ );
161
+ function activeLines(content) {
162
+ const lines = [];
163
+ let inComment = false;
164
+ for (const raw of content.split("\n")) {
165
+ let line = raw;
166
+ if (inComment) {
167
+ const end = line.indexOf("-->");
168
+ if (end === -1) continue;
169
+ inComment = false;
170
+ line = line.slice(end + 3);
171
+ }
172
+ let start = line.indexOf("<!--");
173
+ while (start !== -1) {
174
+ const end = line.indexOf("-->", start + 4);
175
+ if (end === -1) {
176
+ line = line.slice(0, start);
177
+ inComment = true;
178
+ break;
179
+ }
180
+ line = line.slice(0, start) + line.slice(end + 3);
181
+ start = line.indexOf("<!--");
182
+ }
183
+ lines.push(line);
184
+ }
185
+ return lines;
186
+ }
187
+ function parseStatus(lines, errors) {
188
+ for (const line of lines) {
189
+ const trimmed = line.trim();
190
+ if (!trimmed.startsWith(STATUS_PREFIX)) continue;
191
+ const value = trimmed.slice(STATUS_PREFIX.length).trim();
192
+ if (value === "planned" || value === "implemented") return value;
193
+ errors.push(`Unknown status "${value}" \u2014 allowed values: planned, implemented.`);
194
+ return null;
195
+ }
196
+ errors.push(`Missing \`${STATUS_PREFIX}\` line \u2014 add \`${STATUS_PREFIX} planned\` near the top.`);
197
+ return null;
198
+ }
199
+ function collectSectionBodies(lines) {
200
+ const bodies = /* @__PURE__ */ new Map();
201
+ let current = null;
202
+ for (const line of lines) {
203
+ const trimmed = line.trim();
204
+ if (trimmed.startsWith("## ")) {
205
+ current = SECTION_NAMES.get(trimmed.slice(3).trim().toLowerCase()) ?? null;
206
+ if (current && !bodies.has(current)) bodies.set(current, []);
207
+ continue;
208
+ }
209
+ if (trimmed.startsWith("# ")) {
210
+ current = null;
211
+ continue;
212
+ }
213
+ if (current && trimmed !== "") bodies.get(current)?.push(trimmed);
214
+ }
215
+ return bodies;
216
+ }
217
+ function parseImplPlan(content) {
218
+ const errors = [];
219
+ const lines = activeLines(content);
220
+ const status = parseStatus(lines, errors);
221
+ const bodies = collectSectionBodies(lines);
222
+ const sections = {};
223
+ for (const name of IMPL_PLAN_SECTIONS) {
224
+ const body = bodies.get(name);
225
+ if (body === void 0) {
226
+ errors.push(`Missing section heading \`## ${name}\` \u2014 all five sections are required.`);
227
+ continue;
228
+ }
229
+ const skipLine = body.length === 1 && body[0]?.toLowerCase().startsWith(SKIP_PREFIX) ? body[0] : null;
230
+ if (skipLine !== null) {
231
+ const reason = skipLine.slice(SKIP_PREFIX.length).trim();
232
+ if (reason === "") {
233
+ errors.push(`Section "${name}": skip requires a non-empty reason (\`skip: <why>\`).`);
234
+ }
235
+ sections[name] = { satisfied: reason !== "", skip: reason };
236
+ continue;
237
+ }
238
+ if (body.length === 0) {
239
+ errors.push(`Section "${name}" is empty \u2014 add content or \`skip: <why>\`.`);
240
+ }
241
+ sections[name] = { satisfied: body.length > 0, skip: null };
242
+ }
243
+ return { status, sections, errors };
244
+ }
245
+
246
+ // templates/hooks/lib/ledger-validation.ts
247
+ function parseLedger(content) {
248
+ const lines = content.split("\n");
249
+ const scenarios = [];
250
+ let current;
251
+ let crossScenario;
252
+ for (const line of lines) {
253
+ const scenarioMatch = /^#{2,3}\s+Scenario:\s*(.+)$/.exec(line);
254
+ if (scenarioMatch) {
255
+ current = { name: (scenarioMatch[1] ?? "").trim() };
256
+ scenarios.push(current);
257
+ continue;
258
+ }
259
+ const parsed = parseCheckboxAnnotation(line);
260
+ if (!parsed) continue;
261
+ if (parsed.step === "cross-scenario") {
262
+ crossScenario = parsed;
263
+ continue;
264
+ }
265
+ if (current) {
266
+ if (parsed.step === "RED") current.red = parsed;
267
+ else if (parsed.step === "GREEN") current.green = parsed;
268
+ else if (parsed.step === "REFACTOR") current.refactor = parsed;
269
+ }
270
+ }
271
+ return { scenarios, crossScenario };
272
+ }
273
+ function checkSha(value, resolveSha, label) {
274
+ if (!isValidSha(value)) {
275
+ return {
276
+ error: `${label}: "${value}" is not a valid commit SHA (expected 7-40 hex chars).`,
277
+ format: true
278
+ };
279
+ }
280
+ const resolution = resolveSha(value);
281
+ if (resolution === false) {
282
+ return { error: `${label}: SHA ${value} is not reachable from HEAD.`, format: false };
283
+ }
284
+ return {
285
+ format: false,
286
+ canonicalSha: resolution === true ? value : resolution
287
+ };
288
+ }
289
+ function validateScenario(scenario, resolveSha, errors) {
290
+ const steps = [
291
+ { name: "RED", box: scenario.red },
292
+ { name: "GREEN", box: scenario.green },
293
+ { name: "REFACTOR", box: scenario.refactor }
294
+ ];
295
+ const annotatedSteps = steps.filter((s) => s.box && s.box.annotation !== "");
296
+ if (annotatedSteps.length === 0) return;
297
+ const shaToStep = /* @__PURE__ */ new Map();
298
+ let realShaCount = 0;
299
+ for (const step of steps) {
300
+ if (!step.box) continue;
301
+ const kind = classifyAnnotation(step.box.annotation);
302
+ if (kind.kind === "none") continue;
303
+ if (kind.kind === "skip") {
304
+ if (!isValidSkipReason(kind.reason)) {
305
+ errors.push(
306
+ `Scenario "${scenario.name}" ${step.name}: skip reason is empty or whitespace-only. Provide a real reason.`
307
+ );
308
+ }
309
+ continue;
310
+ }
311
+ realShaCount++;
312
+ const checkedSha = checkSha(kind.value, resolveSha, `Scenario "${scenario.name}" ${step.name}`);
313
+ if (checkedSha.error) {
314
+ errors.push(checkedSha.error);
315
+ if (checkedSha.format) continue;
316
+ }
317
+ if (!checkedSha.canonicalSha) continue;
318
+ const existingStep = shaToStep.get(checkedSha.canonicalSha);
319
+ if (existingStep) {
320
+ errors.push(
321
+ `Scenario "${scenario.name}" SHA collision: ${existingStep} and ${step.name} share SHA ${checkedSha.canonicalSha}. Each TDD step must correspond to a distinct commit.`
322
+ );
323
+ } else {
324
+ shaToStep.set(checkedSha.canonicalSha, step.name);
325
+ }
326
+ }
327
+ if (realShaCount === 0) {
328
+ errors.push(
329
+ `Scenario "${scenario.name}" represents work that produced no commits \u2014 all steps are skipped. At least one step must carry a real SHA.`
330
+ );
331
+ }
332
+ }
333
+ function validateCrossScenario(cs, resolveSha, errors) {
334
+ if (!cs) {
335
+ errors.push(
336
+ "Cross-scenario refactor row is missing. Add `- [ ] cross-scenario` to test-definitions.md and complete it with a SHA or `skip: <reason>` before done."
337
+ );
338
+ return;
339
+ }
340
+ if (!cs.checked) {
341
+ errors.push(
342
+ "Cross-scenario refactor row is unchecked. Mark it with a SHA or `skip: <reason>` before done."
343
+ );
344
+ return;
345
+ }
346
+ if (cs.annotation === "") return;
347
+ const kind = classifyAnnotation(cs.annotation);
348
+ if (kind.kind === "skip") {
349
+ if (!isValidSkipReason(kind.reason)) {
350
+ errors.push(
351
+ "Cross-scenario refactor row: skip reason is empty or whitespace-only. Provide a real reason."
352
+ );
353
+ }
354
+ return;
355
+ }
356
+ if (kind.kind === "sha") {
357
+ const checkedSha = checkSha(kind.value, resolveSha, "Cross-scenario refactor row");
358
+ if (checkedSha.error) errors.push(checkedSha.error);
359
+ }
360
+ }
361
+ function wholeTicketPassFromScenarios(scenarios) {
362
+ const hasAnyAnnotation = scenarios.some(
363
+ (s) => [s.red, s.green, s.refactor].some((box) => box?.annotation)
364
+ );
365
+ return scenarios.length >= 2 && hasAnyAnnotation;
366
+ }
367
+ function validateLedger(content, resolveSha) {
368
+ const errors = [];
369
+ const { scenarios, crossScenario } = parseLedger(content);
370
+ for (const scenario of scenarios) {
371
+ validateScenario(scenario, resolveSha, errors);
372
+ }
373
+ if (wholeTicketPassFromScenarios(scenarios) || crossScenario) {
374
+ validateCrossScenario(crossScenario, resolveSha, errors);
375
+ }
376
+ return { ok: errors.length === 0, errors };
377
+ }
378
+
379
+ // src/boundary/engine.ts
380
+ var TICKET_ARTIFACTS = /* @__PURE__ */ new Set([
381
+ "ticket.md",
382
+ "test-definitions.md",
383
+ "verify.md",
384
+ "impl-plan.md",
385
+ "spec.md"
386
+ ]);
387
+ var LEDGER_REQUIRED_PHASES = /* @__PURE__ */ new Set(["scenario-gate", "implement", "verify", "done"]);
388
+ function frontmatterOf2(content) {
389
+ return frontmatterOf(content.replaceAll("\r\n", "\n"));
390
+ }
391
+ function hasEntries(meta, key) {
392
+ const value = meta[key];
393
+ return Array.isArray(value) ? value.length > 0 : typeof value === "string" && value !== "";
394
+ }
395
+ function pass(check) {
396
+ return { check, verdict: "pass" };
397
+ }
398
+ function warnVerdict(check, detail) {
399
+ return { check, verdict: "warn", detail };
400
+ }
401
+ var UNREACHABLE_CAUSES = "possible causes: a forged anchor, or a shallow clone missing the history (fetch full history to verify)";
402
+ function legalityVerdict(ticketFile, legalitySteps) {
403
+ let steps = [];
404
+ if (legalitySteps && legalitySteps.length > 0) {
405
+ steps = legalitySteps;
406
+ } else if (ticketFile.proposed !== void 0) {
407
+ steps = [{ prior: ticketFile.prior, proposed: ticketFile.proposed }];
408
+ }
409
+ for (const step of steps) {
410
+ const verdict = evaluateTicketWrite(step.prior, step.proposed);
411
+ if (!verdict.ok) {
412
+ const at = step.commit === void 0 ? "" : ` (commit ${step.commit})`;
413
+ return warnVerdict("phase-legality", `${verdict.reason}${at}`);
414
+ }
415
+ }
416
+ return pass("phase-legality");
417
+ }
418
+ function ticketFileChecks(ticketFile, resolveSha, legalitySteps) {
419
+ if (ticketFile.proposed === void 0) return [];
420
+ const checks = [];
421
+ if (frontmatterOf2(ticketFile.proposed) === void 0) {
422
+ checks.push(
423
+ warnVerdict(
424
+ "classify",
425
+ "ticket.md cannot be classified \u2014 its frontmatter is missing or unparseable"
426
+ )
427
+ );
428
+ }
429
+ checks.push(legalityVerdict(ticketFile, legalitySteps));
430
+ try {
431
+ const anchor = detectUnanchoredPhaseTransition(
432
+ ticketFile.prior,
433
+ ticketFile.proposed,
434
+ resolveSha
435
+ );
436
+ if (anchor.kind === "unanchored") {
437
+ const causes = /not reachable/i.test(anchor.reason) ? ` \u2014 ${UNREACHABLE_CAUSES}` : "";
438
+ checks.push(warnVerdict("phase-anchor", `${anchor.reason}${causes}`));
439
+ } else {
440
+ checks.push(pass("phase-anchor"));
441
+ }
442
+ } catch {
443
+ checks.push({
444
+ check: "phase-anchor",
445
+ verdict: "indeterminate",
446
+ detail: "SHA resolution failed mid-run \u2014 reachability could not be determined"
447
+ });
448
+ }
449
+ return checks;
450
+ }
451
+ function atRestBirthCheck(change) {
452
+ if (change.artifacts.some((a) => a.artifact === "ticket.md")) return [];
453
+ if (change.ticketCurrent === void 0) return [];
454
+ const meta = frontmatterOf2(change.ticketCurrent);
455
+ if (meta === void 0) return [];
456
+ if (scalar(meta, "type") !== "feature") return [];
457
+ const phase = scalar(meta, "phase");
458
+ if (phase === void 0 || phase === "intake") return [];
459
+ if (hasEntries(meta, "phase_anchors")) return [pass("birth")];
460
+ const birth = evaluateTicketWrite(void 0, change.ticketCurrent);
461
+ return birth.ok ? [pass("birth")] : [warnVerdict("birth", `ticket at rest looks born past intake: ${birth.reason}`)];
462
+ }
463
+ function ledgerRequiredPhase(change) {
464
+ if (change.ticketCurrent === void 0) return void 0;
465
+ const meta = frontmatterOf2(change.ticketCurrent);
466
+ if (meta === void 0 || scalar(meta, "type") !== "feature") return void 0;
467
+ const phase = scalar(meta, "phase");
468
+ return phase !== void 0 && LEDGER_REQUIRED_PHASES.has(phase) ? phase : void 0;
469
+ }
470
+ function ledgerChecks(change, resolveSha) {
471
+ const checks = [];
472
+ const requiredAt = ledgerRequiredPhase(change);
473
+ if (requiredAt !== void 0 && !change.hasLedger) {
474
+ checks.push(
475
+ warnVerdict(
476
+ "ledger",
477
+ `feature at phase ${requiredAt} has no test-definitions.md \u2014 the R/G/R ledger is missing`
478
+ )
479
+ );
480
+ }
481
+ const ledgerFile = change.artifacts.find((a) => a.artifact === "test-definitions.md");
482
+ if (ledgerFile?.proposed !== void 0) {
483
+ try {
484
+ const result = validateLedger(ledgerFile.proposed, resolveSha ?? (() => true));
485
+ checks.push(
486
+ result.ok ? pass("ledger-format") : warnVerdict(
487
+ "ledger-format",
488
+ `ledger annotation problems: ${result.errors.join(" | ")}`
489
+ )
490
+ );
491
+ } catch {
492
+ checks.push({
493
+ check: "ledger-format",
494
+ verdict: "indeterminate",
495
+ detail: "SHA resolution failed mid-run \u2014 ledger reachability could not be determined"
496
+ });
497
+ }
498
+ }
499
+ return checks;
500
+ }
501
+ function artifactShapeChecks(change) {
502
+ const checks = [];
503
+ const verifyFile = change.artifacts.find((a) => a.artifact === "verify.md");
504
+ if (verifyFile?.proposed !== void 0) {
505
+ const verdict = checkVerifyArtifact(verifyFile.proposed);
506
+ checks.push(
507
+ verdict.ok ? pass("verify-shape") : warnVerdict("verify-shape", `verify.md fails its shape check: ${verdict.reason}`)
508
+ );
509
+ }
510
+ const planFile = change.artifacts.find((a) => a.artifact === "impl-plan.md");
511
+ if (planFile?.proposed !== void 0) {
512
+ const parsed = parseImplPlan(planFile.proposed);
513
+ checks.push(
514
+ parsed.errors.length === 0 ? pass("impl-plan-shape") : warnVerdict(
515
+ "impl-plan-shape",
516
+ `impl-plan.md fails its shape check: ${parsed.errors.join(" | ")}`
517
+ )
518
+ );
519
+ }
520
+ return checks;
521
+ }
522
+ function reconcileTicket(change, resolveSha) {
523
+ const ticketFile = change.artifacts.find((a) => a.artifact === "ticket.md");
524
+ return [
525
+ ...ticketFile ? ticketFileChecks(ticketFile, resolveSha, change.legalitySteps) : [],
526
+ ...atRestBirthCheck(change),
527
+ ...ledgerChecks(change, resolveSha),
528
+ ...artifactShapeChecks(change)
529
+ ];
530
+ }
531
+ function reconcileChange(changes, resolveSha) {
532
+ return changes.map((change) => ({
533
+ ticket: change.ticketFolder,
534
+ checks: reconcileTicket(change, resolveSha)
535
+ }));
536
+ }
537
+ function findings(reconciliations) {
538
+ return reconciliations.flatMap(
539
+ ({ ticket, checks }) => checks.filter((c) => c.verdict !== "pass").map((check) => ({ ticket, check }))
540
+ );
541
+ }
542
+
543
+ // src/commands/boundary.ts
544
+ var AUDIT_RELATIVE_PATH = nodePath5.join(".safeword", "boundary-audit.jsonl");
545
+ function tryGit(cwd, args) {
546
+ try {
547
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
548
+ } catch {
549
+ return void 0;
550
+ }
551
+ }
552
+ function splitLines(listing) {
553
+ return listing?.split("\n").filter((line) => line !== "");
554
+ }
555
+ function pushRange(cwd) {
556
+ const upstreamPaths = splitLines(tryGit(cwd, ["diff", "--name-only", "@{u}...HEAD"]));
557
+ if (upstreamPaths !== void 0) {
558
+ const base2 = tryGit(cwd, ["merge-base", "@{u}", "HEAD"])?.trim();
559
+ return { paths: upstreamPaths, priorRef: base2 ?? "@{u}" };
560
+ }
561
+ const unpushed = splitLines(tryGit(cwd, ["rev-list", "HEAD", "--not", "--remotes"]));
562
+ if (unpushed === void 0 || unpushed.length === 0) return void 0;
563
+ const oldest = unpushed.at(-1) ?? "";
564
+ const base = tryGit(cwd, ["rev-parse", `${oldest}~1`])?.trim();
565
+ const listing = tryGit(
566
+ cwd,
567
+ base === void 0 ? ["log", "--name-only", "--format=", "HEAD"] : ["diff", "--name-only", `${base}..HEAD`]
568
+ );
569
+ const paths = splitLines(listing);
570
+ return paths === void 0 ? void 0 : { paths: [...new Set(paths)], priorRef: base };
571
+ }
572
+ function boundaryRange(cwd, at) {
573
+ if (at === "push") return pushRange(cwd);
574
+ const staged = splitLines(
575
+ tryGit(cwd, ["diff", "--cached", "--name-only", "--diff-filter=ACMRD"])
576
+ );
577
+ return staged === void 0 ? void 0 : { paths: staged, priorRef: "HEAD" };
578
+ }
579
+ function contentAt(cwd, spec) {
580
+ return tryGit(cwd, ["show", spec]);
581
+ }
582
+ function readArtifact(cwd, path, basename, range, at) {
583
+ const proposedSpec = at === "commit" ? `:${path}` : `HEAD:${path}`;
584
+ return {
585
+ artifact: basename,
586
+ prior: range.priorRef === void 0 ? void 0 : contentAt(cwd, `${range.priorRef}:${path}`),
587
+ proposed: contentAt(cwd, proposedSpec)
588
+ };
589
+ }
590
+ function parseTicketPath(path, prefix) {
591
+ if (!path.startsWith(prefix)) return void 0;
592
+ const segments = path.slice(prefix.length).split("/");
593
+ const ticketFolder = segments[0] ?? "";
594
+ const basename = segments.slice(1).join("/");
595
+ if (ticketFolder === "" || !TICKET_ARTIFACTS.has(basename)) return void 0;
596
+ return { ticketFolder, basename };
597
+ }
598
+ function collectChanges(cwd, range, at) {
599
+ const ticketsDirectory = nodePath5.relative(cwd, resolveTicketsDirectory(cwd));
600
+ const prefix = `${ticketsDirectory}/`;
601
+ const byTicket = /* @__PURE__ */ new Map();
602
+ for (const path of range.paths) {
603
+ const parsed = parseTicketPath(path, prefix);
604
+ if (parsed === void 0) continue;
605
+ const change = byTicket.get(parsed.ticketFolder) ?? {
606
+ ticketFolder: parsed.ticketFolder,
607
+ artifacts: [],
608
+ hasLedger: false
609
+ };
610
+ change.artifacts.push(readArtifact(cwd, path, parsed.basename, range, at));
611
+ byTicket.set(parsed.ticketFolder, change);
612
+ }
613
+ for (const change of byTicket.values()) {
614
+ const folder = nodePath5.join(cwd, ticketsDirectory, change.ticketFolder);
615
+ const staged = change.artifacts.find((a) => a.artifact === "ticket.md")?.proposed;
616
+ change.ticketCurrent = staged ?? readFileSafe(nodePath5.join(folder, "ticket.md"));
617
+ change.hasLedger = change.artifacts.some(
618
+ (a) => a.artifact === "test-definitions.md" && a.proposed !== void 0
619
+ ) || existsSync5(nodePath5.join(folder, "test-definitions.md"));
620
+ if (at === "push" && change.artifacts.some((a) => a.artifact === "ticket.md")) {
621
+ const path = `${prefix}${change.ticketFolder}/ticket.md`;
622
+ change.legalitySteps = legalityStepsFor(cwd, path, range.priorRef);
623
+ }
624
+ }
625
+ return byTicket.values().toArray();
626
+ }
627
+ function legalityStepsFor(cwd, path, priorReference) {
628
+ const range = priorReference === void 0 ? "HEAD" : `${priorReference}..HEAD`;
629
+ const commits = splitLines(tryGit(cwd, ["rev-list", "--reverse", range, "--", path])) ?? [];
630
+ return commits.map((commit) => ({
631
+ prior: contentAt(cwd, `${commit}~1:${path}`),
632
+ proposed: contentAt(cwd, `${commit}:${path}`) ?? "",
633
+ commit: commit.slice(0, 7)
634
+ }));
635
+ }
636
+ function appendAudit(cwd, entry) {
637
+ const auditPath = nodePath5.join(cwd, AUDIT_RELATIVE_PATH);
638
+ mkdirSync2(nodePath5.dirname(auditPath), { recursive: true });
639
+ appendFileSync(auditPath, `${JSON.stringify(entry)}
640
+ `);
641
+ }
642
+ function reconcileBoundary(cwd, at) {
643
+ const range = boundaryRange(cwd, at);
644
+ if (range === void 0 || range.paths.length === 0) return;
645
+ const changes = collectChanges(cwd, range, at);
646
+ if (changes.length === 0) return;
647
+ const resolveSha = at === "push" ? createLedgerShaResolver(cwd) : void 0;
648
+ const reconciliations = reconcileChange(changes, resolveSha);
649
+ for (const finding of findings(reconciliations)) {
650
+ warn(
651
+ `boundary(${at}) ${finding.ticket}: [${finding.check.check}] ${finding.check.detail ?? finding.check.verdict}`
652
+ );
653
+ }
654
+ appendAudit(cwd, {
655
+ boundary: at,
656
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
657
+ head: tryGit(cwd, ["rev-parse", "--short", "HEAD"])?.trim() ?? "unknown",
658
+ tickets: reconciliations
659
+ });
660
+ }
661
+ function boundary(options) {
662
+ try {
663
+ const at = options.at === "push" ? "push" : "commit";
664
+ const cwd = process2.cwd();
665
+ if (existsSync5(nodePath5.join(cwd, ".safeword"))) {
666
+ reconcileBoundary(cwd, at);
667
+ }
668
+ } catch (error) {
669
+ warn(
670
+ `boundary check could not complete: ${error instanceof Error ? error.message : String(error)}`
671
+ );
672
+ }
673
+ return Promise.resolve();
674
+ }
675
+ export {
676
+ boundary
677
+ };
678
+ //# sourceMappingURL=boundary-SBKG6B55.js.map