@gobing-ai/spur 0.3.55 → 0.3.57

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 (27) hide show
  1. package/.claude-plugin/marketplace.json +2 -3
  2. package/config/corpus-baseline.json +2521 -49
  3. package/config/rules/strict/runtime-boundaries.yaml +3 -0
  4. package/config/rules/surface/check-cli-surface.yaml +1 -0
  5. package/config/workflow-composition-baseline.json +241 -22
  6. package/package.json +1 -1
  7. package/plugins/sp/agents/expert-spur.md +3 -0
  8. package/plugins/sp/commands/dev-idea.md +7 -19
  9. package/plugins/sp/plugin.json +1 -1
  10. package/plugins/sp/scripts/task-size-precheck.ts +11 -6
  11. package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +4 -4
  12. package/plugins/sp/skills/dogfood-testing/references/report-template.md +4 -2
  13. package/plugins/sp/skills/issue-finding/SKILL.md +23 -11
  14. package/plugins/sp/skills/spur-cli/SKILL.md +23 -15
  15. package/plugins/sp/skills/spur-cli/references/agent.md +5 -0
  16. package/plugins/sp/skills/spur-cli/references/builder.md +49 -0
  17. package/plugins/sp/skills/spur-cli/references/features.md +6 -1
  18. package/plugins/sp/skills/spur-cli/references/message.md +5 -0
  19. package/plugins/sp/skills/spur-cli/references/rules.md +5 -0
  20. package/plugins/sp/skills/spur-cli/references/self.md +101 -0
  21. package/plugins/sp/skills/spur-cli/references/tasks.md +6 -1
  22. package/plugins/sp/skills/spur-cli/references/team.md +5 -0
  23. package/plugins/sp/skills/spur-cli/references/workflows.md +40 -0
  24. package/plugins/sp/skills/spur-dev/references/dev-operations.md +9 -8
  25. package/plugins/sp/skills/spur-dev/references/execution-batch.md +19 -0
  26. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +27 -5
  27. package/spur.js +1546 -314
package/spur.js CHANGED
@@ -37430,7 +37430,10 @@ var init_finding_codes = __esm(() => {
37430
37430
  "L2.missing-required-section",
37431
37431
  "L2.forbidden-section",
37432
37432
  "L2.disallowed-section",
37433
+ "L2.heading-level",
37434
+ "L2.section-order",
37433
37435
  "L3.requirements-format",
37436
+ "L3.requirements-checkbox",
37434
37437
  "L3.requirements-empty",
37435
37438
  "L3.ac-empty",
37436
37439
  "L3.ac-requirement-coverage",
@@ -37478,7 +37481,10 @@ var init_finding_codes = __esm(() => {
37478
37481
  L2_MISSING_REQUIRED_SECTION: "L2.missing-required-section",
37479
37482
  L2_FORBIDDEN_SECTION: "L2.forbidden-section",
37480
37483
  L2_DISALLOWED_SECTION: "L2.disallowed-section",
37484
+ L2_HEADING_LEVEL: "L2.heading-level",
37485
+ L2_SECTION_ORDER: "L2.section-order",
37481
37486
  L3_REQUIREMENTS_FORMAT: "L3.requirements-format",
37487
+ L3_REQUIREMENTS_CHECKBOX: "L3.requirements-checkbox",
37482
37488
  L3_REQUIREMENTS_EMPTY: "L3.requirements-empty",
37483
37489
  L3_AC_EMPTY: "L3.ac-empty",
37484
37490
  L3_AC_REQUIREMENT_COVERAGE: "L3.ac-requirement-coverage",
@@ -50615,6 +50621,10 @@ async function countCheckpointsBySource(db2, source) {
50615
50621
  const row = await db2.queryFirst("SELECT COUNT(*) AS cnt FROM history_import_checkpoint WHERE source = ?", source);
50616
50622
  return row?.cnt ?? 0;
50617
50623
  }
50624
+ async function countToolCallsSince(db2, source, runStartedAt) {
50625
+ const row = await db2.queryFirst("SELECT COUNT(*) AS cnt FROM history_tool_call WHERE source = ? AND imported_at >= ?", source, runStartedAt);
50626
+ return row?.cnt ?? 0;
50627
+ }
50618
50628
  function withStepPredicates(wmWhere, predicates) {
50619
50629
  return wmWhere === "" ? `WHERE ${predicates}` : `${wmWhere} AND ${predicates}`;
50620
50630
  }
@@ -51040,7 +51050,7 @@ function renderBySession(artifact) {
51040
51050
  }
51041
51051
  for (const s of artifact.bySession) {
51042
51052
  const tokens = (s.tokens / 1e6).toFixed(1);
51043
- lines.push(` ${s.sessionId.slice(0, 18).padEnd(18)} ` + `${s.source.slice(0, 8).padEnd(8)} ` + `${String(s.messages).padStart(5)} msg ${String(s.toolCalls).padStart(5)} calls ` + `${tokens.padStart(6)}M tok $${s.costUsd.toFixed(2).padStart(7)}` + (s.topTool ? ` top: ${s.topTool}` : ""));
51053
+ lines.push(` ${s.sessionId.slice(0, 18).padEnd(18)} ` + `${s.source.slice(0, 8).padEnd(8)} ` + `${(s.startedAt ?? "\u2026").slice(0, 10).padStart(10)} ` + `${String(s.messages).padStart(5)} msg ${String(s.toolCalls).padStart(5)} calls ` + `${tokens.padStart(6)}M tok $${s.costUsd.toFixed(2).padStart(7)}` + (s.topTool ? ` top: ${s.topTool}` : ""));
51044
51054
  }
51045
51055
  return lines;
51046
51056
  }
@@ -53764,6 +53774,9 @@ class TaskRunLinkDao {
53764
53774
  await this.db.run(`INSERT INTO task_run_links (id, wbs, run_id, kind, created_at)
53765
53775
  VALUES (?, ?, ?, ?, ?)`, [input.id, input.wbs, input.run_id, input.kind, input.created_at]);
53766
53776
  }
53777
+ async updateRunId(id, runId) {
53778
+ await this.db.run(`UPDATE task_run_links SET run_id = ?1 WHERE id = ?2`, runId, id);
53779
+ }
53767
53780
  async listByWbs(wbs, limit) {
53768
53781
  try {
53769
53782
  return await this.db.queryAll(`SELECT id, wbs, run_id, kind, created_at
@@ -62114,7 +62127,8 @@ async function applyCliMigrations(adapter, migrations = CLI_MIGRATIONS) {
62114
62127
  const argsRawSkip = migration.id === "0012_spur_cli_history_tool_call_args_raw" && !await tableExists(adapter, "history_tool_call");
62115
62128
  const callIdSkip = migration.id === "0015_spur_cli_history_tool_call_call_id" && !await tableExists(adapter, "history_tool_call");
62116
62129
  const nameOccurredIndexSkip = migration.id === "0014_spur_cli_system_events_name_occurred_idx" && !await tableExists(adapter, "system_events");
62117
- if (shouldApplySql && !sequenceIndexSkip && !argsRawSkip && !nameOccurredIndexSkip && !callIdSkip && !tsNullableSkip) {
62130
+ const runsStatusDoneSkip = migration.id === "0017_spur_cli_runs_status_completed_to_done" && (!await tableExists(adapter, "runs") || !await columnExists(adapter, "runs", "status"));
62131
+ if (shouldApplySql && !sequenceIndexSkip && !argsRawSkip && !runsStatusDoneSkip && !nameOccurredIndexSkip && !callIdSkip && !tsNullableSkip) {
62118
62132
  for (const statement of splitSqlStatements(migration.sql)) {
62119
62133
  await adapter.exec(statement);
62120
62134
  }
@@ -62369,6 +62383,10 @@ ${HISTORY_RUN_SESSION_SCHEMA_SQL}
62369
62383
  {
62370
62384
  id: "0016_spur_cli_history_message_ts_nullable",
62371
62385
  sql: HISTORY_MESSAGE_TS_NULLABLE_SCHEMA_SQL
62386
+ },
62387
+ {
62388
+ id: "0017_spur_cli_runs_status_completed_to_done",
62389
+ sql: "UPDATE runs SET status = 'done' WHERE status = 'completed'"
62372
62390
  }
62373
62391
  ];
62374
62392
  });
@@ -64266,8 +64284,8 @@ var init_markdown_document = __esm(() => {
64266
64284
  "Q&A",
64267
64285
  "Design",
64268
64286
  "Plan",
64269
- "Solution",
64270
64287
  "Root Cause",
64288
+ "Solution",
64271
64289
  "Testing",
64272
64290
  "Review",
64273
64291
  "References",
@@ -64599,6 +64617,62 @@ var init_task_skeleton = __esm(() => {
64599
64617
  CANONICAL_INDEX = new Map(TASK_CANONICAL_SECTIONS.map((s2, i2) => [s2, i2]));
64600
64618
  });
64601
64619
 
64620
+ // ../../packages/domain/src/retention.ts
64621
+ import { readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync6 } from "fs";
64622
+ import { join as join7 } from "path";
64623
+ async function runRetention(db2, cwd, now = new Date) {
64624
+ const result = { ruleEvalRuns: 0, queueJobs: 0, ledgerRows: 0, backupFiles: 0 };
64625
+ result.ruleEvalRuns = await purgeRuleEvalRuns(db2, now);
64626
+ result.queueJobs = await purgeQueueJobs(db2, now);
64627
+ result.ledgerRows = await purgeLedger(db2, now);
64628
+ result.backupFiles = pruneBackups(cwd, now);
64629
+ return result;
64630
+ }
64631
+ async function purgeRuleEvalRuns(db2, now) {
64632
+ return purgeRows(db2, `DELETE FROM rule_eval_runs WHERE created_at < ?`, new Date(now.getTime() - RULE_EVAL_RUN_RETENTION_DAYS * DAY_MS).toISOString());
64633
+ }
64634
+ async function purgeQueueJobs(db2, now) {
64635
+ return purgeRows(db2, `DELETE FROM queue_jobs WHERE status IN ('completed', 'failed') AND updated_at < ?`, now.getTime() - QUEUE_JOB_RETENTION_DAYS * DAY_MS);
64636
+ }
64637
+ async function purgeLedger(db2, now) {
64638
+ return purgeRows(db2, `DELETE FROM history_import_ledger WHERE imported_at < ?`, new Date(now.getTime() - LEDGER_RETENTION_DAYS * DAY_MS).toISOString());
64639
+ }
64640
+ function pruneBackups(cwd, now) {
64641
+ const dir = join7(cwd, ".spur", "backups");
64642
+ let entries;
64643
+ try {
64644
+ entries = readdirSync5(dir);
64645
+ } catch {
64646
+ return 0;
64647
+ }
64648
+ const cutoffMs = now.getTime() - BACKUP_RETENTION_DAYS * DAY_MS;
64649
+ let reclaimed = 0;
64650
+ for (const name of entries) {
64651
+ const path9 = join7(dir, name);
64652
+ try {
64653
+ const stat = statSync6(path9);
64654
+ if (!stat.isFile() || stat.mtimeMs >= cutoffMs)
64655
+ continue;
64656
+ rmSync3(path9, { force: true });
64657
+ reclaimed += 1;
64658
+ } catch {}
64659
+ }
64660
+ return reclaimed;
64661
+ }
64662
+ async function purgeRows(db2, sql4, cutoff) {
64663
+ try {
64664
+ await db2.run(sql4, cutoff);
64665
+ const row = await db2.queryFirst("SELECT changes() AS n");
64666
+ return row?.n ?? 0;
64667
+ } catch {
64668
+ return 0;
64669
+ }
64670
+ }
64671
+ var RULE_EVAL_RUN_RETENTION_DAYS = 90, QUEUE_JOB_RETENTION_DAYS = 30, LEDGER_RETENTION_DAYS = 180, BACKUP_RETENTION_DAYS = 30, DAY_MS;
64672
+ var init_retention = __esm(() => {
64673
+ DAY_MS = 24 * 60 * 60 * 1000;
64674
+ });
64675
+
64602
64676
  // ../../packages/domain/src/stage-registry/validator.ts
64603
64677
  function generateRunId() {
64604
64678
  const crypto3 = globalThis.crypto;
@@ -64881,6 +64955,7 @@ __export(exports_src, {
64881
64955
  selectorDigest: () => selectorDigest,
64882
64956
  sectionMatrixSchema: () => sectionMatrixSchema,
64883
64957
  scaffoldFeatureScenarios: () => scaffoldFeatureScenarios,
64958
+ runRetention: () => runRetention,
64884
64959
  roleTokenSummary: () => roleTokenSummary,
64885
64960
  resolveReportMode: () => resolveReportMode,
64886
64961
  resolvePricing: () => resolvePricing,
@@ -64963,6 +65038,7 @@ __export(exports_src, {
64963
65038
  createId: () => createId,
64964
65039
  createDefaultRegistry: () => createDefaultRegistry,
64965
65040
  createBoundaryContext: () => createBoundaryContext,
65041
+ countToolCallsSince: () => countToolCallsSince,
64966
65042
  countCheckpointsBySource: () => countCheckpointsBySource,
64967
65043
  computeSnapshotHash: () => computeSnapshotHash,
64968
65044
  computeRecordCost: () => computeRecordCost,
@@ -65086,6 +65162,7 @@ var init_src2 = __esm(() => {
65086
65162
  init_rebuild_events();
65087
65163
  init_schema6();
65088
65164
  init_task_skeleton();
65165
+ init_retention();
65089
65166
  init_stage_registry();
65090
65167
  });
65091
65168
 
@@ -65601,7 +65678,7 @@ var isAgentExecution = (e) => ("executionId" in e), isActionOutput = (e) => ("st
65601
65678
 
65602
65679
  // ../../packages/app/src/observability/workflow-run-log-sink.ts
65603
65680
  import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, writeSync as writeSync2 } from "fs";
65604
- import { join as join7 } from "path";
65681
+ import { join as join8 } from "path";
65605
65682
 
65606
65683
  class WorkflowRunLogSink {
65607
65684
  filePath;
@@ -65617,7 +65694,7 @@ class WorkflowRunLogSink {
65617
65694
  bus;
65618
65695
  handlers;
65619
65696
  constructor(options) {
65620
- this.filePath = join7(options.dir, `${options.runId}.log`);
65697
+ this.filePath = join8(options.dir, `${options.runId}.log`);
65621
65698
  this.maxBytes = options.maxBytes ?? DEFAULT_RUN_LOG_MAX_BYTES;
65622
65699
  this.maxLines = options.maxLines;
65623
65700
  this.planPreview = options.planPreview;
@@ -66128,7 +66205,24 @@ class AgentService {
66128
66205
  async doctor(args, deps) {
66129
66206
  const executors = this.ctx.agentConfig?.executors;
66130
66207
  const doctorRunner = deps?.doctorRunner ?? new DoctorRunner({ env: this.ctx.env, executors });
66131
- const results = args.agent === undefined ? await doctorRunner.runAll() : [await doctorRunner.runOne(args.agent)];
66208
+ if (args.agent === undefined) {
66209
+ const results2 = await doctorRunner.runAll();
66210
+ return this.renderDoctor(results2, executors, args.json, args.agent);
66211
+ }
66212
+ const roleDef = this.ctx.roles?.get(args.agent);
66213
+ if (roleDef !== undefined) {
66214
+ const resolved = await this.resolveRole(args.agent, roleDef.tier, doctorRunner);
66215
+ if (!resolved.ok) {
66216
+ this.ctx.output.error(resolved.message);
66217
+ return resolved.exitCode;
66218
+ }
66219
+ const results2 = [await doctorRunner.runOne(resolved.executor ?? resolved.agent)];
66220
+ return this.renderDoctor(results2, executors, args.json, resolved.executor ?? resolved.agent);
66221
+ }
66222
+ const results = [await doctorRunner.runOne(args.agent)];
66223
+ return this.renderDoctor(results, executors, args.json, args.agent);
66224
+ }
66225
+ renderDoctor(results, executors, json3, agent) {
66132
66226
  const modelByExecutor = new Map((executors ?? []).filter((e) => e.model !== undefined).map((e) => [e.name, e.model]));
66133
66227
  for (const result of results) {
66134
66228
  if (result.modelStatus && (result.modelStatus.status === "quota_exhausted" || result.modelStatus.status === "unavailable")) {
@@ -66136,14 +66230,14 @@ class AgentService {
66136
66230
  this.ctx.output.error(`Warning: executor ${result.agent} (model ${model}) reports ${result.modelStatus.status}. Consider \`--agent <alt>\` or check token quota.`);
66137
66231
  }
66138
66232
  }
66139
- if (args.json) {
66233
+ if (json3) {
66140
66234
  const executorByName = new Map((executors ?? []).map((e) => [e.name, e]));
66141
66235
  const rows = results.map((result) => {
66142
66236
  const executor = executorByName.get(result.agent) ?? { name: result.agent, agent: result.agent };
66143
66237
  return { ...result, capabilityTier: getExecutorTier(executor) };
66144
66238
  });
66145
66239
  this.ctx.output.write(toJson({ agents: rows }));
66146
- } else if (args.agent !== undefined) {
66240
+ } else if (agent !== undefined) {
66147
66241
  this.ctx.output.write(renderDoctorDetail(results[0] ?? null));
66148
66242
  } else {
66149
66243
  this.ctx.output.write(renderDoctorTable(results));
@@ -67092,7 +67186,6 @@ function renderDoctorTable(results) {
67092
67186
  state: usable ? "usable" : "missing",
67093
67187
  agent: result.agent,
67094
67188
  tier: String(result.tier),
67095
- auth: usable ? renderAuth(result.authenticated) : dash,
67096
67189
  version: result.version ?? dash,
67097
67190
  model: renderModelStatus(result.modelStatus)
67098
67191
  };
@@ -67102,7 +67195,6 @@ function renderDoctorTable(results) {
67102
67195
  state: "STATUS",
67103
67196
  agent: "AGENT",
67104
67197
  tier: "TIER",
67105
- auth: "AUTH",
67106
67198
  version: "VERSION",
67107
67199
  model: "MODEL"
67108
67200
  };
@@ -67111,9 +67203,8 @@ function renderDoctorTable(results) {
67111
67203
  const wState = width("state");
67112
67204
  const wAgent = width("agent");
67113
67205
  const wTier = width("tier");
67114
- const wAuth = width("auth");
67115
67206
  const wVersion = width("version");
67116
- const line = (row) => `${row.glyph} ${row.state.padEnd(wState)} ${row.agent.padEnd(wAgent)} ${row.tier.padEnd(wTier)} ${row.auth.padEnd(wAuth)} ${row.version.padEnd(wVersion)} ${row.model}`.trimEnd();
67207
+ const line = (row) => `${row.glyph} ${row.state.padEnd(wState)} ${row.agent.padEnd(wAgent)} ${row.tier.padEnd(wTier)} ${row.version.padEnd(wVersion)} ${row.model}`.trimEnd();
67117
67208
  const usableCount = rows.filter((row) => row.state === "usable").length;
67118
67209
  const missingTier1 = results.filter((result) => !result.usable && result.tier === 1).length;
67119
67210
  const footer = missingTier1 > 0 ? `${usableCount} usable, ${missingTier1} missing (tier-1)` : `${usableCount} usable, ${rows.length - usableCount} missing`;
@@ -67239,7 +67330,7 @@ var init_agent_service = __esm(() => {
67239
67330
  });
67240
67331
 
67241
67332
  // ../../packages/app/src/services/anchor-qualifier.ts
67242
- import { basename as basename2, join as join8 } from "path";
67333
+ import { basename as basename2, join as join9 } from "path";
67243
67334
  async function resolveRepoRoot(projectRoot) {
67244
67335
  if (projectRoot)
67245
67336
  return projectRoot;
@@ -67365,7 +67456,7 @@ async function qualifyAnchors(fs3, opts) {
67365
67456
  } catch {
67366
67457
  continue;
67367
67458
  }
67368
- const mdFiles = entries.filter((name) => name.endsWith(".md") && name !== "kanban.md").map((name) => join8(dir, name)).sort();
67459
+ const mdFiles = entries.filter((name) => name.endsWith(".md") && name !== "kanban.md").map((name) => join9(dir, name)).sort();
67369
67460
  for (const filePath of mdFiles) {
67370
67461
  let raw;
67371
67462
  try {
@@ -67690,6 +67781,249 @@ var init_finding_codes2 = __esm(() => {
67690
67781
  init_src();
67691
67782
  });
67692
67783
 
67784
+ // ../../packages/app/src/services/structural-repair.ts
67785
+ function canonicalOrder(domain2) {
67786
+ return domain2 === "task" ? TASK_CANONICAL_SECTIONS : FEATURE_CANONICAL_SECTIONS;
67787
+ }
67788
+ function splitFrontmatter(content) {
67789
+ const m = FM_RE.exec(content);
67790
+ if (m && m.index === 0) {
67791
+ return { frontmatter: m[0], body: content.slice(m[0].length) };
67792
+ }
67793
+ return { frontmatter: "", body: content };
67794
+ }
67795
+ function scanHeadings(body, level) {
67796
+ const headings = [];
67797
+ let inCodeBlock = false;
67798
+ let lineStart = 0;
67799
+ for (let i2 = 0;i2 <= body.length; i2++) {
67800
+ const atEnd = i2 === body.length;
67801
+ if (!atEnd && body[i2] !== `
67802
+ `)
67803
+ continue;
67804
+ const line = body.slice(lineStart, i2);
67805
+ if (line.startsWith("```")) {
67806
+ inCodeBlock = !inCodeBlock;
67807
+ } else {
67808
+ const m = /^(#{1,6}) (.+)$/.exec(line);
67809
+ if (m !== null && !inCodeBlock) {
67810
+ const hashes = (m[1] ?? "").length;
67811
+ headings.push({
67812
+ start: lineStart,
67813
+ line,
67814
+ hashes,
67815
+ name: (m[2] ?? "").trim(),
67816
+ atLevel: hashes === level
67817
+ });
67818
+ }
67819
+ }
67820
+ lineStart = i2 + 1;
67821
+ }
67822
+ return headings;
67823
+ }
67824
+ function requirementsMissingCheckbox(body) {
67825
+ const out = [];
67826
+ for (const [i2, line] of body.split(`
67827
+ `).entries()) {
67828
+ if (/\[[ xX]\]/.test(line))
67829
+ continue;
67830
+ const m = /^(\s*)([-*])?\s*R\d+\.?(\s.*)?$/.exec(line);
67831
+ if (m !== null)
67832
+ out.push({ index: i2, bullet: m[2] ?? "" });
67833
+ }
67834
+ return out;
67835
+ }
67836
+ function correctedHeadings(headings, level, canonical) {
67837
+ return headings.map((h2) => h2.atLevel || !canonical.has(h2.name) ? h2 : { start: h2.start, line: h2.line, hashes: level, name: h2.name, atLevel: true });
67838
+ }
67839
+ function structuralFindings(content, domain2) {
67840
+ const findings = [];
67841
+ const level = LEVEL[domain2];
67842
+ const order = canonicalOrder(domain2);
67843
+ const { body } = splitFrontmatter(content);
67844
+ const headings = scanHeadings(body, level);
67845
+ const canonical = new Set(order);
67846
+ for (const h2 of headings) {
67847
+ if (h2.atLevel || !canonical.has(h2.name))
67848
+ continue;
67849
+ findings.push({
67850
+ layer: "L2",
67851
+ code: FINDING_CODES.L2_HEADING_LEVEL,
67852
+ severity: "warning",
67853
+ section: h2.name,
67854
+ message: `Section "${h2.name}" uses ${h2.hashes} heading level; expected ${level} (${"#".repeat(level)} ${h2.name})`
67855
+ });
67856
+ }
67857
+ const corrected = correctedHeadings(headings, level, canonical);
67858
+ const atLevel = corrected.filter((h2) => h2.atLevel);
67859
+ const presentCanonical = atLevel.filter((h2) => canonical.has(h2.name));
67860
+ const allCanonical = atLevel.length === presentCanonical.length;
67861
+ const ranked = presentCanonical.map((h2) => order.indexOf(h2.name));
67862
+ const sorted = [...ranked].sort((a2, b) => a2 - b);
67863
+ const outOfOrder = ranked.some((r, i2) => r !== (sorted[i2] ?? -1));
67864
+ if (allCanonical && outOfOrder && presentCanonical.length > 1) {
67865
+ findings.push({
67866
+ layer: "L2",
67867
+ code: FINDING_CODES.L2_SECTION_ORDER,
67868
+ severity: "warning",
67869
+ section: "",
67870
+ message: `Sections are out of canonical order: ${presentCanonical.map((h2) => h2.name).join(" \u2192 ")} (expected ${order.join(" \u2192 ")})`
67871
+ });
67872
+ }
67873
+ const reqHeading = atLevel.find((h2) => h2.name === "Requirements");
67874
+ if (reqHeading !== undefined) {
67875
+ const bodyStart = reqHeading.start + reqHeading.line.length + 1;
67876
+ const next = atLevel.find((h2) => h2.start > reqHeading.start);
67877
+ const bodyEnd = next !== undefined ? next.start : body.length;
67878
+ const missing = requirementsMissingCheckbox(body.slice(bodyStart, bodyEnd));
67879
+ if (missing.length > 0) {
67880
+ findings.push({
67881
+ layer: "L3",
67882
+ code: FINDING_CODES.L3_REQUIREMENTS_CHECKBOX,
67883
+ severity: "warning",
67884
+ section: "Requirements",
67885
+ message: `${missing.length} R-item(s) missing the checkbox marker \u2014 write as "- [ ] R1. \u2026"`
67886
+ });
67887
+ }
67888
+ }
67889
+ return findings;
67890
+ }
67891
+ function applyStructuralRepairs(content, domain2, entry) {
67892
+ const repairs = [];
67893
+ const level = LEVEL[domain2];
67894
+ const order = canonicalOrder(domain2);
67895
+ const canonical = new Set(order);
67896
+ const { frontmatter, body } = splitFrontmatter(content);
67897
+ const headings = scanHeadings(body, level);
67898
+ const levelEdits = new Map;
67899
+ for (const h2 of headings) {
67900
+ if (h2.atLevel || !canonical.has(h2.name))
67901
+ continue;
67902
+ const fixed = `${"#".repeat(level)} ${h2.name}`;
67903
+ levelEdits.set(h2.start, fixed);
67904
+ repairs.push({
67905
+ kind: "heading-level",
67906
+ section: h2.name,
67907
+ detail: `"${h2.line.trim()}" \u2192 "${fixed.trim()}"`
67908
+ });
67909
+ }
67910
+ const corrected = correctedHeadings(headings, level, canonical);
67911
+ const atLevel = corrected.filter((h2) => h2.atLevel);
67912
+ const leadStart = atLevel.length > 0 ? atLevel[0]?.start ?? 0 : body.length;
67913
+ const lead = body.slice(0, leadStart);
67914
+ const blocks = [];
67915
+ for (let i2 = 0;i2 < atLevel.length; i2++) {
67916
+ const h2 = atLevel[i2];
67917
+ if (h2 === undefined)
67918
+ continue;
67919
+ const next = atLevel[i2 + 1];
67920
+ const end = next !== undefined ? next.start : body.length;
67921
+ const headingLine = levelEdits.get(h2.start) ?? h2.line;
67922
+ const bodyStart = h2.start + h2.line.length + 1;
67923
+ blocks.push({
67924
+ name: h2.name,
67925
+ heading: headingLine,
67926
+ body: body.slice(bodyStart, end),
67927
+ canonicalRank: order.indexOf(h2.name)
67928
+ });
67929
+ }
67930
+ const present = new Set(blocks.map((b) => b.name));
67931
+ const missingNames = (entry?.required ?? []).filter((n2) => !present.has(n2) && canonical.has(n2)).sort((a2, b) => order.indexOf(a2) - order.indexOf(b));
67932
+ for (const name of missingNames) {
67933
+ repairs.push({
67934
+ kind: "missing-section",
67935
+ section: name,
67936
+ detail: `inserted empty "${"#".repeat(level)} ${name}"`
67937
+ });
67938
+ }
67939
+ const presentCanonical = blocks.filter((b) => b.canonicalRank >= 0);
67940
+ const allCanonical = blocks.length === presentCanonical.length;
67941
+ const ranked = presentCanonical.map((b) => b.canonicalRank);
67942
+ const sortedRanks = [...ranked].sort((a2, b) => a2 - b);
67943
+ const outOfOrder = ranked.some((r, i2) => r !== (sortedRanks[i2] ?? -1));
67944
+ const reorder = allCanonical && outOfOrder && presentCanonical.length > 1;
67945
+ if (reorder) {
67946
+ repairs.push({
67947
+ kind: "section-order",
67948
+ section: "",
67949
+ detail: `reordered to ${[...presentCanonical].sort((a2, b) => a2.canonicalRank - b.canonicalRank).map((b) => b.name).join(" \u2192 ")}`
67950
+ });
67951
+ }
67952
+ const reqBlock = blocks.find((b) => b.name === "Requirements");
67953
+ let reqRepairs = [];
67954
+ if (reqBlock !== undefined) {
67955
+ reqRepairs = requirementsMissingCheckbox(reqBlock.body);
67956
+ if (reqRepairs.length > 0) {
67957
+ repairs.push({
67958
+ kind: "requirement-checkbox",
67959
+ section: "Requirements",
67960
+ detail: `${reqRepairs.length} R-item(s) gained the "[ ] " checkbox marker`
67961
+ });
67962
+ }
67963
+ }
67964
+ if (repairs.length === 0) {
67965
+ return { content, changed: false, repairs: [] };
67966
+ }
67967
+ const orderedBlocks = reorder ? [...presentCanonical].sort((a2, b) => a2.canonicalRank - b.canonicalRank) : blocks;
67968
+ const emitted = new Set;
67969
+ const rendered = [];
67970
+ for (const b of orderedBlocks) {
67971
+ emitted.add(b.name);
67972
+ let blockBody = b.body;
67973
+ if (b.name === "Requirements" && reqRepairs.length > 0) {
67974
+ const lines = blockBody.split(`
67975
+ `);
67976
+ for (const rr of reqRepairs) {
67977
+ const orig = lines[rr.index];
67978
+ if (orig === undefined)
67979
+ continue;
67980
+ const leadWs = /^(\s*)/.exec(orig)?.[1] ?? "";
67981
+ const rest = orig.slice(leadWs.length).replace(/^[-*]\s*/, "");
67982
+ lines[rr.index] = `${leadWs}${rr.bullet === "" ? "- [ ]" : `${rr.bullet} [ ]`} ${rest}`;
67983
+ }
67984
+ blockBody = lines.join(`
67985
+ `);
67986
+ }
67987
+ rendered.push(`${b.heading}
67988
+ ${blockBody}`);
67989
+ }
67990
+ for (const b of blocks) {
67991
+ if (!emitted.has(b.name)) {
67992
+ rendered.push(`${b.heading}
67993
+ ${b.body}`);
67994
+ }
67995
+ }
67996
+ if (missingNames.length > 0) {
67997
+ for (const name of missingNames) {
67998
+ const rank = order.indexOf(name);
67999
+ const at = rendered.findIndex((blk) => {
68000
+ const headingName = /^#{1,6} (.+)$/.exec(blk.split(`
68001
+ `)[0] ?? "")?.[1]?.trim() ?? "";
68002
+ return canonical.has(headingName) && order.indexOf(headingName) > rank;
68003
+ });
68004
+ const heading = `${"#".repeat(level)} ${name}
68005
+ `;
68006
+ const prev = at === -1 ? rendered[rendered.length - 1] : rendered[at - 1];
68007
+ const sep = prev !== undefined && !prev.endsWith(`
68008
+
68009
+ `) ? `
68010
+ ` : "";
68011
+ rendered.splice(at === -1 ? rendered.length : at, 0, `${sep}${heading}
68012
+ `);
68013
+ }
68014
+ }
68015
+ const result = frontmatter + lead + rendered.join("");
68016
+ const changed = result !== content;
68017
+ return { content: result, changed, repairs };
68018
+ }
68019
+ var LEVEL, FM_RE;
68020
+ var init_structural_repair = __esm(() => {
68021
+ init_src2();
68022
+ init_planning_check_base();
68023
+ LEVEL = { task: 3, feature: 2 };
68024
+ FM_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
68025
+ });
68026
+
67693
68027
  // ../../packages/app/src/services/planning-check-base.ts
67694
68028
  function key(e) {
67695
68029
  return `${e.kind}:${e.id}:${e.code}`;
@@ -67741,7 +68075,7 @@ class PlanningCheckService {
67741
68075
  }
67742
68076
  return doc2;
67743
68077
  }
67744
- runL2(doc2, entry, findings) {
68078
+ runL2(doc2, entry, findings, raw) {
67745
68079
  if (!entry)
67746
68080
  return;
67747
68081
  const sectionNames = doc2.sectionNames;
@@ -67786,6 +68120,7 @@ class PlanningCheckService {
67786
68120
  });
67787
68121
  }
67788
68122
  }
68123
+ findings.push(...structuralFindings(raw, this.docKind));
67789
68124
  }
67790
68125
  summarizeWithStatus(status, findings, strict, overrides, accepted, id) {
67791
68126
  const effectiveFindings = [];
@@ -67830,6 +68165,7 @@ class PlanningCheckService {
67830
68165
  var init_planning_check_base = __esm(() => {
67831
68166
  init_src2();
67832
68167
  init_finding_codes2();
68168
+ init_structural_repair();
67833
68169
  });
67834
68170
 
67835
68171
  // ../../packages/app/src/services/feature-check.ts
@@ -67841,7 +68177,7 @@ __export(exports_feature_check, {
67841
68177
  FeatureCheckService: () => FeatureCheckService,
67842
68178
  DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX
67843
68179
  });
67844
- import { dirname as dirname8, join as join9 } from "path";
68180
+ import { dirname as dirname8, join as join10 } from "path";
67845
68181
  function isGroupFeature(fm) {
67846
68182
  const tags = fm.tags;
67847
68183
  return Array.isArray(tags) && tags.includes("group") ? "group" : "standard";
@@ -67939,9 +68275,9 @@ function decodeVerdictRows(source, sectionName, required2) {
67939
68275
  function defaultVerdictRunDir(tasksDir) {
67940
68276
  const norm = tasksDir.replace(/\\/g, "/");
67941
68277
  if (/\/docs\/tasks\d*$/.test(norm) || /\/docs\/tasks$/.test(norm)) {
67942
- return join9(dirname8(dirname8(tasksDir)), ".spur", "run");
68278
+ return join10(dirname8(dirname8(tasksDir)), ".spur", "run");
67943
68279
  }
67944
- return join9(dirname8(tasksDir), ".spur", "run");
68280
+ return join10(dirname8(tasksDir), ".spur", "run");
67945
68281
  }
67946
68282
  function rowMatchesScenario(id, sc) {
67947
68283
  const stripped = id.replace(/^\[[^\]]*\]\s*/, "").replace(/^Scenario:\s*/i, "").replace(/^\[[^\]]*\]\s*/, "").trim();
@@ -67963,6 +68299,7 @@ var init_feature_check = __esm(() => {
67963
68299
  init_src2();
67964
68300
  init_done_transition_guard();
67965
68301
  init_planning_check_base();
68302
+ init_structural_repair();
67966
68303
  DEFAULT_FEATURE_MATRIX = {
67967
68304
  variants: {
67968
68305
  standard: {
@@ -68037,7 +68374,22 @@ var init_feature_check = __esm(() => {
68037
68374
  }
68038
68375
  async check(filePath, featureId2, options) {
68039
68376
  const strict = options?.strict === true;
68040
- const raw = await this.fs.readFile(filePath);
68377
+ const rawSource = await this.fs.readFile(filePath);
68378
+ let raw = rawSource;
68379
+ let repairs = [];
68380
+ if (options?.fix === true) {
68381
+ const probe = MarkdownDocument.parse(rawSource, "feature");
68382
+ const probeFm = probe.frontmatterData ?? {};
68383
+ const probeStatus = probeFm.status ?? "backlog";
68384
+ const probeEffective = options?.asStatus ?? probeStatus;
68385
+ const fixEntry = this.resolveMatrixEntry(isGroupFeature(probeFm), probeEffective);
68386
+ const fixed = applyStructuralRepairs(rawSource, "feature", fixEntry);
68387
+ if (fixed.changed) {
68388
+ await this.fs.writeFile(filePath, fixed.content);
68389
+ repairs = fixed.repairs;
68390
+ raw = fixed.content;
68391
+ }
68392
+ }
68041
68393
  const findings = [];
68042
68394
  const doc2 = this.runL1(raw, featureId2, findings);
68043
68395
  if (doc2 === null) {
@@ -68046,18 +68398,22 @@ var init_feature_check = __esm(() => {
68046
68398
  const fm = doc2.frontmatterData ?? {};
68047
68399
  const status = fm.status ?? "backlog";
68048
68400
  const entry = this.resolveMatrixEntry(isGroupFeature(fm), status);
68049
- this.runL2(doc2, entry, findings);
68401
+ this.runL2(doc2, entry, findings, raw);
68050
68402
  this.runL3(doc2, findings, fm);
68051
68403
  if (options?.featuresDir) {
68052
68404
  await this.checkOneActiveGoal(fm, featureId2, options.featuresDir, findings, options.asStatus);
68053
68405
  await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
68054
68406
  }
68055
- const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join9(dirname8(options.featuresDir), "dogfood") : undefined);
68407
+ const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join10(dirname8(options.featuresDir), "dogfood") : undefined);
68056
68408
  const primaryTasksDir = options?.tasksDir ?? options?.tasksDirs?.[0];
68057
68409
  const runDir = options?.runDir ?? (primaryTasksDir ? defaultVerdictRunDir(primaryTasksDir) : undefined);
68058
68410
  const taskScanDirs = options?.tasksDirs && options.tasksDirs.length > 0 ? options.tasksDirs : options?.tasksDir ? [options.tasksDir] : [];
68059
68411
  await this.runL4(doc2, featureId2, status, taskScanDirs, dogfoodDir, runDir, findings);
68060
- return { id: featureId2, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
68412
+ return {
68413
+ id: featureId2,
68414
+ ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides),
68415
+ repairs
68416
+ };
68061
68417
  }
68062
68418
  runL3(doc2, findings, fm) {
68063
68419
  const rawAc = doc2.getSection("Acceptance Criteria");
@@ -68494,7 +68850,7 @@ var init_task_locator = __esm(() => {
68494
68850
  });
68495
68851
 
68496
68852
  // ../../packages/app/src/services/task-check.ts
68497
- import { basename as basename3, dirname as dirname9, join as join10 } from "path";
68853
+ import { basename as basename3, dirname as dirname9, join as join11 } from "path";
68498
68854
  function isPlaceholderBody(body) {
68499
68855
  const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
68500
68856
  return stripped.length === 0;
@@ -68696,6 +69052,7 @@ var EXTERNAL_EVIDENCE_RE, TaskCheckService;
68696
69052
  var init_task_check = __esm(() => {
68697
69053
  init_src2();
68698
69054
  init_planning_check_base();
69055
+ init_structural_repair();
68699
69056
  init_task_locator();
68700
69057
  init_verify_verdict();
68701
69058
  EXTERNAL_EVIDENCE_RE = /`([^`\n]+?)`\s+(?:line|lines?)\s+(\d+)(?:-(\d+))?/g;
@@ -68719,7 +69076,23 @@ var init_task_check = __esm(() => {
68719
69076
  }
68720
69077
  async check(filePath, wbs, options) {
68721
69078
  const strict = options?.strict === true;
68722
- const raw = await this.fs.readFile(filePath);
69079
+ const rawSource = await this.fs.readFile(filePath);
69080
+ let raw = rawSource;
69081
+ let repairs = [];
69082
+ if (options?.fix === true) {
69083
+ const probe = MarkdownDocument.parse(rawSource, "task");
69084
+ const probeFm = probe.frontmatterData ?? {};
69085
+ const probeStatus = probeFm.status ?? "backlog";
69086
+ const probeEffective = options?.asStatus ?? probeStatus;
69087
+ const probeVariant = probeFm.template ?? DEFAULT_TASK_VARIANT;
69088
+ const fixEntry = this.resolveMatrixEntry(probeVariant, probeEffective);
69089
+ const fixed = applyStructuralRepairs(rawSource, "task", fixEntry);
69090
+ if (fixed.changed) {
69091
+ await this.fs.writeFile(filePath, fixed.content);
69092
+ repairs = fixed.repairs;
69093
+ raw = fixed.content;
69094
+ }
69095
+ }
68723
69096
  const findings = [];
68724
69097
  const doc2 = this.runL1(raw, wbs, findings);
68725
69098
  if (doc2 === null) {
@@ -68733,10 +69106,10 @@ var init_task_check = __esm(() => {
68733
69106
  const effectiveStatus = options?.asStatus ?? status;
68734
69107
  const variant = fm.template ?? DEFAULT_TASK_VARIANT;
68735
69108
  const entry = this.resolveMatrixEntry(variant, effectiveStatus);
68736
- this.runL2(doc2, entry, findings);
69109
+ this.runL2(doc2, entry, findings, raw);
68737
69110
  this.runL3(doc2, entry, effectiveStatus, findings);
68738
69111
  const tasksDir = dirname9(filePath);
68739
- const featuresDir = join10(dirname9(tasksDir), "features");
69112
+ const featuresDir = join11(dirname9(tasksDir), "features");
68740
69113
  await this.runL4(doc2, fm, effectiveStatus, findings, featuresDir, tasksDir, wbs);
68741
69114
  await this.runL4Rollup(doc2, wbs, effectiveStatus, findings, tasksDir);
68742
69115
  if (effectiveStatus !== "done" && effectiveStatus !== "cancelled") {
@@ -68744,7 +69117,8 @@ var init_task_check = __esm(() => {
68744
69117
  }
68745
69118
  return {
68746
69119
  wbs,
68747
- ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs)
69120
+ ...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs),
69121
+ repairs
68748
69122
  };
68749
69123
  }
68750
69124
  runL3(doc2, entry, status, findings) {
@@ -69171,7 +69545,7 @@ var init_task_check = __esm(() => {
69171
69545
  const body = doc2.getSection(section);
69172
69546
  if (body === null)
69173
69547
  continue;
69174
- if (/\b(HITL|human[- ]in[- ]the[- ]loop|approval|approved|merge event|merged|content-gate|GATED|capstone)\b/i.test(body)) {
69548
+ if (/(?<![\w-])(HITL|human[- ]in[- ]the-loop|approval|approved|merge event|merged|content-gate|GATED|capstone)(?![\w-])/i.test(body)) {
69175
69549
  findings.push({
69176
69550
  layer: "L4",
69177
69551
  code: FINDING_CODES.L4_GATE_LANGUAGE,
@@ -69227,7 +69601,7 @@ var init_task_check = __esm(() => {
69227
69601
  for (const cite of citations) {
69228
69602
  if (reported >= 5)
69229
69603
  break;
69230
- const abs = join10(projectRoot, cite.path);
69604
+ const abs = join11(projectRoot, cite.path);
69231
69605
  let exists2 = false;
69232
69606
  try {
69233
69607
  exists2 = await this.fs.exists(abs);
@@ -69309,7 +69683,7 @@ var init_task_check = __esm(() => {
69309
69683
  continue;
69310
69684
  }
69311
69685
  try {
69312
- const abs = join10(dir, name);
69686
+ const abs = join11(dir, name);
69313
69687
  const stat = await this.fs.stat(abs);
69314
69688
  if (stat !== null && stat !== undefined && stat.isDirectory?.())
69315
69689
  stack.push(abs);
@@ -69404,7 +69778,7 @@ var init_task_check = __esm(() => {
69404
69778
  }
69405
69779
  async checkVerdictArtifact(wbs, tasksDir, findings) {
69406
69780
  const projectRoot = resolveProjectRootFromTasksDir(tasksDir);
69407
- const runDir = join10(projectRoot, ".spur", "run");
69781
+ const runDir = join11(projectRoot, ".spur", "run");
69408
69782
  const verdictPath = `${runDir}/${wbs}-verdict.json`;
69409
69783
  const outcome = await readVerifyVerdict(this.fs, verdictPath, wbs);
69410
69784
  if (outcome.kind === "missing")
@@ -69449,7 +69823,7 @@ var init_task_check = __esm(() => {
69449
69823
  });
69450
69824
 
69451
69825
  // ../../packages/app/src/services/corpus-check.ts
69452
- import { basename as basename4, dirname as dirname10, join as join11, relative, resolve as resolve3 } from "path";
69826
+ import { basename as basename4, dirname as dirname10, join as join12, relative, resolve as resolve3 } from "path";
69453
69827
  function baselineSeverity(e) {
69454
69828
  return e.severity ?? "error";
69455
69829
  }
@@ -69457,7 +69831,7 @@ function resolveProjectRoot(cwd) {
69457
69831
  const fs3 = createNodeFileSystem3(cwd);
69458
69832
  let current = resolve3(cwd);
69459
69833
  while (true) {
69460
- if (fs3.exists(join11(current, ".spur", "config.yaml")) || fs3.exists(join11(current, "config", "corpus-baseline.json"))) {
69834
+ if (fs3.exists(join12(current, ".spur", "config.yaml")) || fs3.exists(join12(current, "config", "corpus-baseline.json"))) {
69461
69835
  return current;
69462
69836
  }
69463
69837
  const parent = dirname10(current);
@@ -69468,10 +69842,10 @@ function resolveProjectRoot(cwd) {
69468
69842
  }
69469
69843
  async function loadTaskMatrix(projectRoot) {
69470
69844
  const fs3 = createNodeFileSystem3(projectRoot);
69471
- const candidates = [join11(projectRoot, ".spur", "tasks", "section-matrix.yaml")];
69845
+ const candidates = [join12(projectRoot, ".spur", "tasks", "section-matrix.yaml")];
69472
69846
  const bundledRoot = bundledConfigRoot();
69473
69847
  if (bundledRoot !== null)
69474
- candidates.push(join11(bundledRoot, "tasks", "section-matrix.yaml"));
69848
+ candidates.push(join12(bundledRoot, "tasks", "section-matrix.yaml"));
69475
69849
  let matrixPath;
69476
69850
  for (const candidate of candidates) {
69477
69851
  if (await fs3.exists(candidate)) {
@@ -69514,7 +69888,7 @@ async function structuralSweep(projectRoot) {
69514
69888
  const wbs = fileName.match(/^(\d{4})_.+\.md$/)?.[1];
69515
69889
  if (wbs === undefined)
69516
69890
  continue;
69517
- const result = await taskService.check(join11(tasksDir, fileName), wbs, {
69891
+ const result = await taskService.check(join12(tasksDir, fileName), wbs, {
69518
69892
  severityOverrides: planning.severityOverrides
69519
69893
  });
69520
69894
  for (const finding of result.findings) {
@@ -69534,7 +69908,7 @@ async function structuralSweep(projectRoot) {
69534
69908
  const id = fileName.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1];
69535
69909
  if (id === undefined)
69536
69910
  continue;
69537
- const result = await featureService.check(join11(featuresDir, fileName), id, {
69911
+ const result = await featureService.check(join12(featuresDir, fileName), id, {
69538
69912
  featuresDir,
69539
69913
  tasksDir: activeTasksDir,
69540
69914
  tasksDirs: taskDirs,
@@ -69564,7 +69938,7 @@ async function duplicateIds(cwd, taskDirs, featuresDir) {
69564
69938
  return [];
69565
69939
  }
69566
69940
  const pattern = kind === "task" ? /^(\d{4})_/ : /^([A-Z][0-9]*)_/;
69567
- return names.map((n2) => ({ m: n2.match(pattern), n: n2 })).filter((x) => x.m !== null).map((x) => ({ id: x.m[1], file: relative(cwd, join11(dir, x.n)), kind }));
69941
+ return names.map((n2) => ({ m: n2.match(pattern), n: n2 })).filter((x) => x.m !== null).map((x) => ({ id: x.m[1], file: relative(cwd, join12(dir, x.n)), kind }));
69568
69942
  };
69569
69943
  const all = [
69570
69944
  ...(await Promise.all(taskDirs.map((dir) => scan(dir, "task")))).flat(),
@@ -69603,14 +69977,14 @@ function diskReader(cwd) {
69603
69977
  return {
69604
69978
  list: async (dir) => {
69605
69979
  try {
69606
- return (await fs3.readDir(join11(cwd, dir))).map((n2) => `${dir}/${n2}`);
69980
+ return (await fs3.readDir(join12(cwd, dir))).map((n2) => `${dir}/${n2}`);
69607
69981
  } catch {
69608
69982
  return [];
69609
69983
  }
69610
69984
  },
69611
69985
  read: async (path9) => {
69612
69986
  try {
69613
- return await fs3.readFile(join11(cwd, path9));
69987
+ return await fs3.readFile(join12(cwd, path9));
69614
69988
  } catch {
69615
69989
  return null;
69616
69990
  }
@@ -69802,7 +70176,7 @@ function reconcileBaseline(observed, baseline) {
69802
70176
  }
69803
70177
  async function runCorpusCheck(cwd, since) {
69804
70178
  const projectRoot = resolveProjectRoot(cwd);
69805
- const baselineFile = join11(projectRoot, "config", "corpus-baseline.json");
70179
+ const baselineFile = join12(projectRoot, "config", "corpus-baseline.json");
69806
70180
  let baseline = { entries: [] };
69807
70181
  if (await Bun.file(baselineFile).exists()) {
69808
70182
  baseline = await Bun.file(baselineFile).json();
@@ -69822,7 +70196,7 @@ async function runCorpusCheck(cwd, since) {
69822
70196
  }
69823
70197
  async function loadAcceptedFindings(cwd) {
69824
70198
  const projectRoot = resolveProjectRoot(cwd);
69825
- const baselineFile = join11(projectRoot, "config", "corpus-baseline.json");
70199
+ const baselineFile = join12(projectRoot, "config", "corpus-baseline.json");
69826
70200
  const accepted = new Map;
69827
70201
  try {
69828
70202
  if (await Bun.file(baselineFile).exists()) {
@@ -69852,7 +70226,7 @@ var init_corpus_check = __esm(() => {
69852
70226
  });
69853
70227
 
69854
70228
  // ../../packages/app/src/services/corpus-migrator.ts
69855
- import { join as join12, relative as relative2 } from "path";
70229
+ import { join as join13, relative as relative2 } from "path";
69856
70230
  function applyM1(data, flags) {
69857
70231
  const raw = data.status;
69858
70232
  if (typeof raw !== "string")
@@ -70152,7 +70526,7 @@ class CorpusMigrator {
70152
70526
  }
70153
70527
  async discoverFiles() {
70154
70528
  const entries = await this.fs.readDir(this.corpusDir);
70155
- return entries.filter((name) => name.endsWith(".md")).filter((name) => name !== "kanban.md").map((name) => join12(this.corpusDir, name)).sort();
70529
+ return entries.filter((name) => name.endsWith(".md")).filter((name) => name !== "kanban.md").map((name) => join13(this.corpusDir, name)).sort();
70156
70530
  }
70157
70531
  async migrateFile(filePath, dryRun, gitDates = new Map) {
70158
70532
  const flags = [];
@@ -73000,15 +73374,15 @@ var init_feature_service = __esm(() => {
73000
73374
  import {
73001
73375
  existsSync as existsSync5,
73002
73376
  mkdirSync as mkdirSync6,
73003
- readdirSync as readdirSync5,
73377
+ readdirSync as readdirSync6,
73004
73378
  readFileSync as readFileSync8,
73005
73379
  readlinkSync,
73006
- rmSync as rmSync3,
73380
+ rmSync as rmSync4,
73007
73381
  symlinkSync,
73008
73382
  unlinkSync as unlinkSync2,
73009
73383
  writeFileSync as writeFileSync4
73010
73384
  } from "fs";
73011
- import { dirname as dirname11, isAbsolute as isAbsolute3, join as join13, resolve as resolve4 } from "path";
73385
+ import { dirname as dirname11, isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "path";
73012
73386
  async function buildRefreshCoverage(db2, selector, fanOut) {
73013
73387
  const statusBySource = new Map(fanOut.entries.map((e) => [e.source, e.status]));
73014
73388
  const refreshed = FULL_FIDELITY_SOURCES.filter((s3) => statusBySource.has(s3) && statusBySource.get(s3) !== "failed");
@@ -73157,8 +73531,9 @@ class HistoryService {
73157
73531
  const timeoutMs = opts.sourceTimeout ?? DEFAULT_SOURCE_TIMEOUT_MS;
73158
73532
  const entries = [];
73159
73533
  const warnings = [];
73534
+ const runStartedAt = new Date().toISOString();
73160
73535
  for (const source of sources) {
73161
- const { coverageEntry, sourceWarnings } = await this.importOneIsolated(source, opts, timeoutMs);
73536
+ const { coverageEntry, sourceWarnings } = await this.importOneIsolated(source, opts, timeoutMs, runStartedAt);
73162
73537
  entries.push(coverageEntry);
73163
73538
  warnings.push(...sourceWarnings);
73164
73539
  }
@@ -73193,10 +73568,11 @@ class HistoryService {
73193
73568
  writeFileSync4(reportPath, renderMarkdown(artifact, opts.mode));
73194
73569
  }
73195
73570
  const pruned = pruneReports(cwd, REPORT_RETENTION_DAYS);
73571
+ const retained = await runRetention(db2, cwd);
73196
73572
  const coverage = await buildRefreshCoverage(db2, selector, fanOut);
73197
- return { fanOut, artifact, pruned, coverage, ...reportPath !== undefined ? { reportPath } : {} };
73573
+ return { fanOut, artifact, pruned, retained, coverage, ...reportPath !== undefined ? { reportPath } : {} };
73198
73574
  }
73199
- async importOneIsolated(source, opts, timeoutMs) {
73575
+ async importOneIsolated(source, opts, timeoutMs, runStartedAt) {
73200
73576
  const db2 = await this.ctx.getDb();
73201
73577
  const sourceWarnings = [];
73202
73578
  const checkpointCount = await countCheckpointsBySource(db2, source);
@@ -73235,12 +73611,13 @@ class HistoryService {
73235
73611
  detail: `source '${source}' previously had checkpoint rows but discovered 0 files`
73236
73612
  });
73237
73613
  }
73614
+ const toolCalls = opts.dryRun ? 0 : await countToolCallsSince(db2, source, runStartedAt);
73238
73615
  const coverageEntry = {
73239
73616
  source,
73240
73617
  status,
73241
73618
  files: result.scannedFiles,
73242
73619
  messages: result.importedRecords,
73243
- toolCalls: 0,
73620
+ toolCalls,
73244
73621
  unknownRecords: result.unknownRecords,
73245
73622
  lastImportedAt: null,
73246
73623
  parseErrors: result.parseErrors.length,
@@ -73292,7 +73669,7 @@ function emptyTotals2() {
73292
73669
  function foldMessage(bucket, row) {
73293
73670
  bucket.messages += row.messages;
73294
73671
  bucket.records += row.messages;
73295
- bucket.inputTokens += row.inputTokens ?? 0;
73672
+ bucket.inputTokens += (row.inputTokens ?? 0) + (row.cacheReadTokens ?? 0) + (row.cacheWriteTokens ?? 0);
73296
73673
  bucket.outputTokens += row.outputTokens ?? 0;
73297
73674
  bucket.cacheReadTokens += row.cacheReadTokens ?? 0;
73298
73675
  bucket.cacheWriteTokens += row.cacheWriteTokens ?? 0;
@@ -73440,9 +73817,9 @@ function resolveArtifactPaths(artifact, opts) {
73440
73817
  return { artifactPath: out, sidecarPath: `${out.replace(/\.json$/, "")}.errors.jsonl` };
73441
73818
  }
73442
73819
  const base2 = resolve4(opts.cwd, ".spur", "reports", "history");
73443
- const dateDir = join13(base2, artifact.generatedAt.slice(0, 10));
73820
+ const dateDir = join14(base2, artifact.generatedAt.slice(0, 10));
73444
73821
  const name = `analyze-${selectorDigest(artifact.selector)}`;
73445
- return { artifactPath: join13(dateDir, `${name}.json`), sidecarPath: join13(dateDir, `${name}.errors.jsonl`) };
73822
+ return { artifactPath: join14(dateDir, `${name}.json`), sidecarPath: join14(dateDir, `${name}.errors.jsonl`) };
73446
73823
  }
73447
73824
  function boundCoverage(coverage) {
73448
73825
  const overflow = [];
@@ -73465,7 +73842,7 @@ function boundCoverage(coverage) {
73465
73842
  }
73466
73843
  function updateLatestPointer(opts, artifactPath) {
73467
73844
  const base2 = resolve4(opts.cwd, ".spur", "reports", "history");
73468
- const latest = join13(base2, "latest.json");
73845
+ const latest = join14(base2, "latest.json");
73469
73846
  mkdirSync6(base2, { recursive: true });
73470
73847
  if (existsSync5(latest))
73471
73848
  unlinkSync2(latest);
@@ -73531,10 +73908,10 @@ function formatIssue(issue2) {
73531
73908
  return `${issue2.sourceFile}:${issue2.sourceLine}: ${issue2.reason}`;
73532
73909
  }
73533
73910
  function pruneReports(cwd, retentionDays = REPORT_RETENTION_DAYS, now2 = new Date) {
73534
- const dir = join13(cwd, ".spur", "reports", "history");
73911
+ const dir = join14(cwd, ".spur", "reports", "history");
73535
73912
  let entries;
73536
73913
  try {
73537
- entries = readdirSync5(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
73914
+ entries = readdirSync6(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
73538
73915
  } catch {
73539
73916
  return [];
73540
73917
  }
@@ -73549,7 +73926,7 @@ function pruneReports(cwd, retentionDays = REPORT_RETENTION_DAYS, now2 = new Dat
73549
73926
  if (entryDate >= cutoff)
73550
73927
  continue;
73551
73928
  try {
73552
- rmSync3(join13(dir, name), { recursive: true, force: true });
73929
+ rmSync4(join14(dir, name), { recursive: true, force: true });
73553
73930
  pruned.push(name);
73554
73931
  } catch {}
73555
73932
  }
@@ -73880,6 +74257,16 @@ async function ensurePipelineRunLink(db2, wbs, options = {}) {
73880
74257
  const links = await dao2.listByWbs(wbs, 20);
73881
74258
  const existing = links.find((l) => l.kind === "pipeline");
73882
74259
  if (existing) {
74260
+ if (options.runId !== undefined && options.runId !== existing.run_id) {
74261
+ await dao2.updateRunId(existing.id, options.runId);
74262
+ return {
74263
+ created: false,
74264
+ id: existing.id,
74265
+ wbs: existing.wbs,
74266
+ runId: options.runId,
74267
+ kind: "pipeline"
74268
+ };
74269
+ }
73883
74270
  return {
73884
74271
  created: false,
73885
74272
  id: existing.id,
@@ -74376,17 +74763,17 @@ var init_process_inventory_service = __esm(() => {
74376
74763
  });
74377
74764
 
74378
74765
  // ../../packages/app/src/services/project-registry.ts
74379
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
74766
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
74380
74767
  import { connect, createServer } from "net";
74381
74768
  import { homedir as homedir5 } from "os";
74382
- import { dirname as dirname12, join as join14, resolve as resolve5 } from "path";
74769
+ import { dirname as dirname12, join as join15, resolve as resolve5 } from "path";
74383
74770
  function normalizeProjectPath(pathInput) {
74384
74771
  const trimmed = pathInput.trim();
74385
74772
  let expanded = trimmed;
74386
74773
  if (trimmed === "~") {
74387
74774
  expanded = homedir5();
74388
74775
  } else if (trimmed.startsWith("~/")) {
74389
- expanded = join14(homedir5(), trimmed.slice(2));
74776
+ expanded = join15(homedir5(), trimmed.slice(2));
74390
74777
  } else {
74391
74778
  expanded = resolve5(trimmed);
74392
74779
  }
@@ -74509,7 +74896,7 @@ class ProjectRegistry {
74509
74896
  }
74510
74897
  if (!acquired) {
74511
74898
  try {
74512
- rmSync4(this.lockDir, { recursive: true, force: true });
74899
+ rmSync5(this.lockDir, { recursive: true, force: true });
74513
74900
  mkdirSync7(this.lockDir);
74514
74901
  acquired = true;
74515
74902
  } catch {
@@ -74521,7 +74908,7 @@ class ProjectRegistry {
74521
74908
  } finally {
74522
74909
  if (acquired) {
74523
74910
  try {
74524
- rmSync4(this.lockDir, { recursive: true, force: true });
74911
+ rmSync5(this.lockDir, { recursive: true, force: true });
74525
74912
  } catch {}
74526
74913
  }
74527
74914
  }
@@ -74826,7 +75213,7 @@ var init_project_start = __esm(() => {
74826
75213
 
74827
75214
  // ../../packages/app/src/services/rule-service.ts
74828
75215
  import { homedir as homedir6 } from "os";
74829
- import { delimiter, join as join15, relative as relative3, resolve as resolve7 } from "path";
75216
+ import { delimiter, join as join16, relative as relative3, resolve as resolve7 } from "path";
74830
75217
 
74831
75218
  class RuleService {
74832
75219
  context;
@@ -75021,13 +75408,13 @@ ${ruleIds.join(`
75021
75408
  const hasGlobalOverride = globalOverride !== undefined && globalOverride.length > 0;
75022
75409
  layers.push({
75023
75410
  id: "global",
75024
- path: hasGlobalOverride ? resolve7(cwd, globalOverride) : join15(homedir6(), GLOBAL_RULES_DIR),
75411
+ path: hasGlobalOverride ? resolve7(cwd, globalOverride) : join16(homedir6(), GLOBAL_RULES_DIR),
75025
75412
  priority: 10
75026
75413
  });
75027
75414
  if (opts.includeBundled && !hasGlobalOverride) {
75028
75415
  const bundledConfig = bundledConfigRoot();
75029
75416
  if (bundledConfig !== null) {
75030
- layers.push({ id: "bundled-config", path: join15(bundledConfig, "rules"), priority: 15 });
75417
+ layers.push({ id: "bundled-config", path: join16(bundledConfig, "rules"), priority: 15 });
75031
75418
  }
75032
75419
  const bundled = await bundledRulesRoot();
75033
75420
  if (bundled !== null)
@@ -75240,7 +75627,7 @@ ${ruleIds.join(`
75240
75627
  const { fs: fs3 } = this.context;
75241
75628
  for (const root of await this.ruleRoots()) {
75242
75629
  for (const ext of ["yaml", "yml", "json"]) {
75243
- if (await fs3.exists(join15(root, `${name}.${ext}`)))
75630
+ if (await fs3.exists(join16(root, `${name}.${ext}`)))
75244
75631
  return true;
75245
75632
  }
75246
75633
  }
@@ -75276,7 +75663,7 @@ ${ruleIds.join(`
75276
75663
  for (const layer of await this.existingRuleSourceLayers()) {
75277
75664
  const entries = await this.context.fs.readDir(layer.path);
75278
75665
  const named = entries.map((entry) => ({ entry, name: entry.match(/^([\w-]+)\.(?:ya?ml|json)$/i)?.[1] ?? "" })).filter(({ name }) => name.length > 0);
75279
- const stats = await Promise.all(named.map(({ entry }) => this.context.fs.stat(join15(layer.path, entry))));
75666
+ const stats = await Promise.all(named.map(({ entry }) => this.context.fs.stat(join16(layer.path, entry))));
75280
75667
  named.forEach(({ name }, i2) => {
75281
75668
  if (stats[i2]?.isFile())
75282
75669
  candidates.add(name);
@@ -75298,7 +75685,7 @@ ${ruleIds.join(`
75298
75685
  for (const layer of layers) {
75299
75686
  for (const file2 of await this.listRuleFilesInLayer(layer.path)) {
75300
75687
  if (!merged.has(file2))
75301
- merged.set(file2, { absolutePath: join15(layer.path, file2), source: layer.id });
75688
+ merged.set(file2, { absolutePath: join16(layer.path, file2), source: layer.id });
75302
75689
  }
75303
75690
  }
75304
75691
  const files = [];
@@ -75366,7 +75753,7 @@ ${ruleIds.join(`
75366
75753
  for (const entry of entries.sort()) {
75367
75754
  if (entry === "presets")
75368
75755
  continue;
75369
- const stat = await this.context.fs.stat(join15(root, entry));
75756
+ const stat = await this.context.fs.stat(join16(root, entry));
75370
75757
  if (stat?.isDirectory())
75371
75758
  dirs.push(entry);
75372
75759
  }
@@ -75374,12 +75761,12 @@ ${ruleIds.join(`
75374
75761
  }
75375
75762
  async listRuleFiles(root, relativeDir) {
75376
75763
  const { fs: fs3 } = this.context;
75377
- const dir = join15(root, relativeDir);
75764
+ const dir = join16(root, relativeDir);
75378
75765
  const entries = await fs3.readDir(dir);
75379
75766
  const files = [];
75380
75767
  for (const entry of entries.sort()) {
75381
- const relativePath2 = relativeDir.length === 0 ? entry : join15(relativeDir, entry);
75382
- const absolutePath = join15(root, relativePath2);
75768
+ const relativePath2 = relativeDir.length === 0 ? entry : join16(relativeDir, entry);
75769
+ const absolutePath = join16(root, relativePath2);
75383
75770
  const stat = await fs3.stat(absolutePath);
75384
75771
  if (stat?.isDirectory()) {
75385
75772
  files.push(...await this.listRuleFiles(root, relativePath2));
@@ -75513,8 +75900,8 @@ var init_rule_service = __esm(() => {
75513
75900
  init_dist10();
75514
75901
  init_agent_execution();
75515
75902
  init_system_event_envelope();
75516
- LOCAL_RULES_DIR = join15(".spur", "rules");
75517
- GLOBAL_RULES_DIR = join15(".config", "spur", "rules");
75903
+ LOCAL_RULES_DIR = join16(".spur", "rules");
75904
+ GLOBAL_RULES_DIR = join16(".config", "spur", "rules");
75518
75905
  SEVERITY_RANK2 = {
75519
75906
  info: 0,
75520
75907
  warning: 1,
@@ -76132,12 +76519,12 @@ class SystemEventEmitter {
76132
76519
  secretValues;
76133
76520
  projectContext;
76134
76521
  quotas;
76135
- constructor(dao2, logger3, retention = {}, secretValues = [], projectContext = systemEventProjectContext("")) {
76522
+ constructor(dao2, logger3, retention2 = {}, secretValues = [], projectContext = systemEventProjectContext("")) {
76136
76523
  this.dao = dao2;
76137
76524
  this.logger = logger3;
76138
76525
  this.secretValues = secretValues;
76139
76526
  this.projectContext = projectContext;
76140
- this.quotas = resolveRetentionQuotas(retention);
76527
+ this.quotas = resolveRetentionQuotas(retention2);
76141
76528
  }
76142
76529
  async emit(event) {
76143
76530
  const entry = systemEventCatalogEntry(event.event);
@@ -76519,7 +76906,7 @@ var init_task_size_precheck = __esm(() => {
76519
76906
  });
76520
76907
 
76521
76908
  // ../../packages/app/src/services/task-service.ts
76522
- import { dirname as dirname14, isAbsolute as isAbsolute4, join as join16, relative as relative4 } from "path";
76909
+ import { dirname as dirname14, isAbsolute as isAbsolute4, join as join17, relative as relative4 } from "path";
76523
76910
  function renderRosterTable(rows) {
76524
76911
  const header = `| WBS | Sub-task | Status |
76525
76912
  | --- | -------- | ------ |`;
@@ -76943,7 +77330,7 @@ class TaskService {
76943
77330
  if (!featureId2 || featureId2.length === 0)
76944
77331
  return [];
76945
77332
  const tasksDir = dirname14(taskFilePath);
76946
- const featuresDir = join16(tasksDir, "..", "features");
77333
+ const featuresDir = join17(tasksDir, "..", "features");
76947
77334
  const featurePath = await (async () => {
76948
77335
  try {
76949
77336
  for (const name of await this.ctx.fs.readDir(featuresDir)) {
@@ -76986,9 +77373,11 @@ class TaskService {
76986
77373
  reviewWritten: false,
76987
77374
  solutionBackfilled: false
76988
77375
  };
76989
- const testingBody = renderTesting(verdict);
76990
- await this.writeService.updateSection(ref, "Testing", testingBody);
76991
- result.testingWritten = true;
77376
+ if (!(verdict.verdict === "UNKNOWN" && !sectionIsBare(doc2, "Testing"))) {
77377
+ const testingBody = renderTesting(verdict);
77378
+ await this.writeService.updateSection(ref, "Testing", testingBody);
77379
+ result.testingWritten = true;
77380
+ }
76992
77381
  if (sectionIsBare(doc2, "Review")) {
76993
77382
  const reviewBody = renderReview(verdict);
76994
77383
  await this.writeService.updateSection(ref, "Review", reviewBody);
@@ -77691,7 +78080,7 @@ var init_task_verdict = __esm(() => {
77691
78080
  });
77692
78081
 
77693
78082
  // ../../packages/app/src/services/team-service.ts
77694
- import { join as join17, resolve as resolve8 } from "path";
78083
+ import { join as join18, resolve as resolve8 } from "path";
77695
78084
 
77696
78085
  class TeamService {
77697
78086
  ctx;
@@ -77699,7 +78088,7 @@ class TeamService {
77699
78088
  orchestratorPromise;
77700
78089
  constructor(ctx) {
77701
78090
  this.ctx = ctx;
77702
- this.configDir = join17(ctx.cwd, ".spur", "agents");
78091
+ this.configDir = join18(ctx.cwd, ".spur", "agents");
77703
78092
  }
77704
78093
  async sendMessage(fromId, toId, body, replyTo) {
77705
78094
  validateAgentId(toId);
@@ -78167,7 +78556,7 @@ class TeamService {
78167
78556
  async resolveTaskFile(taskId) {
78168
78557
  const fs3 = this.ctx.fs;
78169
78558
  const { foldersConfig } = await resolvePlanningFolders(fs3);
78170
- const dirs = [...new Set([foldersConfig.active_folder, ...Object.keys(foldersConfig.folders)])].map((dir) => join17(this.ctx.cwd, dir));
78559
+ const dirs = [...new Set([foldersConfig.active_folder, ...Object.keys(foldersConfig.folders)])].map((dir) => join18(this.ctx.cwd, dir));
78171
78560
  return await TaskLocator.forDirs(fs3, dirs).findPathByWbs(taskId);
78172
78561
  }
78173
78562
  }
@@ -78205,7 +78594,7 @@ var init_team_service = __esm(() => {
78205
78594
 
78206
78595
  // ../../packages/app/src/services/token-ledger-service.ts
78207
78596
  import { closeSync as closeSync3, existsSync as existsSync8, fstatSync, openSync as openSync3, readSync } from "fs";
78208
- import { join as join18 } from "path";
78597
+ import { join as join19 } from "path";
78209
78598
  function isSparseToolActivity(events2) {
78210
78599
  return events2.length === 0 || !events2.some((e) => TOOL_ACTIVITY_TYPES.has(e.type));
78211
78600
  }
@@ -78306,7 +78695,7 @@ class TokenLedgerService {
78306
78695
  ledgerPath;
78307
78696
  chunkSize;
78308
78697
  constructor(options) {
78309
- this.ledgerPath = options.ledgerPath ?? join18(options.cwd, TOKEN_LEDGER_RELATIVE_PATH);
78698
+ this.ledgerPath = options.ledgerPath ?? join19(options.cwd, TOKEN_LEDGER_RELATIVE_PATH);
78310
78699
  this.chunkSize = options.chunkSize ?? DEFAULT_CHUNK;
78311
78700
  }
78312
78701
  get path() {
@@ -78347,7 +78736,7 @@ class TokenLedgerService {
78347
78736
  var TOOL_ACTIVITY_TYPES, TOKEN_LEDGER_DEFAULT_LIMIT = 200, TOKEN_LEDGER_MAX_LIMIT = 1000, TOKEN_LEDGER_RELATIVE_PATH, DEFAULT_CHUNK;
78348
78737
  var init_token_ledger_service = __esm(() => {
78349
78738
  TOOL_ACTIVITY_TYPES = new Set(["read", "write", "bash", "grep", "glob"]);
78350
- TOKEN_LEDGER_RELATIVE_PATH = join18(".spur", "context", "token-ledger.jsonl");
78739
+ TOKEN_LEDGER_RELATIVE_PATH = join19(".spur", "context", "token-ledger.jsonl");
78351
78740
  DEFAULT_CHUNK = 64 * 1024;
78352
78741
  });
78353
78742
 
@@ -78717,7 +79106,7 @@ var init_steering = __esm(() => {
78717
79106
 
78718
79107
  // ../../packages/app/src/workflow/actions/agent-run.ts
78719
79108
  import { tmpdir } from "os";
78720
- import { dirname as dirname15, isAbsolute as isAbsolute5, join as join19 } from "path";
79109
+ import { dirname as dirname15, isAbsolute as isAbsolute5, join as join20 } from "path";
78721
79110
 
78722
79111
  class AgentRunActionRunner {
78723
79112
  observabilityBus;
@@ -78754,11 +79143,11 @@ class AgentRunActionRunner {
78754
79143
  let sessionDir = asOptionalString(context4.vars.__agentSessionDir);
78755
79144
  if (affinityOn) {
78756
79145
  if (!sessionDir || prevAgent && prevAgent !== targetAgentDir) {
78757
- sessionDir = join19(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
79146
+ sessionDir = join20(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
78758
79147
  }
78759
79148
  } else {
78760
79149
  if (!sessionDir) {
78761
- sessionDir = join19(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
79150
+ sessionDir = join20(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
78762
79151
  }
78763
79152
  }
78764
79153
  const storedSessionId = asOptionalString(context4.vars.__agentSessionId);
@@ -78868,13 +79257,13 @@ class AgentRunActionRunner {
78868
79257
  const ok = exitCode === 0;
78869
79258
  const invocation = traced.invocation;
78870
79259
  if (capture && answerFile !== undefined) {
78871
- const target = isAbsolute5(answerFile) ? answerFile : join19(cwd, answerFile);
79260
+ const target = isAbsolute5(answerFile) ? answerFile : join20(cwd, answerFile);
78872
79261
  const fs3 = createNodeFileSystem3(cwd);
78873
79262
  await fs3.ensureDir(dirname15(target));
78874
79263
  await fs3.writeFile(target, answer);
78875
79264
  }
78876
79265
  if (ok && expectFile !== undefined) {
78877
- const target = isAbsolute5(expectFile) ? expectFile : join19(cwd, expectFile);
79266
+ const target = isAbsolute5(expectFile) ? expectFile : join20(cwd, expectFile);
78878
79267
  const fs3 = createNodeFileSystem3(cwd);
78879
79268
  if (!await fs3.exists(target)) {
78880
79269
  return {
@@ -78913,7 +79302,7 @@ class AgentRunActionRunner {
78913
79302
  const resolvedAgent = invocation?.agent ?? targetAgentDir;
78914
79303
  let resolvedSessionDir = sessionDir;
78915
79304
  if (ok && affinityOn && resolvedAgent !== targetAgentDir && !context4.vars.__agentSessionDir) {
78916
- resolvedSessionDir = join19(cwd, ".spur", "run", context4.runId, "agent-sessions", resolvedAgent);
79305
+ resolvedSessionDir = join20(cwd, ".spur", "run", context4.runId, "agent-sessions", resolvedAgent);
78917
79306
  }
78918
79307
  let discoveredSessionId = storedSessionId;
78919
79308
  if (ok && affinityOn && resolvedSessionDir && !discoveredSessionId) {
@@ -78921,7 +79310,7 @@ class AgentRunActionRunner {
78921
79310
  }
78922
79311
  if (ok && affinityOn && resolvedSessionDir) {
78923
79312
  try {
78924
- const sidecarPath = join19(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
79313
+ const sidecarPath = join20(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
78925
79314
  const fs3 = createNodeFileSystem3(cwd);
78926
79315
  await fs3.ensureDir(dirname15(sidecarPath));
78927
79316
  await fs3.writeFile(sidecarPath, JSON.stringify({
@@ -78964,7 +79353,7 @@ async function discoverSessionId(sessionDir) {
78964
79353
  let newestFile;
78965
79354
  let newestMtime = 0;
78966
79355
  for (const entry of jsonEntries) {
78967
- const fullPath = join19(sessionDir, entry);
79356
+ const fullPath = join20(sessionDir, entry);
78968
79357
  const st = await fs3.stat(fullPath);
78969
79358
  if (st?.isFile() && st.mtimeMs > newestMtime) {
78970
79359
  newestMtime = st.mtimeMs;
@@ -79060,7 +79449,7 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
79060
79449
  const headerLine = model !== undefined ? `${agentLabel} (model: ${model})` : agentLabel;
79061
79450
  const inv = traced.invocation;
79062
79451
  const argvLine = inv ? `${inv.command} ${inv.argv.join(" ")}` : "(invocation not captured)";
79063
- const latchedSessionPath = join19(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
79452
+ const latchedSessionPath = join20(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
79064
79453
  const resumeContext = [
79065
79454
  "",
79066
79455
  "## resume context",
@@ -79076,9 +79465,9 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
79076
79465
  try {
79077
79466
  const fs4 = createNodeFileSystem3(cwd);
79078
79467
  const locator = TaskLocator.forDirs(fs4, [
79079
- join19(cwd, "docs", "tasks3"),
79080
- join19(cwd, "docs", "tasks2"),
79081
- join19(cwd, "docs", "tasks")
79468
+ join20(cwd, "docs", "tasks3"),
79469
+ join20(cwd, "docs", "tasks2"),
79470
+ join20(cwd, "docs", "tasks")
79082
79471
  ]);
79083
79472
  const hit = await locator.findByWbs(wbs);
79084
79473
  if (hit) {
@@ -79134,7 +79523,7 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
79134
79523
  completedSection
79135
79524
  ].join(`
79136
79525
  `);
79137
- const target = join19(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
79526
+ const target = join20(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
79138
79527
  const fs3 = createNodeFileSystem3(cwd);
79139
79528
  await fs3.ensureDir(dirname15(target));
79140
79529
  await fs3.writeFile(target, body);
@@ -79156,7 +79545,7 @@ async function gitDiffStat(cwd) {
79156
79545
  }
79157
79546
  }
79158
79547
  async function createGitWorkingTreeSnapshot(cwd, excludeGlobs = ["docs/tasks3/*", "docs/features/*"]) {
79159
- const indexFile = join19(tmpdir(), `spur-implement-scope-${crypto.randomUUID()}.index`);
79548
+ const indexFile = join20(tmpdir(), `spur-implement-scope-${crypto.randomUUID()}.index`);
79160
79549
  const fs3 = createNodeFileSystem3(cwd);
79161
79550
  let keepIndex = false;
79162
79551
  try {
@@ -79287,9 +79676,9 @@ async function findOutOfScopeChanges(cwd, wbs, changed) {
79287
79676
  try {
79288
79677
  const fs3 = createNodeFileSystem3(cwd);
79289
79678
  const locator = TaskLocator.forDirs(fs3, [
79290
- join19(cwd, "docs", "tasks3"),
79291
- join19(cwd, "docs", "tasks2"),
79292
- join19(cwd, "docs", "tasks")
79679
+ join20(cwd, "docs", "tasks3"),
79680
+ join20(cwd, "docs", "tasks2"),
79681
+ join20(cwd, "docs", "tasks")
79293
79682
  ]);
79294
79683
  const hit = await locator.findByWbs(wbs);
79295
79684
  if (!hit)
@@ -79516,9 +79905,10 @@ function parseDoctorJson(stdout) {
79516
79905
  const first = parsed.agents?.[0];
79517
79906
  const auth = typeof first?.authenticated === "string" ? first.authenticated : "unknown";
79518
79907
  const detail = typeof first?.modelStatus?.detail === "string" ? first.modelStatus.detail : "";
79519
- return { auth, detail };
79908
+ const resolvedAgent = typeof first?.agent === "string" && first.agent.length > 0 ? first.agent : "";
79909
+ return { auth, detail, resolvedAgent };
79520
79910
  } catch {
79521
- return { auth: "unknown", detail: "" };
79911
+ return { auth: "unknown", detail: "", resolvedAgent: "" };
79522
79912
  }
79523
79913
  }
79524
79914
 
@@ -79604,18 +79994,20 @@ ${res.stderr}`.trim();
79604
79994
  status = "FAIL";
79605
79995
  continue;
79606
79996
  }
79607
- const { auth, detail } = parseDoctorJson(res.stdout);
79608
- const probe = classifyDoctorProbe(detail, exe);
79609
- const line = `precheck: ${exe} auth=${auth} probe=${probe} ${detail}`.replace(/\s+$/, "");
79997
+ const { auth, detail, resolvedAgent } = parseDoctorJson(res.stdout);
79998
+ const classifyAgainst = resolvedAgent !== "" ? resolvedAgent : exe;
79999
+ const probe = classifyDoctorProbe(detail, classifyAgainst);
80000
+ const resolvedSuffix = resolvedAgent !== "" && resolvedAgent !== exe ? ` (resolved ${resolvedAgent})` : "";
80001
+ const line = `precheck: ${exe}${resolvedSuffix} auth=${auth} probe=${probe} ${detail}`.replace(/\s+$/, "");
79610
80002
  lines.push(line);
79611
80003
  emit(line);
79612
80004
  if (auth === "unauthenticated") {
79613
- if (RELAY_FAMILY.test(exe) && (probe === "env-miss" || probe === "unknown")) {
79614
- const soft = `precheck: SOFT - executor ${exe} auth probe cannot see agent-owned credentials`;
80005
+ if (RELAY_FAMILY.test(classifyAgainst) && (probe === "env-miss" || probe === "unknown")) {
80006
+ const soft = `precheck: SOFT - executor ${classifyAgainst} auth probe cannot see agent-owned credentials`;
79615
80007
  lines.push(soft);
79616
80008
  emit(soft);
79617
80009
  } else {
79618
- const hard = `precheck: FAIL - executor ${exe} is unauthenticated; fix agent.default or pass --vars '{"agent":"<authenticated-executor>"}' (${spurBin} agent doctor ${exe} --json); ${detail}`;
80010
+ const hard = `precheck: FAIL - executor ${classifyAgainst} is unauthenticated; fix agent.default or pass --vars '{"agent":"<authenticated-executor>"}' (${spurBin} agent doctor ${classifyAgainst} --json); ${detail}`;
79619
80011
  lines.push(hard);
79620
80012
  emit(hard);
79621
80013
  status = "FAIL";
@@ -80270,9 +80662,9 @@ var init_composition_baseline = __esm(() => {
80270
80662
  // ../../packages/app/src/workflow/proof-input-fingerprint.ts
80271
80663
  import crypto4 from "crypto";
80272
80664
  import { tmpdir as tmpdir2 } from "os";
80273
- import { dirname as dirname16, join as join20 } from "path";
80665
+ import { dirname as dirname16, join as join21 } from "path";
80274
80666
  async function createGitAlternateTree(cwd, excludeGlobs = DEFAULT_EXCLUDE_GLOBS, executor = new NodeProcessExecutor3, fs3 = createNodeFileSystem3()) {
80275
- const indexFile = join20(tmpdir2(), `spur-proof-fingerprint-${crypto4.randomUUID()}.index`);
80667
+ const indexFile = join21(tmpdir2(), `spur-proof-fingerprint-${crypto4.randomUUID()}.index`);
80276
80668
  try {
80277
80669
  await fs3.ensureDir(dirname16(indexFile));
80278
80670
  const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
@@ -80818,8 +81210,9 @@ var init_builtins2 = __esm(() => {
80818
81210
  });
80819
81211
 
80820
81212
  // ../../packages/app/src/services/workflow-service.ts
81213
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
80821
81214
  import { homedir as homedir7 } from "os";
80822
- import { dirname as dirname17, join as join21, resolve as resolve13 } from "path";
81215
+ import { basename as basename7, dirname as dirname17, join as join22, resolve as resolve13 } from "path";
80823
81216
  function signalSubprocess(pid) {
80824
81217
  if (!Number.isInteger(pid) || pid <= 1)
80825
81218
  return false;
@@ -80955,8 +81348,9 @@ class WorkflowAppService {
80955
81348
  if (roleErrors.length > 0) {
80956
81349
  return { ok: false, valid: false, file: file2, errors: roleErrors };
80957
81350
  }
81351
+ const composition = collectCompositionAdvisory(workflow, absolute);
80958
81352
  await this.loadWorkflowExtensions(createDefaultWorkflowEngineHost(), workflow, absolute);
80959
- return { ok: true, valid: true, workflow };
81353
+ return { ok: true, valid: true, workflow, composition };
80960
81354
  } catch (error51) {
80961
81355
  return {
80962
81356
  ok: false,
@@ -81073,7 +81467,7 @@ class WorkflowAppService {
81073
81467
  };
81074
81468
  }
81075
81469
  async cleanRunLogs(retentionDays = 30, dryRun = false) {
81076
- const runDir = join21(this.ctx.cwd, ".spur", "run");
81470
+ const runDir = join22(this.ctx.cwd, ".spur", "run");
81077
81471
  const fs3 = createNodeFileSystem3();
81078
81472
  const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
81079
81473
  const reclaimed = [];
@@ -81085,7 +81479,7 @@ class WorkflowAppService {
81085
81479
  return { retentionDays, dryRun, reclaimed, failures };
81086
81480
  }
81087
81481
  for (const name of entries) {
81088
- const path9 = join21(runDir, name);
81482
+ const path9 = join22(runDir, name);
81089
81483
  const stat = await fs3.stat(path9);
81090
81484
  if (stat === null || !stat.isFile() || stat.mtimeMs >= cutoffMs)
81091
81485
  continue;
@@ -81169,7 +81563,7 @@ class WorkflowAppService {
81169
81563
  }
81170
81564
  async list(workflowPaths = [".spur/workflows/"]) {
81171
81565
  const projectRoot = this.ctx.cwd;
81172
- const globalRoot = join21(homedir7(), ".config", "spur");
81566
+ const globalRoot = join22(homedir7(), ".config", "spur");
81173
81567
  const layers = [];
81174
81568
  const entries = [];
81175
81569
  const scannedPaths = new Set;
@@ -81443,6 +81837,100 @@ function collectAgentRunRoleViolations(def) {
81443
81837
  }
81444
81838
  return violations;
81445
81839
  }
81840
+ function collectCompositionAdvisory(def, workflowFile) {
81841
+ const findings = [];
81842
+ let suppressed = 0;
81843
+ const shellLines = (command) => command.split(/\n|;/).map((u2) => u2.trim()).filter((u2) => u2.length > 0 && !u2.startsWith("#")).length;
81844
+ const agentRunSeverity = (prompt) => {
81845
+ if (prompt.length < 200)
81846
+ return "low";
81847
+ if (prompt.length <= 1000)
81848
+ return "medium";
81849
+ return "high";
81850
+ };
81851
+ const baseline = loadCompositionBaselineFor(workflowFile);
81852
+ const workflowName = basename7(workflowFile, ".yaml");
81853
+ const adjudicated = (actionKey) => {
81854
+ if (!baseline)
81855
+ return false;
81856
+ const entry = baseline.workflows[workflowName];
81857
+ if (!entry)
81858
+ return false;
81859
+ const action = entry.actions[actionKey];
81860
+ return action?.disposition !== undefined;
81861
+ };
81862
+ const visitAction = (stateId, action, idx, phase) => {
81863
+ const actionKey = `${stateId}:${phase}:${idx}`;
81864
+ if (action.kind === "shell") {
81865
+ const cmd = action.options?.command;
81866
+ if (typeof cmd !== "string" || cmd.length === 0)
81867
+ return;
81868
+ const measured = shellLines(cmd);
81869
+ if (measured < 6)
81870
+ return;
81871
+ if (adjudicated(actionKey)) {
81872
+ suppressed += 1;
81873
+ return;
81874
+ }
81875
+ findings.push({
81876
+ workflow: workflowName,
81877
+ state: stateId,
81878
+ actionKey,
81879
+ measure: { kind: "shell-lines", measured, threshold: 6 },
81880
+ recommendation: `shell action at ${actionKey} measures ${measured} lines (>5 frozen threshold, ADR-069) \u2014 extract to a script or record a disposition in config/workflow-composition-baseline.json`
81881
+ });
81882
+ } else if (action.kind === "agent.run") {
81883
+ const input = action.options?.input;
81884
+ if (typeof input !== "string" || input.length === 0)
81885
+ return;
81886
+ if (input.trimStart().startsWith("/"))
81887
+ return;
81888
+ if (adjudicated(actionKey)) {
81889
+ suppressed += 1;
81890
+ } else {
81891
+ const severity = agentRunSeverity(input);
81892
+ findings.push({
81893
+ workflow: workflowName,
81894
+ state: stateId,
81895
+ actionKey,
81896
+ measure: { kind: "agent-run-chars", measured: input.length, severity },
81897
+ recommendation: `agent.run prompt at ${actionKey} is ${input.length} chars, not slash-pinned (severity ${severity}) \u2014 pin to a slash command or a script with a bounded prompt`
81898
+ });
81899
+ }
81900
+ }
81901
+ };
81902
+ if (def.kind === "transition-flow" || def.kind === undefined) {
81903
+ const flowDef = def;
81904
+ for (const node of flowDef.nodes ?? []) {
81905
+ if (node.action)
81906
+ visitAction(node.id, node.action, 0, "onEnter");
81907
+ }
81908
+ } else {
81909
+ const smDef = def;
81910
+ for (const state of smDef.states ?? []) {
81911
+ for (const [i2, action] of (state.onEnter ?? []).entries())
81912
+ visitAction(state.id, action, i2, "onEnter");
81913
+ for (const [i2, action] of (state.onExit ?? []).entries())
81914
+ visitAction(state.id, action, i2, "onExit");
81915
+ }
81916
+ }
81917
+ return { findings, suppressed };
81918
+ }
81919
+ function loadCompositionBaselineFor(workflowFile) {
81920
+ let dir = dirname17(resolve13(workflowFile));
81921
+ for (let i2 = 0;i2 < 10; i2++) {
81922
+ const candidate = join22(dir, "config/workflow-composition-baseline.json");
81923
+ if (existsSync10(candidate)) {
81924
+ try {
81925
+ return JSON.parse(readFileSync10(candidate, "utf8"));
81926
+ } catch {
81927
+ return;
81928
+ }
81929
+ }
81930
+ dir = dirname17(dir);
81931
+ }
81932
+ return;
81933
+ }
81446
81934
  async function fileExists(path9) {
81447
81935
  const fs3 = createNodeFileSystem3();
81448
81936
  return await fs3.exists(path9);
@@ -81495,8 +81983,8 @@ async function resolveOutputLogConfig(cwd) {
81495
81983
  }
81496
81984
  }
81497
81985
  async function outputArtifactForRun(cwd, runId) {
81498
- const relative5 = join21(".spur", "run", `${runId}.log`);
81499
- return await fileExists(join21(cwd, relative5)) ? relative5 : undefined;
81986
+ const relative5 = join22(".spur", "run", `${runId}.log`);
81987
+ return await fileExists(join22(cwd, relative5)) ? relative5 : undefined;
81500
81988
  }
81501
81989
  async function scanWorkflowFiles(rootPath, source) {
81502
81990
  const entries = [];
@@ -81671,7 +82159,7 @@ function projectActionTraceResult(resultJson, secretValues = []) {
81671
82159
  async function partialArtifactForAction(cwd, runId, node, ok) {
81672
82160
  if (ok !== false || !TRACE_IDENTIFIER.test(runId) || !TRACE_IDENTIFIER.test(node))
81673
82161
  return;
81674
- const relativePath2 = join21(".spur", "run", `${runId}-${node}-partial.md`);
82162
+ const relativePath2 = join22(".spur", "run", `${runId}-${node}-partial.md`);
81675
82163
  return await fileExists(resolve13(cwd, relativePath2)) ? relativePath2 : undefined;
81676
82164
  }
81677
82165
  var TASK_PIPELINE_WORKFLOW = "task-pipeline", EMBEDDED_SCHEMA_PREFIX = "\x00embedded-spur", SPUR_SCHEMA_MANIFEST = "@gobing-ai/spur/package.json", PIPELINE_LINK_KIND = "pipeline", TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
@@ -81903,7 +82391,8 @@ class LifecycleAdapter {
81903
82391
  const db2 = await this.opts.getDb();
81904
82392
  const host = createDefaultWorkflowEngineHost();
81905
82393
  host.registerGuard(new EnvShellGuardRunner(new NodeProcessExecutor3), "builtin");
81906
- const svc = new WorkflowService(host, new DbWorkflowPersistenceAdapter(db2));
82394
+ const persistence2 = new DbWorkflowPersistenceAdapter(db2);
82395
+ const svc = new WorkflowService(host, persistence2);
81907
82396
  const workflow = this.bindGuardVar(await this.loadWorkflow(), ref.id);
81908
82397
  const externalKey = `${profile.entityPrefix}:${ref.id}`;
81909
82398
  const now2 = new Date().toISOString();
@@ -81963,6 +82452,8 @@ class LifecycleAdapter {
81963
82452
  await svc.reseedRun(workflow, runId, currentStatus);
81964
82453
  const result = await svc.requestTransition(workflow, runId, to, { workdir: this.opts.cwd });
81965
82454
  if (result.allowed) {
82455
+ const terminalStatus = to === "cancelled" ? "failed" : to === "done" ? "done" : null;
82456
+ await persistence2.finalizeRun(runId, terminalStatus ?? "running", new Date().toISOString());
81966
82457
  return { allowed: true, from: result.fromState, to: result.toState };
81967
82458
  }
81968
82459
  return {
@@ -82385,7 +82876,7 @@ var init_progress_follow = __esm(() => {
82385
82876
  });
82386
82877
 
82387
82878
  // ../../packages/app/src/workflow/trace-writer.ts
82388
- import { dirname as dirname18, join as join22 } from "path";
82879
+ import { dirname as dirname18, join as join23 } from "path";
82389
82880
 
82390
82881
  class WorkflowTraceWriter {
82391
82882
  path;
@@ -82393,7 +82884,7 @@ class WorkflowTraceWriter {
82393
82884
  pending = Promise.resolve();
82394
82885
  constructor(cwd, runId) {
82395
82886
  const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
82396
- this.path = join22(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
82887
+ this.path = join23(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
82397
82888
  }
82398
82889
  attach(bus) {
82399
82890
  bus.on("workflow.run.started", (event) => this.enqueue("workflow.run.started", event));
@@ -90010,25 +90501,114 @@ async function attachSystemEventLedger(bus, context4, options = {}) {
90010
90501
  };
90011
90502
  }
90012
90503
 
90504
+ // src/commands/shared-options.ts
90505
+ var SHARED_OPTIONS = {
90506
+ json: ["--json", "Output machine-readable JSON"],
90507
+ jsonSupported: ["--json", "Output machine-readable JSON where supported"],
90508
+ section: ["--section <name>", "Section name to replace"],
90509
+ fromFile: ["--from-file <path>", "File to read section body from (requires --section)"],
90510
+ statusFilter: ["--status <s>", "Filter by status"],
90511
+ last: ["--last <n>", "Limit results (default 20)"],
90512
+ noSchema: ["--no-schema", "Skip schema validation"],
90513
+ since: ["--since <iso-date>", "Filter runs started on or after this date"],
90514
+ timeout: ["--timeout <ms>", "Caller deadline in milliseconds"],
90515
+ folderTasks: ["--folder <path>", "Custom tasks folder"],
90516
+ folderFeatures: ["--folder <path>", "Custom features folder"],
90517
+ agentIdMessage: ["--agent <id>", "Agent id"],
90518
+ agentIdWatch: ["--agent <id>", "Agent id to watch"],
90519
+ agentIdLegacyRecipient: ["--agent <id>", "Agent spec id / message recipient (legacy \u2014 prefer --spec)"],
90520
+ cwdServe: ["--cwd <path>", "Working directory"],
90521
+ cwdAgent: ["--cwd <path>", "Working directory for agent execution"],
90522
+ modeAgent: ["--mode <mode>", "Agent output mode: text|json"],
90523
+ modeHistory: ["--mode <mode>", "full|incremental|force-file"],
90524
+ nameAgent: ["--name <name>", "Agent name"],
90525
+ nameProjectDisplay: ["--name <name>", "Display name for the project"],
90526
+ nameProjectInit: ["--name <name>", "Project name (default: current directory name)"],
90527
+ pollWorkflow: ["--poll <ms>", "Follow polling interval in milliseconds"],
90528
+ pollAgent: ["--poll <ms>", "Idle poll interval in milliseconds"],
90529
+ portProjects: ["--port <n>", "Explicit port to bind"],
90530
+ portServe: ["--port <n>", "Server port (env: PORT, default: 3000)"],
90531
+ priorityFilter: ["--priority <p>", "Filter by priority"],
90532
+ prioritySet: ["--priority <p>", "Set the priority frontmatter field (P0\u2013P3)"],
90533
+ runHistory: ["--run <runId>", "Narrow to a single workflow run id"],
90534
+ runAgentPin: ["--run <runId>", "Pin a specific run id (default: spec latest run)"],
90535
+ runIdTask: ["--run-id <id>", "Explicit run_id (auto-generated when omitted)"],
90536
+ runIdWorkflow: ["--run-id <id>", "Persisted run id for workflow run"],
90537
+ sourceLink: ["--source <source>", "Link source identifier (e.g. next-auto)"],
90538
+ sourceHistory: [
90539
+ "--source <source>",
90540
+ "pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all"
90541
+ ],
90542
+ statusDoneFailed: ["--status <status>", "Filter by status: done, failed"],
90543
+ statusDoneFailedRunning: ["--status <status>", "Filter by status: done, failed, running"],
90544
+ untilAgent: ["--until <state>", "Lifecycle state to wait for (repeatable OR)"],
90545
+ untilMessage: ["--until <state>", "Wait target for --wait: injected|invoke-exit"],
90546
+ verboseWorkflow: ["--verbose", "Include transitions and correlation diagnostics in human progress"],
90547
+ verboseRule: ["--verbose", "Stream per-rule progress to stderr"],
90548
+ jsonArtifact: ["--json", "Emit the artifact as JSON instead of the human summary"],
90549
+ jsonDaily: ["--json", "Emit the daily result as JSON"],
90550
+ jsonParsedArtifact: ["--json", "Emit the parsed artifact as JSON instead of the human report"],
90551
+ jsonProjectsArray: ["--json", "Output JSON array of projects"],
90552
+ jsonProjectsResponse: ["--json", "Output JSON response"],
90553
+ jsonMessageStream: ["--json", "Output one JSON object per new message (machine-consumable)"],
90554
+ jsonServePortUrl: ["--json", "Output { port, url, pid } and exit"],
90555
+ fromFileOutcomeRows: ["--from-file <path>", "Path to JSON array of {wbs,outcome[,reason]} rows"],
90556
+ asFeature0418: [
90557
+ "--as <status>",
90558
+ "Evaluate the one-active-goal rule as if the feature were in <status> (0418: lifecycle FSM guards pass the transition target)"
90559
+ ],
90560
+ asTaskF92: [
90561
+ "--as <status>",
90562
+ "Evaluate the task AS if it were in <status> (F92 R2): the lifecycle guards pass the transition target so testing\u2192done checks the done row. Validate against canonical task statuses. Omitted \u2192 current-status diagnostics."
90563
+ ],
90564
+ dryRunFeatureMap: ["--dry-run", "Show the old\u2192new ID map + affected tasks without writing"],
90565
+ dryRunFeatureSync: ["--dry-run", "Report proposed status sync transitions without applying"],
90566
+ dryRunHistoryScan: ["--dry-run", "Scan without persisting imported records"],
90567
+ dryRunRuleFix: ["--dry-run", "Preview fixes without writing (use with --fix-mode auto)"],
90568
+ dryRunTaskReport: ["--dry-run", "Produce the full report without writing files"],
90569
+ dryRunWorkflowValidate: ["--dry-run", "Validate and walk transitions without executing actions"],
90570
+ dryRunWorkflowClean: ["--dry-run", "List what would be cleaned without writing (applies to both scopes)"],
90571
+ featureTrace: ["--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation"],
90572
+ featureFrontmatter: ["--feature <id>", "Set the feature_id frontmatter field (traceability edge)"],
90573
+ featureFilterEdge: ["--feature <id>", "Filter by linked feature ID (feature_id edge)"],
90574
+ featureTasksRewrite: [
90575
+ "--feature <id>",
90576
+ "Restrict the ## Tasks rewrite to one feature (INDEX.md still regenerated)"
90577
+ ],
90578
+ fileHistoryJsonl: ["--file <path>", "Import one JSONL file (single-source only)"],
90579
+ fileRuleAdhoc: ["--file <path>", "Ad-hoc rule file"],
90580
+ fileRuleAdhocPath: ["--file <path>", "Ad-hoc rule file path"],
90581
+ fileTaskBatch: ["--file <path>", "Path to the batch JSON file validated against task-batch.schema.json"],
90582
+ fileTaskTest: ["--file <path>", "Custom target test file path"],
90583
+ forceAgentDelete: ["--force", "Required for delete"],
90584
+ forceFeatureReopen: ["--force", "Force applying reopen proposals without confirmation"],
90585
+ forceInitRecreate: ["--force", "Recreate files that already exist"],
90586
+ forceWorkflowClean: ["--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)"],
90587
+ strictFeature: ["--strict", "Elevate warnings to failures"],
90588
+ strictTaskAll: ["--strict", "Elevate ALL warnings to failures"],
90589
+ strictTaskPath: ["--strict", "Match only the exact corpus path (no basename-WBS fallback)"]
90590
+ };
90591
+ var SHARED_OPTION_FLAGS = new Set(Object.values(SHARED_OPTIONS).map(([flags]) => flags));
90592
+
90013
90593
  // src/commands/agent.ts
90014
90594
  function registerAgentCommand(program2, context4) {
90015
90595
  const agent = program2.command("agent").summary("run and inspect supported coding agents");
90016
- agent.command("list").description("List detected coding agents, or team agent specs with --specs.").option("--json", "Output machine-readable JSON").option("--specs", "List team specs instead of detected agents").action(async (options) => {
90596
+ agent.command("list").description("List detected coding agents, or team agent specs with --specs.").option(...SHARED_OPTIONS.json).option("--specs", "List team specs instead of detected agents").action(async (options) => {
90017
90597
  const svc = new AgentService({ cwd: context4.cwd, env: context4.env, output: context4.output });
90018
90598
  const code = await runAgentList(svc, context4, { json: options.json, specs: options.specs });
90019
90599
  context4.setExitCode(code);
90020
90600
  });
90021
- agent.command("doctor").description("Check agent readiness.").option("--json", "Output machine-readable JSON").argument("[agent]", "Agent to check").action(async (agentName, options) => {
90601
+ agent.command("doctor").description("Check agent readiness.").option(...SHARED_OPTIONS.json).argument("[agent]", "Agent to check").action(async (agentName, options) => {
90022
90602
  const svc = context4.agentService();
90023
90603
  const code = await svc.doctor({ json: options.json === true, agent: agentName }, undefined);
90024
90604
  context4.setExitCode(code);
90025
90605
  });
90026
- agent.command("run").description("Execute a prompt or slash command via a coding agent.").option("--agent <name>", "Role, executor, agent binary, auto, or inline (host-session-only; errors on headless surfaces)").option("--spec <id>", "Team agent spec id (occupant addressing; pairs with --drain)").option("--continue", "Resume the previous agent session").option("--model <name>", "Agent model argument").option("--mode <mode>", "Agent output mode: text|json").option("--cwd <path>", "Working directory for agent execution").option("--json", "Output machine-readable JSON where supported").option("--drain", "Prepend pending inbox messages for --spec <id>").argument("<prompt>", "The prompt or slash command to execute").action(async (prompt, options) => {
90606
+ agent.command("run").description("Execute a prompt or slash command via a coding agent.").option("--agent <name>", "Role, executor, agent binary, auto, or inline (host-session-only; errors on headless surfaces)").option("--spec <id>", "Team agent spec id (occupant addressing; pairs with --drain)").option("--continue", "Resume the previous agent session").option("--model <name>", "Agent model argument").option(...SHARED_OPTIONS.modeAgent).option(...SHARED_OPTIONS.cwdAgent).option(...SHARED_OPTIONS.jsonSupported).option("--drain", "Prepend pending inbox messages for --spec <id>").argument("<prompt>", "The prompt or slash command to execute").action(async (prompt, options) => {
90027
90607
  const flags = commanderOptionsToFlags(options);
90028
90608
  const code = await runAgentRun(prompt, context4, flags);
90029
90609
  context4.setExitCode(code);
90030
90610
  });
90031
- agent.command("loop").description("Run the persistent self-draining loop for a team member (used by the supervisor).").option("--spec <id>", "Agent spec id / message recipient").option("--agent <id>", "Agent spec id / message recipient (legacy \u2014 prefer --spec)").option("--poll <ms>", "Idle poll interval in milliseconds", String(DEFAULT_LOOP_POLL_MS)).action(async (options) => {
90611
+ agent.command("loop").description("Run the persistent self-draining loop for a team member (used by the supervisor).").option("--spec <id>", "Agent spec id / message recipient").option(...SHARED_OPTIONS.agentIdLegacyRecipient).option(...SHARED_OPTIONS.pollAgent, String(DEFAULT_LOOP_POLL_MS)).action(async (options) => {
90032
90612
  const controller = new AbortController;
90033
90613
  const onSignal = () => controller.abort();
90034
90614
  process.on("SIGINT", onSignal);
@@ -90042,11 +90622,11 @@ function registerAgentCommand(program2, context4) {
90042
90622
  process.off("SIGTERM", onSignal);
90043
90623
  }
90044
90624
  });
90045
- agent.command("wait").description("Wait for a pinned occupant run to reach a lifecycle state (G4 wave 2).").argument("<specId>", "Agent spec id whose occupant to wait on").option("--run <runId>", "Pin a specific run id (default: spec latest run)").option("--until <state>", "Lifecycle state to wait for (repeatable OR)", collectUntil, []).option("--timeout <ms>", "Caller deadline in milliseconds", parseTimeout).option("--json", "Output machine-readable JSON").action(async (specId, options) => {
90625
+ agent.command("wait").description("Wait for a pinned occupant run to reach a lifecycle state (G4 wave 2).").argument("<specId>", "Agent spec id whose occupant to wait on").option(...SHARED_OPTIONS.runAgentPin).option(...SHARED_OPTIONS.untilAgent, collectUntil, []).option(...SHARED_OPTIONS.timeout, parseTimeout).option(...SHARED_OPTIONS.json).action(async (specId, options) => {
90046
90626
  const code = await runAgentWait(context4, specId, options);
90047
90627
  context4.setExitCode(code);
90048
90628
  });
90049
- agent.command("create").description("Write a team agent spec to .spur/agents/<id>.yaml.").option("--type <agent-type>", "Agent spec type for create").option("--tags <a,b>", "Team identity tags").option("--system-prompt <text>", "Team identity system prompt").option("--name <name>", "Agent name").option("--workspace <path>", "Workspace path").option("--purpose <text>", "Team identity purpose").option("--auto-start", "Auto-start flag").option("--model <name>", "Agent model argument").option("--autonomy <level>", "Autonomy level").option("--no-identity-preamble", "Disable identity preamble").option("--json", "Output machine-readable JSON").argument("<id>", "Agent spec id").action(async (id, options) => {
90629
+ agent.command("create").description("Write a team agent spec to .spur/agents/<id>.yaml.").option("--type <agent-type>", "Agent spec type for create").option("--tags <a,b>", "Team identity tags").option("--system-prompt <text>", "Team identity system prompt").option(...SHARED_OPTIONS.nameAgent).option("--workspace <path>", "Workspace path").option("--purpose <text>", "Team identity purpose").option("--auto-start", "Auto-start flag").option("--model <name>", "Agent model argument").option("--autonomy <level>", "Autonomy level").option("--no-identity-preamble", "Disable identity preamble").option(...SHARED_OPTIONS.json).argument("<id>", "Agent spec id").action(async (id, options) => {
90050
90630
  const flags = commanderOptionsToFlags(options);
90051
90631
  const code = await runAgentCreate(id, context4, flags);
90052
90632
  context4.setExitCode(code);
@@ -90055,7 +90635,7 @@ function registerAgentCommand(program2, context4) {
90055
90635
  const code = await runAgentEdit(id, context4);
90056
90636
  context4.setExitCode(code);
90057
90637
  });
90058
- agent.command("delete").description("Remove an agent spec.").option("--force", "Required for delete").argument("<id>", "Agent spec id").action(async (id, options) => {
90638
+ agent.command("delete").description("Remove an agent spec.").option(...SHARED_OPTIONS.forceAgentDelete).argument("<id>", "Agent spec id").action(async (id, options) => {
90059
90639
  const flags = commanderOptionsToFlags(options);
90060
90640
  const code = await runAgentDelete(id, context4, flags);
90061
90641
  context4.setExitCode(code);
@@ -90457,6 +91037,533 @@ function waitFail(context4, options, code, message) {
90457
91037
  return 1;
90458
91038
  }
90459
91039
 
91040
+ // src/release-ops.ts
91041
+ init_dist5();
91042
+ import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
91043
+ import { join as join24 } from "path";
91044
+ var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
91045
+ function releaseTag(config4, version3) {
91046
+ return `${config4.packageName}${config4.tagVersionSeparator}${version3}`;
91047
+ }
91048
+ async function run(repoRoot, cmd) {
91049
+ const executor = new NodeProcessExecutor3({ defaultTimeout: 30000, defaultMaxOutput: 512000 });
91050
+ const result = await executor.run({
91051
+ command: cmd[0] ?? "",
91052
+ args: cmd.slice(1),
91053
+ cwd: repoRoot,
91054
+ forceBuffered: true
91055
+ });
91056
+ return {
91057
+ ok: result.exitCode === 0,
91058
+ stdout: result.stdout.toString().trim(),
91059
+ stderr: result.stderr.toString().trim()
91060
+ };
91061
+ }
91062
+ async function git2(repoRoot, args) {
91063
+ const result = await run(repoRoot, ["git", ...args]);
91064
+ if (!result.ok) {
91065
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
91066
+ }
91067
+ return result.stdout;
91068
+ }
91069
+ async function npmViewVersion(repoRoot, name, version3) {
91070
+ const result = await run(repoRoot, ["npm", "view", `${name}@${version3}`, "version"]);
91071
+ return result.ok && result.stdout === version3;
91072
+ }
91073
+ async function readJson(path9) {
91074
+ const file2 = Bun.file(path9);
91075
+ if (!await file2.exists())
91076
+ return null;
91077
+ try {
91078
+ const parsed = await file2.json();
91079
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
91080
+ return null;
91081
+ return parsed;
91082
+ } catch {
91083
+ return null;
91084
+ }
91085
+ }
91086
+ function rootWorkspaceGlobs(repoRoot) {
91087
+ let root;
91088
+ try {
91089
+ root = JSON.parse(readFileSync11(join24(repoRoot, "package.json"), "utf8"));
91090
+ } catch {
91091
+ return [];
91092
+ }
91093
+ if (root === null || typeof root !== "object")
91094
+ return [];
91095
+ const raw = root.workspaces;
91096
+ const rawPackages = raw.packages;
91097
+ const list = Array.isArray(raw) ? raw : Array.isArray(rawPackages) ? rawPackages : [];
91098
+ return list.filter((g) => typeof g === "string");
91099
+ }
91100
+ function workspaceDirs(repoRoot) {
91101
+ const dirs = [];
91102
+ for (const glob of rootWorkspaceGlobs(repoRoot)) {
91103
+ if (!glob.endsWith("/*"))
91104
+ continue;
91105
+ const parent = glob.slice(0, -2);
91106
+ const parentAbs = join24(repoRoot, parent);
91107
+ if (!existsSync11(parentAbs))
91108
+ continue;
91109
+ for (const entry of readdirSync7(parentAbs, { withFileTypes: true })) {
91110
+ if (entry.isDirectory() && existsSync11(join24(parentAbs, entry.name, "package.json"))) {
91111
+ dirs.push(`${parent}/${entry.name}`);
91112
+ }
91113
+ }
91114
+ }
91115
+ return dirs;
91116
+ }
91117
+ async function releaseContext(repoRoot) {
91118
+ const rootManifest = await readJson(join24(repoRoot, "package.json"));
91119
+ if (rootManifest === null) {
91120
+ throw new Error(`no package.json found at ${repoRoot} \u2014 builder operates on a package workspace root.`);
91121
+ }
91122
+ const rootName = typeof rootManifest.name === "string" ? rootManifest.name : "root";
91123
+ const workspaces2 = [];
91124
+ for (const dir of workspaceDirs(repoRoot)) {
91125
+ const manifest = await readJson(join24(repoRoot, dir, "package.json"));
91126
+ if (manifest === null)
91127
+ continue;
91128
+ workspaces2.push({
91129
+ dir,
91130
+ name: typeof manifest.name === "string" ? manifest.name : dir,
91131
+ version: typeof manifest.version === "string" ? manifest.version : "0.0.0",
91132
+ manifest
91133
+ });
91134
+ }
91135
+ const shortName = (name) => name.split("/").pop() ?? name;
91136
+ const packages = new Map;
91137
+ for (const ws of workspaces2) {
91138
+ const versionSourceFile = existsSync11(join24(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
91139
+ packages.set(shortName(ws.name), {
91140
+ packageDir: ws.dir,
91141
+ packageName: ws.name,
91142
+ versionSourceFile,
91143
+ tagVersionSeparator: "-v",
91144
+ publishWorkflow: "publish.yml",
91145
+ releaseCommitType: "chore",
91146
+ releaseCommitScope: "release",
91147
+ releaseCommitSubject: (version3) => `bump ${shortName(ws.name)} to ${version3}`,
91148
+ releaseTagMessage: (tag) => `release: ${tag}`,
91149
+ ghRunListLimit: 5
91150
+ });
91151
+ }
91152
+ const pinnedNames = new Set;
91153
+ for (const ws of workspaces2) {
91154
+ for (const field2 of ["dependencies", "devDependencies", "peerDependencies"]) {
91155
+ const deps = ws.manifest[field2];
91156
+ if (deps === null || typeof deps !== "object" || Array.isArray(deps))
91157
+ continue;
91158
+ for (const [dep, spec] of Object.entries(deps)) {
91159
+ if (typeof spec === "string" && spec.startsWith("workspace:") && dep !== ws.name) {
91160
+ pinnedNames.add(dep);
91161
+ }
91162
+ }
91163
+ }
91164
+ }
91165
+ pinnedNames.add(rootName);
91166
+ const allPackages = [...packages.values()].filter((c3) => pinnedNames.has(c3.packageName));
91167
+ return { repoRoot, rootName, packages, allPackages };
91168
+ }
91169
+ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2) {
91170
+ const changed = [];
91171
+ for (const dir of workspaceDirs(ctx.repoRoot)) {
91172
+ const relPath = `${dir}/package.json`;
91173
+ const manifest = await readJson(join24(ctx.repoRoot, relPath));
91174
+ if (manifest === null)
91175
+ continue;
91176
+ let dirty = false;
91177
+ const pin = `workspace:${oldVersion}`;
91178
+ const nextPin = `workspace:${newVersion}`;
91179
+ for (const field2 of ["dependencies", "devDependencies", "peerDependencies"]) {
91180
+ const deps = manifest[field2];
91181
+ if (deps === null || typeof deps !== "object" || Array.isArray(deps))
91182
+ continue;
91183
+ const map2 = deps;
91184
+ if (map2[pkgName] === pin) {
91185
+ map2[pkgName] = nextPin;
91186
+ dirty = true;
91187
+ }
91188
+ }
91189
+ if (dirty) {
91190
+ await Bun.write(join24(ctx.repoRoot, relPath), `${JSON.stringify(manifest, null, 4)}
91191
+ `);
91192
+ output2.write(` \u21B3 ${relPath}: ${pkgName} workspace pin ${oldVersion} \u2192 ${newVersion}`);
91193
+ changed.push(relPath);
91194
+ }
91195
+ }
91196
+ return changed;
91197
+ }
91198
+ async function updateVersionSourceFile(ctx, filePath, previous, next, output2) {
91199
+ const absPath = join24(ctx.repoRoot, filePath);
91200
+ const content = await Bun.file(absPath).text();
91201
+ const updated = content.replace(/(binaryVersion:\s*['"])\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?(['"])/, `$1${next}$2`);
91202
+ if (updated === content) {
91203
+ output2.write(` \u26A0 ${filePath}: binaryVersion pattern not found (expected ${previous})`);
91204
+ return false;
91205
+ }
91206
+ await Bun.write(absPath, updated);
91207
+ output2.write(` \u21B3 ${filePath}: binaryVersion ${previous} \u2192 ${next}`);
91208
+ return true;
91209
+ }
91210
+ async function syncMarketplaceAndPlugins(ctx, version3, staged, output2) {
91211
+ const marketplacePath = join24(ctx.repoRoot, ".claude-plugin", "marketplace.json");
91212
+ const marketplaceFile = Bun.file(marketplacePath);
91213
+ if (!await marketplaceFile.exists())
91214
+ return;
91215
+ const marketplace = await readJson(marketplacePath);
91216
+ const rawPlugins = marketplace?.plugins;
91217
+ if (marketplace === null || !Array.isArray(rawPlugins))
91218
+ return;
91219
+ const isPlugin = (entry) => entry !== null && typeof entry === "object" && typeof entry.name === "string" && typeof entry.version === "string" && typeof entry.source === "string";
91220
+ const plugins = [];
91221
+ for (const entry of rawPlugins) {
91222
+ if (isPlugin(entry)) {
91223
+ plugins.push({ name: entry.name, version: entry.version, source: entry.source });
91224
+ }
91225
+ }
91226
+ let mpUpdated = false;
91227
+ for (const entry of plugins) {
91228
+ if (entry.version !== version3) {
91229
+ entry.version = version3;
91230
+ mpUpdated = true;
91231
+ }
91232
+ }
91233
+ if (mpUpdated) {
91234
+ marketplace.plugins = plugins;
91235
+ await Bun.write(marketplacePath, `${JSON.stringify(marketplace, null, 4)}
91236
+ `);
91237
+ staged.push(".claude-plugin/marketplace.json");
91238
+ output2.write(`Bumped marketplace plugins to ${version3}`);
91239
+ }
91240
+ for (const entry of plugins) {
91241
+ const pluginJsonPath = join24(ctx.repoRoot, entry.source, "plugin.json");
91242
+ const pluginJson = await readJson(pluginJsonPath);
91243
+ if (pluginJson === null) {
91244
+ output2.write(` \u26A0 plugin.json not found at ${entry.source}/plugin.json \u2014 skipping`);
91245
+ continue;
91246
+ }
91247
+ if (pluginJson.version !== version3) {
91248
+ pluginJson.version = version3;
91249
+ await Bun.write(pluginJsonPath, `${JSON.stringify(pluginJson, null, 4)}
91250
+ `);
91251
+ staged.push(`${entry.source}/plugin.json`);
91252
+ output2.write(`Bumped ${entry.source}/plugin.json to ${version3}`);
91253
+ }
91254
+ }
91255
+ }
91256
+ async function assertCleanTreeOnBranch(ctx) {
91257
+ if (await git2(ctx.repoRoot, ["status", "--porcelain"]) !== "") {
91258
+ throw new Error("working tree is not clean. Commit or stash changes before releasing.");
91259
+ }
91260
+ const branch = await git2(ctx.repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
91261
+ if (branch === "HEAD") {
91262
+ throw new Error("detached HEAD \u2014 checkout a branch before releasing.");
91263
+ }
91264
+ return branch;
91265
+ }
91266
+ async function assertTagFree(ctx, config4, tag, version3, output2) {
91267
+ const localTags = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
91268
+ `).filter(Boolean));
91269
+ if (localTags.has(tag)) {
91270
+ throw new Error(`tag already exists locally: ${tag}. Run "spur builder drop-tags ${config4.packageName.split("/").pop()} ${version3}" first.`);
91271
+ }
91272
+ const remote = await run(ctx.repoRoot, ["git", "ls-remote", "--tags", "origin"]);
91273
+ if (remote.ok && remote.stdout.includes(`refs/tags/${tag}`)) {
91274
+ throw new Error(`tag already exists on origin: ${tag}. Run "spur builder drop-tags ${config4.packageName.split("/").pop()} ${version3} --remote" first.`);
91275
+ }
91276
+ if (!remote.ok) {
91277
+ output2.write("warning: could not check origin for tag clashes (remote unreachable); continuing.");
91278
+ }
91279
+ }
91280
+ async function bumpVersion(ctx, config4, version3, options, output2) {
91281
+ if (!SEMVER.test(version3)) {
91282
+ throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.4).`);
91283
+ }
91284
+ const tag = releaseTag(config4, version3);
91285
+ const branch = await assertCleanTreeOnBranch(ctx);
91286
+ await assertTagFree(ctx, config4, tag, version3, output2);
91287
+ if (await npmViewVersion(ctx.repoRoot, config4.packageName, version3)) {
91288
+ throw new Error(`${config4.packageName}@${version3} is already published on npm. Use a new version.`);
91289
+ }
91290
+ const manifestPath = join24(ctx.repoRoot, config4.packageDir, "package.json");
91291
+ const manifest = await readJson(manifestPath);
91292
+ if (manifest === null)
91293
+ throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
91294
+ const previous = typeof manifest.version === "string" ? manifest.version : "0.0.0";
91295
+ manifest.version = version3;
91296
+ await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 4)}
91297
+ `);
91298
+ output2.write(`${config4.packageName}: ${previous} -> ${version3}`);
91299
+ const staged = [`${config4.packageDir}/package.json`];
91300
+ if (config4.versionSourceFile) {
91301
+ const updated = await updateVersionSourceFile(ctx, config4.versionSourceFile, previous, version3, output2);
91302
+ if (updated)
91303
+ staged.push(config4.versionSourceFile);
91304
+ }
91305
+ staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
91306
+ await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
91307
+ const lockPath = join24(ctx.repoRoot, "bun.lock");
91308
+ if (existsSync11(lockPath) && Bun.file(lockPath).size > 0)
91309
+ staged.push("bun.lock");
91310
+ await git2(ctx.repoRoot, ["add", ...staged]);
91311
+ const message = `${config4.releaseCommitType}(${config4.releaseCommitScope}): ${config4.releaseCommitSubject(version3)}`;
91312
+ await git2(ctx.repoRoot, ["commit", "-m", message]);
91313
+ output2.write(`Committed: ${message}`);
91314
+ await git2(ctx.repoRoot, ["tag", "-a", tag, "-m", config4.releaseTagMessage(tag)]);
91315
+ output2.write(`Tagged: ${tag}`);
91316
+ if (!options.push) {
91317
+ output2.write(`
91318
+ Done (local). Review, then push to release:`);
91319
+ output2.write(` git push origin ${branch}`);
91320
+ output2.write(` git push origin ${tag}`);
91321
+ output2.write("Or re-run with --push next time to do this automatically.");
91322
+ return;
91323
+ }
91324
+ output2.write(`
91325
+ Pushing branch ${branch} (tags excluded)...`);
91326
+ await git2(ctx.repoRoot, ["push", "origin", branch]);
91327
+ output2.write(`Pushing release trigger tag ${tag}...`);
91328
+ await git2(ctx.repoRoot, ["push", "origin", tag]);
91329
+ output2.write(`
91330
+ Released ${version3}. The publish workflow should now be running:`);
91331
+ output2.write(` gh run list --workflow=${config4.publishWorkflow} --limit ${config4.ghRunListLimit}`);
91332
+ }
91333
+ async function bumpAll(ctx, version3, options, output2) {
91334
+ if (!SEMVER.test(version3)) {
91335
+ throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.4).`);
91336
+ }
91337
+ if (ctx.allPackages.length === 0) {
91338
+ throw new Error('no workspace packages are pinned via "workspace:" by another package \u2014 nothing to bump.');
91339
+ }
91340
+ const branch = await assertCleanTreeOnBranch(ctx);
91341
+ const configs = ctx.allPackages;
91342
+ const aggregateTag = `${ctx.rootName}-v${version3}`;
91343
+ const existingLocal = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
91344
+ `).filter(Boolean));
91345
+ const allTags = [aggregateTag, ...configs.map((c3) => releaseTag(c3, version3))];
91346
+ for (const tag of allTags) {
91347
+ if (existingLocal.has(tag))
91348
+ throw new Error(`tag already exists locally: ${tag}`);
91349
+ }
91350
+ const remoteRefs = await git2(ctx.repoRoot, ["ls-remote", "--tags", "origin"]);
91351
+ for (const tag of allTags) {
91352
+ if (remoteRefs.includes(`refs/tags/${tag}`)) {
91353
+ throw new Error(`tag already exists on origin: ${tag}. Run "spur builder drop-tags --all ${version3} --remote" first.`);
91354
+ }
91355
+ }
91356
+ for (const config4 of configs) {
91357
+ if (await npmViewVersion(ctx.repoRoot, config4.packageName, version3)) {
91358
+ throw new Error(`${config4.packageName}@${version3} is already published on npm. Use a new version.`);
91359
+ }
91360
+ }
91361
+ const staged = [];
91362
+ for (const config4 of configs) {
91363
+ const manifestPath = join24(ctx.repoRoot, config4.packageDir, "package.json");
91364
+ const manifest = await readJson(manifestPath);
91365
+ if (manifest === null) {
91366
+ throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
91367
+ }
91368
+ const previous = typeof manifest.version === "string" ? manifest.version : "0.0.0";
91369
+ manifest.version = version3;
91370
+ await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 4)}
91371
+ `);
91372
+ output2.write(`${config4.packageName}: ${previous} -> ${version3}`);
91373
+ staged.push(`${config4.packageDir}/package.json`);
91374
+ if (config4.versionSourceFile) {
91375
+ const updated = await updateVersionSourceFile(ctx, config4.versionSourceFile, previous, version3, output2);
91376
+ if (updated)
91377
+ staged.push(config4.versionSourceFile);
91378
+ }
91379
+ staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
91380
+ }
91381
+ await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
91382
+ const lockPath = join24(ctx.repoRoot, "bun.lock");
91383
+ if (existsSync11(lockPath) && Bun.file(lockPath).size > 0)
91384
+ staged.push("bun.lock");
91385
+ await git2(ctx.repoRoot, ["add", ...staged]);
91386
+ const shortNames = configs.map((c3) => c3.packageName.split("/").pop() ?? c3.packageName).join(" + ");
91387
+ const message = `chore(release): bump ${shortNames} to ${version3}`;
91388
+ await git2(ctx.repoRoot, ["commit", "-m", message]);
91389
+ output2.write(`Committed: ${message}`);
91390
+ for (const config4 of configs) {
91391
+ const tag = releaseTag(config4, version3);
91392
+ if (tag !== aggregateTag) {
91393
+ await git2(ctx.repoRoot, ["tag", "-a", tag, "-m", config4.releaseTagMessage(tag)]);
91394
+ output2.write(`Tagged (trace): ${tag}`);
91395
+ }
91396
+ }
91397
+ await git2(ctx.repoRoot, ["tag", "-a", aggregateTag, "-m", `${ctx.rootName} ${version3} \u2014 ${shortNames}`]);
91398
+ output2.write(`Tagged (publish): ${aggregateTag}`);
91399
+ if (!options.push) {
91400
+ output2.write(`
91401
+ Done (local). Review, then push to release:`);
91402
+ output2.write(` git push origin ${branch}`);
91403
+ output2.write(` git push origin ${aggregateTag}`);
91404
+ output2.write("Or re-run with --push next time to do this automatically.");
91405
+ return;
91406
+ }
91407
+ output2.write(`
91408
+ Pushing branch ${branch} (tags excluded)...`);
91409
+ await git2(ctx.repoRoot, ["push", "origin", branch]);
91410
+ output2.write(`Pushing release trigger tag ${aggregateTag}...`);
91411
+ await git2(ctx.repoRoot, ["push", "origin", aggregateTag]);
91412
+ output2.write(`
91413
+ Released ${version3}. The publish workflow should now be running:`);
91414
+ output2.write(" gh run list --workflow=publish.yml --limit 3");
91415
+ }
91416
+ async function dropTagsFor(ctx, config4, version3, options, output2) {
91417
+ if (!SEMVER.test(version3)) {
91418
+ throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.2).`);
91419
+ }
91420
+ const tag = releaseTag(config4, version3);
91421
+ const localTags = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
91422
+ `).filter(Boolean));
91423
+ if (localTags.has(tag)) {
91424
+ await git2(ctx.repoRoot, ["tag", "-d", tag]);
91425
+ output2.write(`Deleted local tag ${tag}`);
91426
+ } else {
91427
+ output2.write(`No local tag ${tag}`);
91428
+ }
91429
+ if (options.remote) {
91430
+ const result = await run(ctx.repoRoot, ["git", "push", "origin", `:refs/tags/${tag}`]);
91431
+ output2.write(result.ok ? `Deleted remote tag ${tag}` : `Remote tag ${tag} not present or already removed`);
91432
+ }
91433
+ }
91434
+ async function dropAll(ctx, version3, options, output2) {
91435
+ if (!SEMVER.test(version3)) {
91436
+ throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.2).`);
91437
+ }
91438
+ for (const config4 of ctx.allPackages) {
91439
+ await dropTagsFor(ctx, config4, version3, options, output2);
91440
+ }
91441
+ const aggregateTag = `${ctx.rootName}-v${version3}`;
91442
+ const localTags = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
91443
+ `).filter(Boolean));
91444
+ if (localTags.has(aggregateTag)) {
91445
+ await git2(ctx.repoRoot, ["tag", "-d", aggregateTag]);
91446
+ output2.write(`Deleted local tag ${aggregateTag}`);
91447
+ }
91448
+ if (options.remote) {
91449
+ const result = await run(ctx.repoRoot, ["git", "push", "origin", `:refs/tags/${aggregateTag}`]);
91450
+ output2.write(result.ok ? `Deleted remote tag ${aggregateTag}` : `Remote tag ${aggregateTag} not present or already removed`);
91451
+ }
91452
+ }
91453
+ function releaseUsage(message) {
91454
+ const usage = [
91455
+ "Usage:",
91456
+ " bump-ver <version> | bump-ver --all <version> [--push] bump all released packages",
91457
+ " bump-ver <package-id> <version> [--push] bump one package, commit, tag",
91458
+ " drop-tags <version> | drop-tags --all <version> [--remote] drop all release tags",
91459
+ " drop-tags <package-id> <version> [--remote] drop one package release tag"
91460
+ ].join(`
91461
+ `);
91462
+ return new Error(message === undefined ? usage : `${message}
91463
+
91464
+ ${usage}`);
91465
+ }
91466
+ async function bumpVer(args, repoRoot = process.cwd(), output2 = consoleOutput) {
91467
+ const ctx = await releaseContext(repoRoot);
91468
+ const positional = args.filter((arg) => !arg.startsWith("--"));
91469
+ if (args.includes("--all") || positional.length === 1 && SEMVER.test(positional[0] ?? "")) {
91470
+ const allVersion = positional[0];
91471
+ if (!allVersion)
91472
+ throw releaseUsage("bump-ver [--all] <version> [--push]");
91473
+ await bumpAll(ctx, allVersion, { push: args.includes("--push") }, output2);
91474
+ return;
91475
+ }
91476
+ const packageId = positional[0];
91477
+ const version3 = positional[1];
91478
+ if (!packageId || !version3)
91479
+ throw releaseUsage("bump-ver <version> | bump-ver <package-id> <version> [--push]");
91480
+ const config4 = ctx.packages.get(packageId);
91481
+ if (config4 === undefined) {
91482
+ throw releaseUsage(`unknown package "${packageId}". Package IDs: ${[...ctx.packages.keys()].join(", ")}`);
91483
+ }
91484
+ await bumpVersion(ctx, config4, version3, { push: args.includes("--push") }, output2);
91485
+ }
91486
+ async function dropTags(args, repoRoot = process.cwd(), output2 = consoleOutput) {
91487
+ const ctx = await releaseContext(repoRoot);
91488
+ const positional = args.filter((arg) => !arg.startsWith("--"));
91489
+ if (args.includes("--all") || positional.length === 1 && SEMVER.test(positional[0] ?? "")) {
91490
+ const allVersion = positional[0];
91491
+ if (!allVersion)
91492
+ throw releaseUsage("drop-tags [--all] <version> [--remote]");
91493
+ await dropAll(ctx, allVersion, { remote: args.includes("--remote") }, output2);
91494
+ return;
91495
+ }
91496
+ const packageId = positional[0];
91497
+ const version3 = positional[1];
91498
+ if (!packageId || !version3) {
91499
+ throw releaseUsage("drop-tags <version> | drop-tags <package-id> <version> [--remote]");
91500
+ }
91501
+ const config4 = ctx.packages.get(packageId);
91502
+ if (config4 === undefined) {
91503
+ throw releaseUsage(`unknown package "${packageId}". Package IDs: ${[...ctx.packages.keys()].join(", ")}`);
91504
+ }
91505
+ await dropTagsFor(ctx, config4, version3, { remote: args.includes("--remote") }, output2);
91506
+ }
91507
+
91508
+ // src/commands/builder.ts
91509
+ function registerBuilderCommand(program2, context4) {
91510
+ const noun = program2.command("builder").summary("release plumbing: version bumps and release tags");
91511
+ noun.command("bump-ver").summary("bump a package version, commit, tag, optionally push").description([
91512
+ "Bump one workspace package (or every released package with --all),",
91513
+ "rewrite workspace pins, commit, and tag. A bare version bumps all:",
91514
+ " spur builder bump-ver 0.1.4 # every released package",
91515
+ " spur builder bump-ver spur 0.1.4 # one package by id"
91516
+ ].join(`
91517
+ `)).option("--all", "bump every released package in one commit with per-package + aggregate tags").option("--push", "push the branch and release tag to origin").option(...SHARED_OPTIONS.json).argument("[target]", "package id, or the version itself when bumping all").argument("[version]", "target semver version").action(async (target, version3, options) => {
91518
+ const args = [target, version3].filter((value) => value !== undefined);
91519
+ if (options.all === true)
91520
+ args.push("--all");
91521
+ if (options.push === true)
91522
+ args.push("--push");
91523
+ try {
91524
+ await bumpVer(args, context4.cwd, context4.output);
91525
+ if (options.json === true) {
91526
+ context4.output.write(toJson2({ ok: true, verb: "bump-ver", target: target ?? "all", version: version3 ?? "" }));
91527
+ }
91528
+ } catch (err) {
91529
+ const message = err instanceof Error ? err.message : String(err);
91530
+ if (options.json === true) {
91531
+ context4.output.write(toJson2({ ok: false, verb: "bump-ver", error: message }));
91532
+ context4.setExitCode(1);
91533
+ return;
91534
+ }
91535
+ context4.output.error(message);
91536
+ context4.setExitCode(1);
91537
+ }
91538
+ });
91539
+ noun.command("drop-tags").summary("delete release tags locally and optionally on origin").description([
91540
+ "Delete one package release tag (or every released tag plus the aggregate",
91541
+ "with --all / a bare version). Use --remote to also delete on origin."
91542
+ ].join(`
91543
+ `)).option("--all", "drop every released tag plus the aggregate tag").option("--remote", "also delete the tag(s) on origin").option(...SHARED_OPTIONS.json).argument("[target]", "package id, or the version itself when dropping all").argument("[version]", "semver version of the tags to drop").action(async (target, version3, options) => {
91544
+ const args = [target, version3].filter((value) => value !== undefined);
91545
+ if (options.all === true)
91546
+ args.push("--all");
91547
+ if (options.remote === true)
91548
+ args.push("--remote");
91549
+ try {
91550
+ await dropTags(args, context4.cwd, context4.output);
91551
+ if (options.json === true) {
91552
+ context4.output.write(toJson2({ ok: true, verb: "drop-tags", target: target ?? "all", version: version3 ?? "" }));
91553
+ }
91554
+ } catch (err) {
91555
+ const message = err instanceof Error ? err.message : String(err);
91556
+ if (options.json === true) {
91557
+ context4.output.write(toJson2({ ok: false, verb: "drop-tags", error: message }));
91558
+ context4.setExitCode(1);
91559
+ return;
91560
+ }
91561
+ context4.output.error(message);
91562
+ context4.setExitCode(1);
91563
+ }
91564
+ });
91565
+ }
91566
+
90460
91567
  // src/commands/feature.ts
90461
91568
  await init_src3();
90462
91569
 
@@ -90489,14 +91596,14 @@ function makePlanningEmitter(context4) {
90489
91596
  init_loader();
90490
91597
  init_src2();
90491
91598
  await init_src3();
90492
- import { existsSync as existsSync10 } from "fs";
91599
+ import { existsSync as existsSync12 } from "fs";
90493
91600
  import { homedir as homedir8 } from "os";
90494
- import { join as join23, resolve as resolve16 } from "path";
91601
+ import { join as join25, resolve as resolve16 } from "path";
90495
91602
 
90496
91603
  // src/workflow/resolve-spur-bin.ts
90497
- import { basename as basename7 } from "path";
91604
+ import { basename as basename8 } from "path";
90498
91605
  function resolveSpurBinFrom(launch) {
90499
- const runtime = basename7(launch.execPath).toLowerCase().replace(/\.exe$/, "");
91606
+ const runtime = basename8(launch.execPath).toLowerCase().replace(/\.exe$/, "");
90500
91607
  const isJsRuntime = runtime === "bun" || runtime === "node";
90501
91608
  if (isJsRuntime && launch.mainModule) {
90502
91609
  return `${launch.execPath} ${launch.mainModule}`;
@@ -90509,23 +91616,23 @@ function resolveSpurBin() {
90509
91616
  }
90510
91617
 
90511
91618
  // src/workflow/make-lifecycle-adapter.ts
90512
- var GLOBAL_CONFIG_DIR = join23(".config", "spur");
91619
+ var GLOBAL_CONFIG_DIR = join25(".config", "spur");
90513
91620
  function globalConfigRoot(context4) {
90514
91621
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
90515
- return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) : join23(homedir8(), GLOBAL_CONFIG_DIR);
91622
+ return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) : join25(homedir8(), GLOBAL_CONFIG_DIR);
90516
91623
  }
90517
91624
  function resolveWorkflowPath(context4, profile) {
90518
91625
  const bundledRoot = bundledConfigRoot();
90519
91626
  if (bundledRoot !== null) {
90520
- const bundledPath = join23(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
90521
- if (existsSync10(bundledPath))
91627
+ const bundledPath = join25(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
91628
+ if (existsSync12(bundledPath))
90522
91629
  return bundledPath;
90523
91630
  }
90524
- const projectPath = join23(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
90525
- if (existsSync10(projectPath))
91631
+ const projectPath = join25(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
91632
+ if (existsSync12(projectPath))
90526
91633
  return projectPath;
90527
- const globalPath = join23(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
90528
- if (existsSync10(globalPath))
91634
+ const globalPath = join25(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
91635
+ if (existsSync12(globalPath))
90529
91636
  return globalPath;
90530
91637
  return null;
90531
91638
  }
@@ -90547,7 +91654,7 @@ function makeLifecycleAdapter(context4, profile) {
90547
91654
  // src/commands/feature.ts
90548
91655
  function registerFeatureCommand(program2, context4) {
90549
91656
  const feature = program2.command("feature").summary("manage features (hierarchical IDs)");
90550
- feature.command("create").summary("Create a feature; allocates a hierarchical ID (DD-14) under the create-lock.").argument("<name>", "Feature name").option("--parent <id>", "Parent feature ID (child gets the next free digit 1-9)").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (name, options) => {
91657
+ feature.command("create").summary("Create a feature; allocates a hierarchical ID (DD-14) under the create-lock.").argument("<name>", "Feature name").option("--parent <id>", "Parent feature ID (child gets the next free digit 1-9)").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (name, options) => {
90551
91658
  const svc = await makeService(context4, options.folder);
90552
91659
  try {
90553
91660
  const result = await svc.create(name, options.parent);
@@ -90561,7 +91668,7 @@ function registerFeatureCommand(program2, context4) {
90561
91668
  context4.setExitCode(1);
90562
91669
  }
90563
91670
  });
90564
- feature.command("show").alias("get").summary("Show a feature by ID.").argument("<id>", "Feature ID").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
91671
+ feature.command("show").alias("get").summary("Show a feature by ID.").argument("<id>", "Feature ID").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
90565
91672
  const svc = await makeService(context4, options.folder);
90566
91673
  try {
90567
91674
  const result = await svc.show(id);
@@ -90586,7 +91693,7 @@ function registerFeatureCommand(program2, context4) {
90586
91693
  "`--section` costs a failed write. List valid names first:",
90587
91694
  "`spur task sections <wbs> list`."
90588
91695
  ].join(`
90589
- `)).option("--field <key>", "Frontmatter field to set (e.g. priority)").option("--value <value>", "New value for --field").option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, status, options) => {
91696
+ `)).option("--field <key>", "Frontmatter field to set (e.g. priority)").option("--value <value>", "New value for --field").option(...SHARED_OPTIONS.section).option(...SHARED_OPTIONS.fromFile).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, status, options) => {
90590
91697
  const svc = await makeService(context4, options.folder);
90591
91698
  try {
90592
91699
  let result;
@@ -90642,7 +91749,7 @@ function registerFeatureCommand(program2, context4) {
90642
91749
  context4.setExitCode(1);
90643
91750
  }
90644
91751
  });
90645
- feature.command("advance").summary("Walk a feature through the legal forward lifecycle path.").argument("<id>", "Feature ID to advance").option("--to <status>", "Target status (default: 'done')").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
91752
+ feature.command("advance").summary("Walk a feature through the legal forward lifecycle path.").argument("<id>", "Feature ID to advance").option("--to <status>", "Target status (default: 'done')").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
90646
91753
  const svc = await makeService(context4, options.folder);
90647
91754
  const target = options.to ?? "done";
90648
91755
  const forwardPath = {
@@ -90701,7 +91808,7 @@ function registerFeatureCommand(program2, context4) {
90701
91808
  context4.setExitCode(1);
90702
91809
  }
90703
91810
  });
90704
- feature.command("list").summary("List features with optional status/priority filters.").option("--status <s>", "Filter by status").option("--priority <p>", "Filter by priority").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (options) => {
91811
+ feature.command("list").summary("List features with optional status/priority filters.").option(...SHARED_OPTIONS.statusFilter).option(...SHARED_OPTIONS.priorityFilter).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (options) => {
90705
91812
  const svc = await makeService(context4, options.folder);
90706
91813
  try {
90707
91814
  let features = await svc.list();
@@ -90726,7 +91833,7 @@ function registerFeatureCommand(program2, context4) {
90726
91833
  context4.setExitCode(1);
90727
91834
  }
90728
91835
  });
90729
- feature.command("move").summary("Move a feature to a new parent \u2014 cascade rename of the subtree (DD-14).").argument("<id>", "Feature ID to move").option("--parent <id>", "New parent feature ID (omit to move to a top-level group)").option("--dry-run", "Show the old\u2192new ID map + affected tasks without writing").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
91836
+ feature.command("move").summary("Move a feature to a new parent \u2014 cascade rename of the subtree (DD-14).").argument("<id>", "Feature ID to move").option("--parent <id>", "New parent feature ID (omit to move to a top-level group)").option(...SHARED_OPTIONS.dryRunFeatureMap).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
90730
91837
  const svc = await makeService(context4, options.folder);
90731
91838
  try {
90732
91839
  const result = await svc.move(id, options.parent ?? null, { dryRun: options.dryRun === true });
@@ -90754,7 +91861,7 @@ function registerFeatureCommand(program2, context4) {
90754
91861
  "Does NOT change feature frontmatter status. For lifecycle alignment use `spur feature sync`.",
90755
91862
  "Use after task create/link/done when the ## Tasks table is stale."
90756
91863
  ].join(`
90757
- `)).option("--feature <id>", "Restrict the ## Tasks rewrite to one feature (INDEX.md still regenerated)").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (options) => {
91864
+ `)).option(...SHARED_OPTIONS.featureTasksRewrite).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (options) => {
90758
91865
  const svc = await makeService(context4, options.folder);
90759
91866
  try {
90760
91867
  const result = await svc.refresh({ featureId: options.feature });
@@ -90770,7 +91877,7 @@ function registerFeatureCommand(program2, context4) {
90770
91877
  context4.setExitCode(1);
90771
91878
  }
90772
91879
  });
90773
- feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option("--strict", "Elevate warnings to failures").option("--as <status>", "Evaluate the one-active-goal rule as if the feature were in <status> (0418: lifecycle FSM guards pass the transition target)").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
91880
+ feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option(...SHARED_OPTIONS.strictFeature).option(...SHARED_OPTIONS.asFeature0418).option(...SHARED_OPTIONS.folderFeatures).option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option(...SHARED_OPTIONS.json).action(async (id, options) => {
90774
91881
  const resolved = await resolvePlanningFolders(context4.fs);
90775
91882
  const featuresDir = options.folder ?? context4.fs.resolve(resolved.featuresDir);
90776
91883
  const tasksDir = context4.fs.resolve(resolved.tasksDir);
@@ -90798,10 +91905,16 @@ function registerFeatureCommand(program2, context4) {
90798
91905
  tasksDirs,
90799
91906
  runDir: context4.fs.resolve(".spur/run"),
90800
91907
  severityOverrides: resolved.severityOverrides,
90801
- asStatus: options.as
91908
+ asStatus: options.as,
91909
+ fix: options.fix === true
90802
91910
  });
90803
91911
  results.push(result);
90804
91912
  if (!json3) {
91913
+ if (result.repairs !== undefined && result.repairs.length > 0) {
91914
+ for (const r of result.repairs) {
91915
+ context4.output.write(` [FIX] ${r.kind} ${r.section}: ${r.detail}`);
91916
+ }
91917
+ }
90805
91918
  context4.output.write(`
90806
91919
  ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
90807
91920
  for (const f of result.findings) {
@@ -90829,7 +91942,7 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
90829
91942
  "",
90830
91943
  "Does NOT rewrite INDEX.md or ## Tasks tables. For stale rosters use `spur feature refresh`."
90831
91944
  ].join(`
90832
- `)).argument("[id]", "Feature ID to sync (optional if --all is passed)").option("--all", "Sync all features with linked tasks").option("--dry-run", "Report proposed status sync transitions without applying").option("--force", "Force applying reopen proposals without confirmation").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
91945
+ `)).argument("[id]", "Feature ID to sync (optional if --all is passed)").option("--all", "Sync all features with linked tasks").option(...SHARED_OPTIONS.dryRunFeatureSync).option(...SHARED_OPTIONS.forceFeatureReopen).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
90833
91946
  const svc = await makeService(context4, options.folder);
90834
91947
  try {
90835
91948
  if (!options.all && !id) {
@@ -90922,7 +92035,7 @@ import { createRequire } from "module";
90922
92035
  var CLI_CONFIG = {
90923
92036
  binaryName: "spur",
90924
92037
  binaryLabel: "spur",
90925
- binaryVersion: "0.3.55",
92038
+ binaryVersion: "0.3.57",
90926
92039
  configDir: ".spur",
90927
92040
  configFile: ".spur/config.yaml",
90928
92041
  databaseFile: ".spur/spur.db"
@@ -90949,7 +92062,7 @@ importer: @gobing-ai/ts-llm-jsonl-importer@${provenance.importer}`;
90949
92062
  function registerHistoryCommand(program2, context4) {
90950
92063
  const noun = program2.command("history").summary("import and analyze coding-agent history");
90951
92064
  const makeService2 = () => new HistoryService({ getDb: () => context4.getDb(), agentConfig: context4.agentConfig });
90952
- noun.command("import").description("Import agent conversation JSONL. `--source all` fans out across all sources with " + "per-source failure isolation (task 0470). A single source is the n=1 case of " + "the same contract \u2014 never two import paths.").option("--source <source>", "pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all", "all").option("--file <path>", "Import one JSONL file (single-source only)").option("--root <path>", "Scan a history root").option("--mode <mode>", "full|incremental|force-file").option("--dry-run", "Scan without persisting imported records").option("--source-timeout <ms>", "Per-source timeout in milliseconds (default 600000 = 10 min)", "600000").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
92065
+ noun.command("import").description("Import agent conversation JSONL. `--source all` fans out across all sources with " + "per-source failure isolation (task 0470). A single source is the n=1 case of " + "the same contract \u2014 never two import paths.").option(...SHARED_OPTIONS.sourceHistory, "all").option(...SHARED_OPTIONS.fileHistoryJsonl).option("--root <path>", "Scan a history root").option(...SHARED_OPTIONS.modeHistory).option(...SHARED_OPTIONS.dryRunHistoryScan).option("--source-timeout <ms>", "Per-source timeout in milliseconds (default 600000 = 10 min)", "600000").option(...SHARED_OPTIONS.jsonSupported).action(async (options) => {
90953
92066
  const source = options.source ?? "all";
90954
92067
  if (options.file && source === "all") {
90955
92068
  context4.output.write(options.json ? toJson2({ status: "error", message: '--file requires a single --source, not "all".' }) : 'spur history import: --file requires a single --source, not "all".');
@@ -90988,7 +92101,7 @@ function registerHistoryCommand(program2, context4) {
90988
92101
  ${formatFanOutResult(fanOut)}`);
90989
92102
  context4.setExitCode(fanOut.exitCode);
90990
92103
  });
90991
- noun.command("analyze").description("Aggregate imported history with SQL and write a versioned JSON artifact (Q1-Q10 forensic query set).").option("--since <iso>", "Inclusive lower bound on message timestamp").option("--until <iso>", "Inclusive upper bound on message timestamp").option("--source <source>", "pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all", "all").option("--session <id>", "Narrow to a single session id").option("--run <runId>", "Narrow to a single workflow run id").option("--task <wbs>", "Narrow to a single task WBS").option("--top <n>", "Leaderboard depth for byTool/bySession", "20").option("--out <path>", "Write the artifact to this path instead of the dated reports dir").option("--json", "Emit the artifact as JSON instead of the human summary").action(async (options) => {
92104
+ noun.command("analyze").description("Aggregate imported history with SQL and write a versioned JSON artifact (Q1-Q10 forensic query set).").option("--since <iso>", "Inclusive lower bound on message timestamp").option("--until <iso>", "Inclusive upper bound on message timestamp").option(...SHARED_OPTIONS.sourceHistory, "all").option("--session <id>", "Narrow to a single session id").option(...SHARED_OPTIONS.runHistory).option("--task <wbs>", "Narrow to a single task WBS").option("--top <n>", "Leaderboard depth for byTool/bySession", "20").option("--out <path>", "Write the artifact to this path instead of the dated reports dir").option(...SHARED_OPTIONS.jsonArtifact).action(async (options) => {
90992
92105
  const svc = makeService2();
90993
92106
  const source = options.source ?? "all";
90994
92107
  const selector = {
@@ -91012,7 +92125,7 @@ ${formatFanOutResult(fanOut)}`);
91012
92125
  daily: artifact.daily
91013
92126
  }));
91014
92127
  });
91015
- noun.command("report").description("Render a previously-generated history artifact as a spend + forensic report. " + "Never opens the database \u2014 pure renderer of the analyze JSON. " + "--task / --top narrow the already-loaded artifact client-side (0564 R3).").argument("[path]", "Artifact JSON path (defaults to the latest.json pointer)").option("--json", "Emit the parsed artifact as JSON instead of the human report").option("--mode <name>", "Report mode: default | forensics (registry-resolved; unknown names fail)").option("--task <wbs>", "Narrow to a single task WBS the artifact was analyzed with").option("--top <n>", "Leaderboard depth for byTool/bySession (re-slices the artifact)").action(async (pathArg, options) => {
92128
+ noun.command("report").description("Render a previously-generated history artifact as a spend + forensic report. " + "Never opens the database \u2014 pure renderer of the analyze JSON. " + "--task / --top narrow the already-loaded artifact client-side (0564 R3).").argument("[path]", "Artifact JSON path (defaults to the latest.json pointer)").option(...SHARED_OPTIONS.jsonParsedArtifact).option("--mode <name>", "Report mode: default | forensics (registry-resolved; unknown names fail)").option("--task <wbs>", "Narrow to a single task WBS the artifact was analyzed with").option("--top <n>", "Leaderboard depth for byTool/bySession (re-slices the artifact)").action(async (pathArg, options) => {
91016
92129
  try {
91017
92130
  const top = parseTopOrThrow(options.top);
91018
92131
  const { report, artifactPath, resolution, artifact, banner } = runHistoryReport({
@@ -91047,7 +92160,7 @@ ${formatFanOutResult(fanOut)}`);
91047
92160
  context4.setExitCode(1);
91048
92161
  }
91049
92162
  });
91050
- noun.command("daily").description("Run-once daily pipeline: import-all (fan-out, per-source isolation) \u2192 analyze \u2192 write " + "artifact \u2192 prune reports older than 90 days (task 0470 R6). Import uses checkpoint " + "resume, so a missed night self-heals on the next run with no gap and no double-count.").option("--since <iso>", "Inclusive lower bound on message timestamp for the report (not the import)").option("--until <iso>", "Inclusive upper bound on message timestamp for the report").option("--source-timeout <ms>", "Per-source import timeout in milliseconds (default 600000 = 10 min)", "600000").option("--root <path>", "History root override (default: per-source platform dir)").option("--json", "Emit the daily result as JSON").option("--mode <name>", "Render the artifact as a .md sidecar in this mode after analyze (e.g. forensics)").action(async (options) => {
92163
+ noun.command("daily").description("Run-once daily pipeline: import-all (fan-out, per-source isolation) \u2192 analyze \u2192 write " + "artifact \u2192 prune reports older than 90 days (task 0470 R6). Import uses checkpoint " + "resume, so a missed night self-heals on the next run with no gap and no double-count.").option("--since <iso>", "Inclusive lower bound on message timestamp for the report (not the import)").option("--until <iso>", "Inclusive upper bound on message timestamp for the report").option("--source-timeout <ms>", "Per-source import timeout in milliseconds (default 600000 = 10 min)", "600000").option("--root <path>", "History root override (default: per-source platform dir)").option(...SHARED_OPTIONS.jsonDaily).option("--mode <name>", "Render the artifact as a .md sidecar in this mode after analyze (e.g. forensics)").action(async (options) => {
91051
92164
  const svc = makeService2();
91052
92165
  const sourceTimeout = Number.parseInt(options.sourceTimeout ?? "600000", 10) || 600000;
91053
92166
  const bus = new EventBus;
@@ -91160,7 +92273,7 @@ function parseTopOrThrow(raw) {
91160
92273
  function formatFanOutResult(r) {
91161
92274
  const lines = ["history import (fan-out)", "sources:"];
91162
92275
  for (const e of r.entries) {
91163
- lines.push(` ${e.source}: ${e.status} (files=${e.files} messages=${e.messages})`);
92276
+ lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
91164
92277
  }
91165
92278
  if (r.warnings.length > 0) {
91166
92279
  lines.push("warnings:");
@@ -91176,7 +92289,7 @@ function formatDailyResult(r) {
91176
92289
  const lines = ["history daily"];
91177
92290
  lines.push("import:");
91178
92291
  for (const e of r.fanOut.entries) {
91179
- lines.push(` ${e.source}: ${e.status} (files=${e.files} messages=${e.messages})`);
92292
+ lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
91180
92293
  }
91181
92294
  if (r.fanOut.warnings.length > 0) {
91182
92295
  lines.push("warnings:");
@@ -91200,7 +92313,7 @@ init_loader();
91200
92313
  init_src2();
91201
92314
  init_dist10();
91202
92315
  import { homedir as homedir9 } from "os";
91203
- import { join as join24, resolve as resolve17 } from "path";
92316
+ import { join as join26, resolve as resolve17 } from "path";
91204
92317
 
91205
92318
  // src/config/scaffold-manifest.ts
91206
92319
  var SCAFFOLD_MANIFEST = [
@@ -91247,8 +92360,8 @@ var SCAFFOLD_MANIFEST = [
91247
92360
  ];
91248
92361
 
91249
92362
  // src/commands/init.ts
91250
- var GLOBAL_CONFIG_DIR2 = join24(".config", "spur");
91251
- var GLOBAL_RULES_DIR2 = join24(GLOBAL_CONFIG_DIR2, "rules");
92363
+ var GLOBAL_CONFIG_DIR2 = join26(".config", "spur");
92364
+ var GLOBAL_RULES_DIR2 = join26(GLOBAL_CONFIG_DIR2, "rules");
91252
92365
  var GLOBAL_CONFIG_EXAMPLE = "config.example.yaml";
91253
92366
  var GLOBAL_CONFIG_FILE2 = "config.yaml";
91254
92367
  var INDEXED_CONTEXT_MARKER = "## Indexed context";
@@ -91279,7 +92392,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
91279
92392
  `;
91280
92393
  function globalRulesRoot(context4) {
91281
92394
  const override = context4.env.SPUR_GLOBAL_RULES_DIR;
91282
- return override !== undefined && override.length > 0 ? resolve17(context4.cwd, override) : join24(homedir9(), GLOBAL_RULES_DIR2);
92395
+ return override !== undefined && override.length > 0 ? resolve17(context4.cwd, override) : join26(homedir9(), GLOBAL_RULES_DIR2);
91283
92396
  }
91284
92397
  async function writeIfNew(context4, path9, content, force, result) {
91285
92398
  if (!force && await context4.fs.exists(path9)) {
@@ -91296,11 +92409,11 @@ async function seedGlobalRules(context4) {
91296
92409
  const target = globalRulesRoot(context4);
91297
92410
  let written = 0;
91298
92411
  for (const relPath of await listBundledRuleFiles()) {
91299
- const destination = join24(target, relPath);
92412
+ const destination = join26(target, relPath);
91300
92413
  if (await context4.fs.exists(destination))
91301
92414
  continue;
91302
- await context4.fs.ensureDir(join24(target, ...relPath.split("/").slice(0, -1)));
91303
- await context4.fs.writeFile(destination, await context4.fs.readFile(join24(source, relPath)));
92415
+ await context4.fs.ensureDir(join26(target, ...relPath.split("/").slice(0, -1)));
92416
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join26(source, relPath)));
91304
92417
  written += 1;
91305
92418
  }
91306
92419
  return written;
@@ -91310,20 +92423,20 @@ async function seedGlobalConfig(context4) {
91310
92423
  if (source === null)
91311
92424
  return 0;
91312
92425
  const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
91313
- const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve17(context4.cwd, globalOverride) : join24(homedir9(), GLOBAL_CONFIG_DIR2);
92426
+ const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve17(context4.cwd, globalOverride) : join26(homedir9(), GLOBAL_CONFIG_DIR2);
91314
92427
  let written = 0;
91315
92428
  for (const relPath of listBundledConfigFiles()) {
91316
92429
  if (relPath === GLOBAL_CONFIG_EXAMPLE)
91317
92430
  continue;
91318
- const destination = join24(target, relPath);
92431
+ const destination = join26(target, relPath);
91319
92432
  if (await context4.fs.exists(destination))
91320
92433
  continue;
91321
- await context4.fs.ensureDir(join24(target, ...relPath.split("/").slice(0, -1)));
91322
- await context4.fs.writeFile(destination, await context4.fs.readFile(join24(source, relPath)));
92434
+ await context4.fs.ensureDir(join26(target, ...relPath.split("/").slice(0, -1)));
92435
+ await context4.fs.writeFile(destination, await context4.fs.readFile(join26(source, relPath)));
91323
92436
  written += 1;
91324
92437
  }
91325
- const examplePath = join24(source, GLOBAL_CONFIG_EXAMPLE);
91326
- const globalConfigPath = join24(target, GLOBAL_CONFIG_FILE2);
92438
+ const examplePath = join26(source, GLOBAL_CONFIG_EXAMPLE);
92439
+ const globalConfigPath = join26(target, GLOBAL_CONFIG_FILE2);
91327
92440
  if (await context4.fs.exists(examplePath) && !await context4.fs.exists(globalConfigPath)) {
91328
92441
  await context4.fs.ensureDir(target);
91329
92442
  await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
@@ -91331,13 +92444,13 @@ async function seedGlobalConfig(context4) {
91331
92444
  }
91332
92445
  return written;
91333
92446
  }
91334
- function registerInitCommand(program2, context4) {
91335
- program2.command("init").summary("scaffold a local Spur project").option("--name <name>", "Project name (default: current directory name)").option("--force", "Recreate files that already exist").option("--minimal", "Only write the minimal .spur scaffold").option("--json", "Output machine-readable JSON").action(async (options) => {
91336
- const json3 = options.json === true;
91337
- const force = options.force === true;
91338
- const minimal = options.minimal === true;
91339
- const projectName = options.name ?? "default";
91340
- const configPath = join24(context4.cwd, CLI_CONFIG.configFile);
92447
+ function registerInitCommand(program2, context4, options = {}) {
92448
+ program2.command("init", { hidden: options.hidden === true }).summary("scaffold a local Spur project").option(...SHARED_OPTIONS.nameProjectInit).option(...SHARED_OPTIONS.forceInitRecreate).option("--minimal", "Only write the minimal .spur scaffold").option(...SHARED_OPTIONS.json).action(async (options2) => {
92449
+ const json3 = options2.json === true;
92450
+ const force = options2.force === true;
92451
+ const minimal = options2.minimal === true;
92452
+ const projectName = options2.name ?? "default";
92453
+ const configPath = join26(context4.cwd, CLI_CONFIG.configFile);
91341
92454
  if (!force && await context4.fs.exists(configPath)) {
91342
92455
  const message = `Already initialized: ${CLI_CONFIG.configFile}. Use --force to overwrite.`;
91343
92456
  context4.output.write(json3 ? toJson2({ ok: false, reason: "already-initialized", config: CLI_CONFIG.configFile }) : message);
@@ -91372,13 +92485,13 @@ function registerInitCommand(program2, context4) {
91372
92485
  ].join(`
91373
92486
  `)}
91374
92487
  `;
91375
- await context4.fs.ensureDir(join24(context4.cwd, CLI_CONFIG.configDir));
92488
+ await context4.fs.ensureDir(join26(context4.cwd, CLI_CONFIG.configDir));
91376
92489
  await context4.fs.writeFile(configPath, configYaml);
91377
92490
  result.created.push(configPath);
91378
- const agentsDir = join24(context4.cwd, CLI_CONFIG.configDir, "agents");
92491
+ const agentsDir = join26(context4.cwd, CLI_CONFIG.configDir, "agents");
91379
92492
  await context4.fs.ensureDir(agentsDir);
91380
- await writeIfNew(context4, join24(agentsDir, ".gitkeep"), "", force, result);
91381
- const gitignorePath = join24(context4.cwd, ".gitignore");
92493
+ await writeIfNew(context4, join26(agentsDir, ".gitkeep"), "", force, result);
92494
+ const gitignorePath = join26(context4.cwd, ".gitignore");
91382
92495
  const contextEntry = ".spur/context/";
91383
92496
  if (await context4.fs.exists(gitignorePath)) {
91384
92497
  const existing = await context4.fs.readFile(gitignorePath);
@@ -91400,20 +92513,20 @@ ${contextEntry}
91400
92513
  for (const relPath of listBundledProjectSeedFiles()) {
91401
92514
  if (relPath.startsWith("templates/docs/"))
91402
92515
  continue;
91403
- const sourcePath = join24(configRoot, relPath);
92516
+ const sourcePath = join26(configRoot, relPath);
91404
92517
  if (!await context4.fs.exists(sourcePath))
91405
92518
  continue;
91406
- const targetPath = join24(context4.cwd, CLI_CONFIG.configDir, relPath);
91407
- await context4.fs.ensureDir(join24(targetPath, ".."));
92519
+ const targetPath = join26(context4.cwd, CLI_CONFIG.configDir, relPath);
92520
+ await context4.fs.ensureDir(join26(targetPath, ".."));
91408
92521
  await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), force, result);
91409
92522
  }
91410
92523
  for (const entry of SCAFFOLD_MANIFEST) {
91411
- const sourcePath = join24(configRoot, entry.source);
92524
+ const sourcePath = join26(configRoot, entry.source);
91412
92525
  if (!await context4.fs.exists(sourcePath))
91413
92526
  continue;
91414
- const baseDir = entry.root === true ? context4.cwd : join24(context4.cwd, CLI_CONFIG.configDir);
91415
- const targetPath = join24(baseDir, entry.target);
91416
- await context4.fs.ensureDir(join24(targetPath, ".."));
92527
+ const baseDir = entry.root === true ? context4.cwd : join26(context4.cwd, CLI_CONFIG.configDir);
92528
+ const targetPath = join26(baseDir, entry.target);
92529
+ await context4.fs.ensureDir(join26(targetPath, ".."));
91417
92530
  const entryForce = entry.preserve === true ? false : force;
91418
92531
  let body = await context4.fs.readFile(sourcePath);
91419
92532
  if (entry.target === "AGENTS.md") {
@@ -91426,7 +92539,7 @@ ${contextEntry}
91426
92539
  }
91427
92540
  }
91428
92541
  }
91429
- const agentsMdPath = join24(context4.cwd, "AGENTS.md");
92542
+ const agentsMdPath = join26(context4.cwd, "AGENTS.md");
91430
92543
  if (await context4.fs.exists(agentsMdPath)) {
91431
92544
  const existing = await context4.fs.readFile(agentsMdPath);
91432
92545
  if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
@@ -91471,22 +92584,22 @@ var DEFAULT_FROM = "operator";
91471
92584
  var DEFAULT_WATCH_INTERVAL_MS = 2000;
91472
92585
  function registerMessageCommand(program2, context4) {
91473
92586
  const noun = program2.command("message").summary("send and inspect durable inter-agent messages");
91474
- noun.command("send").description("Enqueue a message for an agent. Use --wait to block until the recipient reaches a state.").argument("<body>", "Message body").requiredOption("--to <id>", "Recipient agent id").option("--from <id>", "Sender id", DEFAULT_FROM).option("--wait", "Block until the recipient occupant reaches --until (default: invoke-exit)").option("--until <state>", "Wait target for --wait: injected|invoke-exit", collectSendUntil, []).option("--timeout <ms>", "Caller deadline in milliseconds", parseTimeout2).option("--json", "Output machine-readable JSON").action(async (body, options) => {
92587
+ noun.command("send").description("Enqueue a message for an agent. Use --wait to block until the recipient reaches a state.").argument("<body>", "Message body").requiredOption("--to <id>", "Recipient agent id").option("--from <id>", "Sender id", DEFAULT_FROM).option("--wait", "Block until the recipient occupant reaches --until (default: invoke-exit)").option(...SHARED_OPTIONS.untilMessage, collectSendUntil, []).option(...SHARED_OPTIONS.timeout, parseTimeout2).option(...SHARED_OPTIONS.json).action(async (body, options) => {
91475
92588
  const svc = new TeamService(context4);
91476
92589
  const code = await runMessageSend(svc, context4, body, options);
91477
92590
  context4.setExitCode(code);
91478
92591
  });
91479
- noun.command("inbox").description("List messages addressed to an agent.").requiredOption("--agent <id>", "Agent id").option("--json", "Output machine-readable JSON").action(async (options) => {
92592
+ noun.command("inbox").description("List messages addressed to an agent.").requiredOption(...SHARED_OPTIONS.agentIdMessage).option(...SHARED_OPTIONS.json).action(async (options) => {
91480
92593
  const svc = new TeamService(context4);
91481
92594
  const code = await runMessageInbox(svc, context4, options);
91482
92595
  context4.setExitCode(code);
91483
92596
  });
91484
- noun.command("reply").description("Thread a reply to a message.").argument("<msg-id>", "Message id to reply to").argument("<body>", "Reply body").option("--json", "Output machine-readable JSON").action(async (msgId, body, options) => {
92597
+ noun.command("reply").description("Thread a reply to a message.").argument("<msg-id>", "Message id to reply to").argument("<body>", "Reply body").option(...SHARED_OPTIONS.json).action(async (msgId, body, options) => {
91485
92598
  const svc = new TeamService(context4);
91486
92599
  const code = await runMessageReply(svc, context4, msgId, body, options);
91487
92600
  context4.setExitCode(code);
91488
92601
  });
91489
- noun.command("watch").description("Follow an agent inbox \u2014 surface new messages as they arrive (Ctrl-C to exit).").requiredOption("--agent <id>", "Agent id to watch").option("--interval <ms>", "Poll interval in milliseconds", String(DEFAULT_WATCH_INTERVAL_MS)).option("--json", "Output one JSON object per new message (machine-consumable)").action(async (options) => {
92602
+ noun.command("watch").description("Follow an agent inbox \u2014 surface new messages as they arrive (Ctrl-C to exit).").requiredOption(...SHARED_OPTIONS.agentIdWatch).option("--interval <ms>", "Poll interval in milliseconds", String(DEFAULT_WATCH_INTERVAL_MS)).option(...SHARED_OPTIONS.jsonMessageStream).action(async (options) => {
91490
92603
  const svc = new TeamService(context4);
91491
92604
  const intervalMs = parseInterval2(options.interval);
91492
92605
  if (intervalMs === null) {
@@ -91769,31 +92882,31 @@ async function waitForPendingDrain(agentService, teamService, pin, timeoutMs, si
91769
92882
 
91770
92883
  // src/commands/migrate.ts
91771
92884
  init_src2();
91772
- import { join as join25 } from "path";
91773
- function registerMigrateCommand(program2, context4) {
91774
- program2.command("migrate").summary("apply CLI-owned schema migrations").option("--json", "Output machine-readable JSON").action(async (options) => {
91775
- const migrations = await loadSqlMigrations(join25(context4.cwd, "drizzle")).catch(() => {
92885
+ import { join as join27 } from "path";
92886
+ function registerMigrateCommand(program2, context4, options = {}) {
92887
+ program2.command("migrate", { hidden: options.hidden === true }).summary("apply CLI-owned schema migrations").option(...SHARED_OPTIONS.json).action(async (options2) => {
92888
+ const migrations = await loadSqlMigrations(join27(context4.cwd, "drizzle")).catch(() => {
91776
92889
  return;
91777
92890
  });
91778
92891
  const applied = await applyCliMigrations(await context4.getDb(), migrations);
91779
92892
  const result = { ok: true, applied };
91780
- context4.output.write(options.json === true ? toJson2(result) : `Database migrations complete (${applied} applied)`);
92893
+ context4.output.write(options2.json === true ? toJson2(result) : `Database migrations complete (${applied} applied)`);
91781
92894
  });
91782
92895
  }
91783
92896
 
91784
92897
  // src/commands/projects.ts
91785
92898
  init_dist5();
91786
92899
  await init_src3();
91787
- import { basename as basename8, resolve as resolve18 } from "path";
92900
+ import { basename as basename9, resolve as resolve18 } from "path";
91788
92901
  function registerProjectsCommand(program2, context4) {
91789
92902
  const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
91790
- projectsCmd.command("add").argument("<path>", "Project root directory path").option("--name <name>", "Display name for the project").option("--json", "Output JSON response").action(async (pathArg, options) => {
92903
+ projectsCmd.command("add").argument("<path>", "Project root directory path").option(...SHARED_OPTIONS.nameProjectDisplay).option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (pathArg, options) => {
91791
92904
  try {
91792
92905
  const absolutePath = resolve18(context4.cwd, pathArg);
91793
92906
  if (!await context4.fs.exists(absolutePath)) {
91794
92907
  throw new Error(`Directory does not exist: ${absolutePath}`);
91795
92908
  }
91796
- const name = options.name ?? basename8(absolutePath);
92909
+ const name = options.name ?? basename9(absolutePath);
91797
92910
  const registry2 = new ProjectRegistry;
91798
92911
  const entry = await registry2.upsert({ path: absolutePath, name, port: 0 });
91799
92912
  if (options.json) {
@@ -91811,7 +92924,7 @@ function registerProjectsCommand(program2, context4) {
91811
92924
  context4.setExitCode(1);
91812
92925
  }
91813
92926
  });
91814
- projectsCmd.command("remove").argument("<target>", "Project display name or directory path").option("--json", "Output JSON response").action(async (target, options) => {
92927
+ projectsCmd.command("remove").argument("<target>", "Project display name or directory path").option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (target, options) => {
91815
92928
  try {
91816
92929
  const registry2 = new ProjectRegistry;
91817
92930
  const removed = await registry2.remove(target);
@@ -91833,7 +92946,7 @@ function registerProjectsCommand(program2, context4) {
91833
92946
  context4.setExitCode(1);
91834
92947
  }
91835
92948
  });
91836
- projectsCmd.command("list").option("--json", "Output JSON array of projects").action(async (options) => {
92949
+ projectsCmd.command("list").option(...SHARED_OPTIONS.jsonProjectsArray).action(async (options) => {
91837
92950
  try {
91838
92951
  const registry2 = new ProjectRegistry;
91839
92952
  const rawProjects = await registry2.list();
@@ -91865,7 +92978,7 @@ function registerProjectsCommand(program2, context4) {
91865
92978
  context4.setExitCode(1);
91866
92979
  }
91867
92980
  });
91868
- projectsCmd.command("start").argument("<target>", "Project display name or path").option("--port <n>", "Explicit port to bind", parseInt).option("--json", "Output JSON response").action(async (target, options) => {
92981
+ projectsCmd.command("start").argument("<target>", "Project display name or path").option(...SHARED_OPTIONS.portProjects, parseInt).option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (target, options) => {
91869
92982
  try {
91870
92983
  const registry2 = new ProjectRegistry;
91871
92984
  const result = await startRegisteredProject(registry2, target, {
@@ -91898,7 +93011,7 @@ function registerProjectsCommand(program2, context4) {
91898
93011
  context4.setExitCode(1);
91899
93012
  }
91900
93013
  });
91901
- projectsCmd.command("stop").argument("<target>", "Project display name or path").option("--json", "Output JSON response").action(async (target, options) => {
93014
+ projectsCmd.command("stop").argument("<target>", "Project display name or path").option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (target, options) => {
91902
93015
  try {
91903
93016
  const registry2 = new ProjectRegistry;
91904
93017
  const entry = await registry2.getByName(target) ?? await registry2.getByPath(target);
@@ -91981,7 +93094,7 @@ function shouldColor(env, stream) {
91981
93094
  // src/commands/rule.ts
91982
93095
  function registerRuleCommand(program2, context4) {
91983
93096
  const rule = program2.command("rule").summary("manage constraint rules and presets");
91984
- rule.command("run").summary("Evaluate constraint rules over the working tree.").option("--preset <name>", "Preset to load (default: recommended-pre-check)", "recommended-pre-check").option("--file <path>", "Ad-hoc rule file").option("--rule <id>", "Filter run to one rule ID").option("--fail-on <severity>", "Exit 1 threshold: error|warning|info (default: error)", "error").option("--stop-on-first [severity]", "Stop evaluation after first rule with findings at/above severity").option("--fix-mode <mode>", "Fix collection/apply mode: none|suggest|auto (default: none)", "none").option("--dry-run", "Preview fixes without writing (use with --fix-mode auto)").option("--verbose", "Stream per-rule progress to stderr").option("--json", "Output machine-readable JSON").action(async (options) => {
93097
+ rule.command("run").summary("Evaluate constraint rules over the working tree.").option("--preset <name>", "Preset to load (default: recommended-pre-check)", "recommended-pre-check").option(...SHARED_OPTIONS.fileRuleAdhoc).option("--rule <id>", "Filter run to one rule ID").option("--fail-on <severity>", "Exit 1 threshold: error|warning|info (default: error)", "error").option("--stop-on-first [severity]", "Stop evaluation after first rule with findings at/above severity").option("--fix-mode <mode>", "Fix collection/apply mode: none|suggest|auto (default: none)", "none").option(...SHARED_OPTIONS.dryRunRuleFix).option(...SHARED_OPTIONS.verboseRule).option(...SHARED_OPTIONS.json).action(async (options) => {
91985
93098
  const service = new RuleService(context4);
91986
93099
  const preset = options.preset ?? "recommended-pre-check";
91987
93100
  const failOn = parseFailOn(options.failOn ?? "error");
@@ -92008,7 +93121,7 @@ function registerRuleCommand(program2, context4) {
92008
93121
  });
92009
93122
  context4.setExitCode(result.exitCode);
92010
93123
  });
92011
- rule.command("validate").summary("Validate a rule file or preset without evaluating it.").argument("[file-or-preset]", "File path or preset name to validate").option("--file <path>", "Ad-hoc rule file path").option("--preset <name>", "Preset name").option("--kind <type>", "Source kind: file or preset").option("--no-schema", "Skip schema validation").option("--json", "Output machine-readable JSON").action(async (fileOrPreset, options) => {
93124
+ rule.command("validate").summary("Validate a rule file or preset without evaluating it.").argument("[file-or-preset]", "File path or preset name to validate").option(...SHARED_OPTIONS.fileRuleAdhocPath).option("--preset <name>", "Preset name").option("--kind <type>", "Source kind: file or preset").option(...SHARED_OPTIONS.noSchema).option(...SHARED_OPTIONS.json).action(async (fileOrPreset, options) => {
92012
93125
  const service = new RuleService(context4);
92013
93126
  const source = resolveSource({ file: options.file, preset: options.preset }, fileOrPreset ? [fileOrPreset] : []);
92014
93127
  if (options.kind && (options.kind === "file" || options.kind === "preset")) {
@@ -92019,13 +93132,13 @@ function registerRuleCommand(program2, context4) {
92019
93132
  const result = await service.validate({ source, json: json3, validateSchema });
92020
93133
  context4.setExitCode(result.exitCode);
92021
93134
  });
92022
- rule.command("list").summary("List discovered rule files, or list resolved rules for a preset.").option("--preset <name>", "Preset to list rules for").option("--json", "Output machine-readable JSON").action(async (options) => {
93135
+ rule.command("list").summary("List discovered rule files, or list resolved rules for a preset.").option("--preset <name>", "Preset to list rules for").option(...SHARED_OPTIONS.json).action(async (options) => {
92023
93136
  const service = new RuleService(context4);
92024
93137
  const preset = options.preset;
92025
93138
  const result = await service.list(preset);
92026
93139
  context4.output.write(options.json ? JSON.stringify(result, null, 2) : preset === undefined ? formatRuleFileList(result) : formatPresetRuleList(result));
92027
93140
  });
92028
- rule.command("trace").summary("Show persisted rule run history.").argument("[run-id]", "Run ID for per-run detail").option("--preset <name>", "Filter by preset name").option("--status <status>", "Filter by status: done, failed").option("--since <iso-date>", "Filter runs started on or after this date").option("--last <n>", "Limit results (default 20)", "20").option("--json", "Output machine-readable JSON").action(async (runId, options) => {
93141
+ rule.command("trace").summary("Show persisted rule run history.").argument("[run-id]", "Run ID for per-run detail").option("--preset <name>", "Filter by preset name").option(...SHARED_OPTIONS.statusDoneFailed).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option(...SHARED_OPTIONS.json).action(async (runId, options) => {
92029
93142
  const last = parseInt(options.last, 10);
92030
93143
  if (!Number.isInteger(last) || last < 1) {
92031
93144
  context4.output.error("--last must be a positive integer");
@@ -92210,7 +93323,7 @@ function formatTraceDetail(detail) {
92210
93323
 
92211
93324
  // src/commands/serve.ts
92212
93325
  init_src();
92213
- import { join as join29 } from "path";
93326
+ import { join as join31 } from "path";
92214
93327
 
92215
93328
  // ../server/src/index.ts
92216
93329
  init_src();
@@ -92220,11 +93333,11 @@ init_src();
92220
93333
  init_loader();
92221
93334
  init_src2();
92222
93335
  await init_src3();
92223
- import { basename as basename10, dirname as dirname20, isAbsolute as isAbsolute6, join as join28 } from "path";
93336
+ import { basename as basename11, dirname as dirname20, isAbsolute as isAbsolute6, join as join30 } from "path";
92224
93337
  init_dist5();
92225
93338
 
92226
93339
  // ../server/src/bootstrap.ts
92227
- import { join as join26 } from "path";
93340
+ import { join as join28 } from "path";
92228
93341
 
92229
93342
  // ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
92230
93343
  var guard = (func, shouldGuard) => {
@@ -98393,7 +99506,7 @@ var featureModule = {
98393
99506
 
98394
99507
  // ../server/src/modules/health/index.ts
98395
99508
  await init_src3();
98396
- import { basename as basename9 } from "path";
99509
+ import { basename as basename10 } from "path";
98397
99510
  var startedAt = Date.now();
98398
99511
  var healthModule = {
98399
99512
  name: "health",
@@ -98419,7 +99532,7 @@ var healthModule = {
98419
99532
  return c3.json({ status: "error", db: "unreachable" }, 503);
98420
99533
  });
98421
99534
  app.get("/api/project", (c3) => {
98422
- return c3.json({ name: ctx ? basename9(ctx.cwd) : null });
99535
+ return c3.json({ name: ctx ? basename10(ctx.cwd) : null });
98423
99536
  });
98424
99537
  app.get("/api/projects", async (c3) => {
98425
99538
  if (!ctx) {
@@ -101159,7 +102272,7 @@ function createApp(appRt, opts) {
101159
102272
  app.use("*", async (c3, next) => {
101160
102273
  const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
101161
102274
  try {
101162
- const file2 = Bun.file(join26(webDistPath, pathname));
102275
+ const file2 = Bun.file(join28(webDistPath, pathname));
101163
102276
  if (await file2.exists()) {
101164
102277
  const headers = new Headers({ "content-type": file2.type });
101165
102278
  return new Response(file2.stream(), { headers });
@@ -101172,7 +102285,7 @@ function createApp(appRt, opts) {
101172
102285
  return c3.json({ error: "Not Found" }, 404);
101173
102286
  }
101174
102287
  try {
101175
- const indexFile = Bun.file(join26(webDistPath, "index.html"));
102288
+ const indexFile = Bun.file(join28(webDistPath, "index.html"));
101176
102289
  if (await indexFile.exists()) {
101177
102290
  return new Response(indexFile.stream(), {
101178
102291
  headers: { "content-type": "text/html; charset=utf-8" }
@@ -101193,7 +102306,7 @@ init_src();
101193
102306
  init_src2();
101194
102307
  init_dist5();
101195
102308
  await init_src3();
101196
- import { dirname as dirname19, join as join27 } from "path";
102309
+ import { dirname as dirname19, join as join29 } from "path";
101197
102310
  var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
101198
102311
 
101199
102312
  class LazyPlanningEventEmitter {
@@ -101228,7 +102341,7 @@ class NotConfiguredError extends Error {
101228
102341
  function createServerContext(appRt, options) {
101229
102342
  const cwd = options.cwd;
101230
102343
  const fs3 = options.fs;
101231
- const dbUrl = options.dbUrl ?? join27(cwd, DEFAULT_DATABASE_URL);
102344
+ const dbUrl = options.dbUrl ?? join29(cwd, DEFAULT_DATABASE_URL);
101232
102345
  const eventsBus = options.eventsBus ?? appRt.events;
101233
102346
  const jobQueueEnabled = options.jobQueueEnabled ?? false;
101234
102347
  const eventProjectContext = systemEventProjectContext(cwd);
@@ -101479,7 +102592,7 @@ async function loadServerSectionMatrix() {
101479
102592
  }
101480
102593
  const root = bundledConfigRoot();
101481
102594
  if (root !== null) {
101482
- const matrixPath = join28(root, "tasks", "section-matrix.yaml");
102595
+ const matrixPath = join30(root, "tasks", "section-matrix.yaml");
101483
102596
  if (await nodeFs.exists(matrixPath)) {
101484
102597
  return await loadStructuredSpurConfig(matrixPath, {
101485
102598
  validateJsonSchema: false
@@ -101488,7 +102601,7 @@ async function loadServerSectionMatrix() {
101488
102601
  }
101489
102602
  throw new Error(`no canonical section-matrix found for task creation (F92 R1); tried:
101490
102603
  ` + ` - ${localPath}
101491
- ` + (root !== null ? ` - ${join28(root, "tasks", "section-matrix.yaml")}
102604
+ ` + (root !== null ? ` - ${join30(root, "tasks", "section-matrix.yaml")}
101492
102605
  ` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
101493
102606
  }
101494
102607
  var SYSTEM_EVENTS_PRUNE_JOB = "system-events-prune";
@@ -101623,15 +102736,15 @@ async function handleFeatureActionJob(ctx, env, payload) {
101623
102736
  await runFeatureActionJob(ctx, env, payload);
101624
102737
  }
101625
102738
  async function resolveWebDistPath(configuredPath) {
101626
- const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute6(configuredPath) ? configuredPath : join28(process.cwd(), configuredPath)] : [
101627
- join28(process.cwd(), "dist/web"),
101628
- join28(import.meta.dir, "web"),
101629
- join28(dirname20(process.execPath), "web"),
101630
- join28(dirname20(process.execPath), "../web"),
101631
- join28(import.meta.dir, "../../../dist/web")
102739
+ const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute6(configuredPath) ? configuredPath : join30(process.cwd(), configuredPath)] : [
102740
+ join30(process.cwd(), "dist/web"),
102741
+ join30(import.meta.dir, "web"),
102742
+ join30(dirname20(process.execPath), "web"),
102743
+ join30(dirname20(process.execPath), "../web"),
102744
+ join30(import.meta.dir, "../../../dist/web")
101632
102745
  ];
101633
102746
  for (const candidate of candidates) {
101634
- if (await Bun.file(join28(candidate, "index.html")).exists()) {
102747
+ if (await Bun.file(join30(candidate, "index.html")).exists()) {
101635
102748
  return candidate;
101636
102749
  }
101637
102750
  }
@@ -101736,7 +102849,7 @@ async function startServer(options, deps = defaultDeps) {
101736
102849
  });
101737
102850
  const projectRegistry = new ProjectRegistry;
101738
102851
  const projectCwd = process.cwd();
101739
- const projectName = basename10(projectCwd);
102852
+ const projectName = basename11(projectCwd);
101740
102853
  try {
101741
102854
  await projectRegistry.upsert({ path: projectCwd, name: projectName, port: server.port });
101742
102855
  } catch (err) {
@@ -101798,18 +102911,18 @@ if (false) {}
101798
102911
 
101799
102912
  // src/commands/serve.ts
101800
102913
  function resolveServeDbUrl(cwd, env, configuredUrl) {
101801
- return env.DATABASE_URL === undefined ? join29(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
102914
+ return env.DATABASE_URL === undefined ? join31(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
101802
102915
  }
101803
- function registerServeCommand(program2, context4) {
101804
- program2.command("serve").summary("start the Spur web server (local fallback)").option("--port <n>", "Server port (env: PORT, default: 3000)", parseInt).option("--host <addr>", "Bind address (env: HOST, default: localhost)").option("--no-open", "Skip opening the browser").option("--cwd <path>", "Working directory", context4.cwd).option("--json", "Output { port, url, pid } and exit").action(async (options) => {
102916
+ function registerServeCommand(program2, context4, options = {}) {
102917
+ program2.command("serve", { hidden: options.hidden === true }).summary("start the Spur web server (local fallback)").option(...SHARED_OPTIONS.portServe, parseInt).option("--host <addr>", "Bind address (env: HOST, default: localhost)").option("--no-open", "Skip opening the browser").option(...SHARED_OPTIONS.cwdServe, context4.cwd).option(...SHARED_OPTIONS.jsonServePortUrl).action(async (options2) => {
101805
102918
  try {
101806
102919
  const env = process.env;
101807
102920
  const config4 = buildConfigFromEnv(env);
101808
- const port = options.port ?? config4.server.port;
101809
- const host = options.host ?? config4.server.host;
101810
- const cwd = options.cwd ?? context4.cwd;
102921
+ const port = options2.port ?? config4.server.port;
102922
+ const host = options2.host ?? config4.server.host;
102923
+ const cwd = options2.cwd ?? context4.cwd;
101811
102924
  const dbUrl = resolveServeDbUrl(cwd, env, config4.database.url);
101812
- if (options.json) {
102925
+ if (options2.json) {
101813
102926
  context4.output.write(toJson2({
101814
102927
  port,
101815
102928
  url: `http://${host}:${port}`,
@@ -101823,7 +102936,7 @@ function registerServeCommand(program2, context4) {
101823
102936
  port,
101824
102937
  host,
101825
102938
  dbUrl,
101826
- openBrowser: options.open ?? true,
102939
+ openBrowser: options2.open ?? true,
101827
102940
  webDistPath: config4.server.webDistPath
101828
102941
  });
101829
102942
  } catch (err) {
@@ -101837,7 +102950,7 @@ function registerServeCommand(program2, context4) {
101837
102950
  }
101838
102951
 
101839
102952
  // src/commands/status.ts
101840
- import { join as join30 } from "path";
102953
+ import { join as join32 } from "path";
101841
102954
 
101842
102955
  // src/errors.ts
101843
102956
  class CommandError extends Error {
@@ -101895,10 +103008,10 @@ async function gitContext(cwd) {
101895
103008
  }
101896
103009
 
101897
103010
  // src/commands/status.ts
101898
- function registerStatusCommand(program2, context4) {
101899
- program2.command("status").summary("show project, Git, and optional path status").option("--json", "Output machine-readable JSON").argument("[path]", "Optional file/dir path to check").action(async (path9, options) => {
103011
+ function registerStatusCommand(program2, context4, options = {}) {
103012
+ program2.command("status", { hidden: options.hidden === true }).summary("show project, Git, and optional path status").option(...SHARED_OPTIONS.json).argument("[path]", "Optional file/dir path to check").action(async (path9, options2) => {
101900
103013
  try {
101901
- const code = await runStatusCore(path9, options, context4);
103014
+ const code = await runStatusCore(path9, options2, context4);
101902
103015
  context4.setExitCode(code);
101903
103016
  } catch (err) {
101904
103017
  context4.output.error(err instanceof Error ? err.message : String(err));
@@ -101907,9 +103020,9 @@ function registerStatusCommand(program2, context4) {
101907
103020
  });
101908
103021
  }
101909
103022
  async function runStatusCore(path9, options, context4) {
101910
- const [packageJsonExists, spurConfigExists, git2, agentSpecs] = await Promise.all([
101911
- context4.fs.exists(join30(context4.cwd, "package.json")),
101912
- context4.fs.exists(join30(context4.cwd, ".spur", "config.yaml")),
103023
+ const [packageJsonExists, spurConfigExists, git3, agentSpecs] = await Promise.all([
103024
+ context4.fs.exists(join32(context4.cwd, "package.json")),
103025
+ context4.fs.exists(join32(context4.cwd, ".spur", "config.yaml")),
101913
103026
  gitContext(context4.cwd),
101914
103027
  listAgentSpecIds(context4)
101915
103028
  ]);
@@ -101918,7 +103031,7 @@ async function runStatusCore(path9, options, context4) {
101918
103031
  ok: spurConfigExists,
101919
103032
  packageJson: packageJsonExists,
101920
103033
  spurConfig: spurConfigExists,
101921
- git: git2,
103034
+ git: git3,
101922
103035
  agentSpecs,
101923
103036
  ...target === undefined ? {} : { target }
101924
103037
  };
@@ -101929,7 +103042,7 @@ async function runStatusCore(path9, options, context4) {
101929
103042
  `Project: ${spurConfigExists ? "ok" : "missing .spur/config.yaml"}`,
101930
103043
  `Package: ${packageJsonExists ? "ok" : "none"}`,
101931
103044
  `Agents: ${agentSpecs.length === 0 ? "none" : agentSpecs.join(", ")}`,
101932
- `Git: ${git2.root === null ? "none" : `${git2.branch ?? "detached"}${git2.dirty ? " dirty" : " clean"}`}`,
103045
+ `Git: ${git3.root === null ? "none" : `${git3.branch ?? "detached"}${git3.dirty ? " dirty" : " clean"}`}`,
101933
103046
  ...target === undefined ? [] : [`Path: ${target.path} ${target.size} bytes`]
101934
103047
  ].join(`
101935
103048
  `));
@@ -101937,14 +103050,14 @@ async function runStatusCore(path9, options, context4) {
101937
103050
  return status.ok ? 0 : 1;
101938
103051
  }
101939
103052
  async function listAgentSpecIds(context4) {
101940
- const dir = join30(context4.cwd, ".spur", "agents");
103053
+ const dir = join32(context4.cwd, ".spur", "agents");
101941
103054
  if (!await context4.fs.exists(dir))
101942
103055
  return [];
101943
103056
  const entries = await context4.fs.readDir(dir);
101944
103057
  return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
101945
103058
  }
101946
103059
  async function readTargetStatus(context4, targetPath) {
101947
- const resolved = join30(context4.cwd, targetPath);
103060
+ const resolved = join32(context4.cwd, targetPath);
101948
103061
  const stat = await context4.fs.stat(resolved);
101949
103062
  if (stat === null)
101950
103063
  throw new CommandError(`status failed: path does not exist at ${resolved}`);
@@ -101956,8 +103069,8 @@ init_loader();
101956
103069
  init_src2();
101957
103070
  init_dist5();
101958
103071
  await init_src3();
101959
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
101960
- import { dirname as dirname21, join as join31 } from "path";
103072
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
103073
+ import { dirname as dirname21, join as join33 } from "path";
101961
103074
  // schemas/section-matrix.schema.json
101962
103075
  var section_matrix_schema_default = {
101963
103076
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -102861,7 +103974,7 @@ function renderMigrationReport(report, dryRun, corpusDir) {
102861
103974
  }
102862
103975
  function registerTaskCommand(program2, context4) {
102863
103976
  const task = program2.command("task").summary("manage tasks");
102864
- task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
103977
+ task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(...SHARED_OPTIONS.featureTrace).option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option(...SHARED_OPTIONS.folderTasks).option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option(...SHARED_OPTIONS.json).action(async (title2, options) => {
102865
103978
  if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
102866
103979
  context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
102867
103980
  context4.setExitCode(2);
@@ -102925,7 +104038,7 @@ function registerTaskCommand(program2, context4) {
102925
104038
  }
102926
104039
  }
102927
104040
  });
102928
- task.command("show").alias("get").summary("Show a task by WBS.").argument("<wbs>", "Task WBS number").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104041
+ task.command("show").alias("get").summary("Show a task by WBS.").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
102929
104042
  const svc = await makeService2(context4, options.folder);
102930
104043
  try {
102931
104044
  const result = await svc.show(wbs);
@@ -102953,7 +104066,7 @@ ${result.content}`);
102953
104066
  "See the gate checklist (spur-dev/references/gate-checklists.md).",
102954
104067
  "Valid section names (no failed write): `spur task sections <wbs> list`."
102955
104068
  ].join(`
102956
- `)).option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--feature <id>", "Set the feature_id frontmatter field (traceability edge)").option("--priority <p>", "Set the priority frontmatter field (P0\u2013P3)").option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--ac-altitude <mode>", "Set the ac_altitude frontmatter field. Valid: `graduating` (default; feature-AC subset rule enforced) or `task-local` (skip the DD-09 subset rule \u2014 task scenarios are intentionally not feature ship criteria). Mirrors the L1 schema enum (packages/domain/src/planning/schema.ts:304).").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, status, options) => {
104069
+ `)).option(...SHARED_OPTIONS.section).option(...SHARED_OPTIONS.fromFile).option(...SHARED_OPTIONS.featureFrontmatter).option(...SHARED_OPTIONS.prioritySet).option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--ac-altitude <mode>", "Set the ac_altitude frontmatter field. Valid: `graduating` (default; feature-AC subset rule enforced) or `task-local` (skip the DD-09 subset rule \u2014 task scenarios are intentionally not feature ship criteria). Mirrors the L1 schema enum (packages/domain/src/planning/schema.ts:304).").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, status, options) => {
102957
104070
  const svc = await makeService2(context4, options.folder, options.lifecycle === false);
102958
104071
  try {
102959
104072
  if (options.section !== undefined) {
@@ -103001,7 +104114,7 @@ ${result.content}`);
103001
104114
  let forcedDoneVerdict;
103002
104115
  if (status === "done") {
103003
104116
  const current = await svc.show(wbs);
103004
- const verdictDir = options.verdictDir ?? join31(context4.cwd, ".spur", "run");
104117
+ const verdictDir = options.verdictDir ?? join33(context4.cwd, ".spur", "run");
103005
104118
  const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
103006
104119
  const guardOutcome = evaluateDoneTransition({
103007
104120
  wbs,
@@ -103083,7 +104196,7 @@ ${result.content}`);
103083
104196
 
103084
104197
  ` + `Validation: WBS format, existence, self-edge, duplicates, and cycle detection
103085
104198
  ` + `all run BEFORE any write (atomic \u2014 R2). Exit codes: 0 success, 1 generic error,
103086
- ` + "2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: set | add | remove | clear").argument("[values...]", "WBS values (required for set/add/remove; forbidden for clear)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, op, values, options) => {
104199
+ ` + "2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: set | add | remove | clear").argument("[values...]", "WBS values (required for set/add/remove; forbidden for clear)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, op, values, options) => {
103087
104200
  const allowedOps = ["set", "add", "remove", "clear"];
103088
104201
  if (!allowedOps.includes(op)) {
103089
104202
  context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
@@ -103123,7 +104236,7 @@ ${result.content}`);
103123
104236
  ` + `(${UNIVERSAL_SECTIONS.join(", ")}) are always allowed. All writes go through the
103124
104237
  ` + `existing planning-write-service.updateSection pipeline \u2014 phantom-section guards,
103125
104238
  ` + `atomic writes, history, and timestamps are inherited. Exit codes: 0 success,
103126
- ` + "1 generic error, 2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: init | add | list").argument("[name]", "Canonical section name (required for add; forbidden for init/list)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, op, name, options) => {
104239
+ ` + "1 generic error, 2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: init | add | list").argument("[name]", "Canonical section name (required for add; forbidden for init/list)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, op, name, options) => {
103127
104240
  const allowedOps = ["init", "add", "list"];
103128
104241
  if (!allowedOps.includes(op)) {
103129
104242
  context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
@@ -103170,7 +104283,7 @@ ${result.content}`);
103170
104283
  }
103171
104284
  }
103172
104285
  });
103173
- task.command("list").summary("List tasks with optional filtering.").option("--status <s>", "Filter by status").option("--phase <p>", "Filter by phase (legacy alias for --status)").option("--parent <wbs>", "Filter by parent WBS").option("--feature <id>", "Filter by linked feature ID (feature_id edge)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (options) => {
104286
+ task.command("list").summary("List tasks with optional filtering.").option(...SHARED_OPTIONS.statusFilter).option("--phase <p>", "Filter by phase (legacy alias for --status)").option("--parent <wbs>", "Filter by parent WBS").option(...SHARED_OPTIONS.featureFilterEdge).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
103174
104287
  const svc = await makeService2(context4, options.folder);
103175
104288
  try {
103176
104289
  const tasks = await svc.list({
@@ -103198,7 +104311,7 @@ ${result.content}`);
103198
104311
  context4.setExitCode(1);
103199
104312
  }
103200
104313
  });
103201
- task.command("refresh").summary("Re-scan the task corpus and report counts (kanban.md retired \u2014 A17 cutover).").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (options) => {
104314
+ task.command("refresh").summary("Re-scan the task corpus and report counts (kanban.md retired \u2014 A17 cutover).").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
103202
104315
  const svc = await makeService2(context4, options.folder);
103203
104316
  try {
103204
104317
  const result = await svc.refresh();
@@ -103212,7 +104325,7 @@ ${result.content}`);
103212
104325
  context4.setExitCode(1);
103213
104326
  }
103214
104327
  });
103215
- task.command("migrate").summary("Run the one-time A17 task corpus normalization pass.").option("--dry-run", "Produce the full report without writing files").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (options) => {
104328
+ task.command("migrate").summary("Run the one-time A17 task corpus normalization pass.").option(...SHARED_OPTIONS.dryRunTaskReport).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
103216
104329
  try {
103217
104330
  const foldersConfig = (await resolvePlanningFolders(context4.fs)).foldersConfig;
103218
104331
  const activeFolder = context4.fs.resolve(foldersConfig.active_folder);
@@ -103230,7 +104343,7 @@ ${result.content}`);
103230
104343
  context4.setExitCode(1);
103231
104344
  }
103232
104345
  });
103233
- task.command("migrate-anchors").summary("Qualify in-repo evidence anchors to repo-relative paths (0583 R1\u2013R3).").option("--dry-run", "Produce the full report without writing files").option("--json", "Output machine-readable JSON").action(async (options) => {
104346
+ task.command("migrate-anchors").summary("Qualify in-repo evidence anchors to repo-relative paths (0583 R1\u2013R3).").option(...SHARED_OPTIONS.dryRunTaskReport).option(...SHARED_OPTIONS.json).action(async (options) => {
103234
104347
  try {
103235
104348
  const dryRun = options.dryRun === true;
103236
104349
  const report = await anchorQualify(context4.fs, {
@@ -103275,7 +104388,7 @@ ${result.content}`);
103275
104388
  context4.setExitCode(1);
103276
104389
  }
103277
104390
  });
103278
- task.command("refresh-roster").summary("Regenerate a parent task's sub-task roster block in its ## Plan (0121 roll-up gate's generator).").argument("<wbs>", "Parent task WBS number").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104391
+ task.command("refresh-roster").summary("Regenerate a parent task's sub-task roster block in its ## Plan (0121 roll-up gate's generator).").argument("<wbs>", "Parent task WBS number").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103279
104392
  const svc = await makeService2(context4, options.folder);
103280
104393
  try {
103281
104394
  const result = await svc.refreshRoster(wbs);
@@ -103291,7 +104404,7 @@ ${result.content}`);
103291
104404
  context4.setExitCode(1);
103292
104405
  }
103293
104406
  });
103294
- task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption("--file <path>", "Path to the batch JSON file validated against task-batch.schema.json").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (options) => {
104407
+ task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption(...SHARED_OPTIONS.fileTaskBatch).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
103295
104408
  const svc = await makeService2(context4, options.folder);
103296
104409
  try {
103297
104410
  const { children, parentsWired } = await svc.batchCreate(options.file);
@@ -103335,7 +104448,7 @@ ${result.content}`);
103335
104448
  }
103336
104449
  }
103337
104450
  });
103338
- task.command("record").summary("Record pipeline results into the task file \u2014 Testing, Review, and optional Solution backfill.").argument("<wbs>", "Task WBS number").option("--verdict-file <path>", "Path to verdict JSON (default: .spur/run/<wbs>-verdict.json)").option("--solution-from-diff", "Backfill Solution from git diff when bare").option("--transition <status>", "Optional lifecycle transition (e.g. testing)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104451
+ task.command("record").summary("Record pipeline results into the task file \u2014 Testing, Review, and optional Solution backfill.").argument("<wbs>", "Task WBS number").option("--verdict-file <path>", "Path to verdict JSON (default: .spur/run/<wbs>-verdict.json)").option("--solution-from-diff", "Backfill Solution from git diff when bare").option("--transition <status>", "Optional lifecycle transition (e.g. testing)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103339
104452
  const svc = await makeService2(context4, options.folder);
103340
104453
  try {
103341
104454
  const result = await svc.record(wbs, {
@@ -103362,7 +104475,7 @@ ${result.content}`);
103362
104475
  context4.setExitCode(1);
103363
104476
  }
103364
104477
  });
103365
- task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104478
+ task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103366
104479
  const { deriveVerdict: deriveVerdict2, verdictRowsMatchScenarios: verdictRowsMatchScenarios2 } = await init_src3().then(() => exports_src2);
103367
104480
  const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
103368
104481
  let answerText;
@@ -103404,7 +104517,7 @@ ${result.content}`);
103404
104517
  context4.setExitCode(1);
103405
104518
  }
103406
104519
  });
103407
- task.command("verifyall-aggregate").summary("Aggregate per-task verify outcomes into a deterministic batch verdict (verifyall).").description("Reads a JSON array of per-task outcomes ({wbs,outcome}) and emits the batch " + "verdict with NOT-STARTED tasks excluded from the rollup. Replaces agent-discretion " + "rollup prose (dev-operations.md \xA73a) with deterministic code (task 0341).").option("--from-file <path>", "Path to JSON array of {wbs,outcome[,reason]} rows").option("--json", "Output machine-readable JSON").action(async (options) => {
104520
+ task.command("verifyall-aggregate").summary("Aggregate per-task verify outcomes into a deterministic batch verdict (verifyall).").description("Reads a JSON array of per-task outcomes ({wbs,outcome}) and emits the batch " + "verdict with NOT-STARTED tasks excluded from the rollup. Replaces agent-discretion " + "rollup prose (dev-operations.md \xA73a) with deterministic code (task 0341).").option(...SHARED_OPTIONS.fromFileOutcomeRows).option(...SHARED_OPTIONS.json).action(async (options) => {
103408
104521
  const inputPath = options.fromFile ?? ".spur/run/verifyall-batch-input.json";
103409
104522
  let raw2;
103410
104523
  try {
@@ -103451,7 +104564,7 @@ ${result.content}`);
103451
104564
  context4.setExitCode(1);
103452
104565
  }
103453
104566
  });
103454
- task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option("--strict", "Elevate ALL warnings to failures").option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option("--as <status>", "Evaluate the task AS if it were in <status> (F92 R2): the lifecycle guards pass the transition target so testing\u2192done checks the done row. Validate against canonical task statuses. Omitted \u2192 current-status diagnostics.").option("--corpus", "Sweep every task and feature against config/corpus-baseline.json").option("--since <ref>", "Scope the corpus fog check to changes since a git ref (requires --corpus)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104567
+ task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option(...SHARED_OPTIONS.strictTaskAll).option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option(...SHARED_OPTIONS.asTaskF92).option("--corpus", "Sweep every task and feature against config/corpus-baseline.json").option("--since <ref>", "Scope the corpus fog check to changes since a git ref (requires --corpus)").option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103455
104568
  const json3 = options.json === true;
103456
104569
  const strict = options.strict === true;
103457
104570
  const asStatus = options.as === undefined ? undefined : canonicalStatusOrRaw(options.as);
@@ -103465,6 +104578,11 @@ ${result.content}`);
103465
104578
  context4.setExitCode(2);
103466
104579
  return;
103467
104580
  }
104581
+ if (options.fix === true && options.corpus === true) {
104582
+ context4.output.error("--fix repairs files in place and cannot be combined with --corpus");
104583
+ context4.setExitCode(2);
104584
+ return;
104585
+ }
103468
104586
  try {
103469
104587
  if (options.corpus === true) {
103470
104588
  if (wbs !== undefined) {
@@ -103521,6 +104639,11 @@ ${result.content}`);
103521
104639
  const printResult = (result) => {
103522
104640
  if (json3)
103523
104641
  return;
104642
+ if (result.repairs !== undefined && result.repairs.length > 0) {
104643
+ for (const r2 of result.repairs) {
104644
+ context4.output.write(` [FIX] ${r2.kind} ${r2.section}: ${r2.detail}`);
104645
+ }
104646
+ }
103524
104647
  context4.output.write(`
103525
104648
  ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103526
104649
  for (const f of result.findings) {
@@ -103543,7 +104666,8 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103543
104666
  strict,
103544
104667
  asStatus,
103545
104668
  severityOverrides: planningFolders.severityOverrides,
103546
- accepted
104669
+ accepted,
104670
+ fix: options.fix === true
103547
104671
  });
103548
104672
  results.push(result);
103549
104673
  printResult(result);
@@ -103562,7 +104686,8 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103562
104686
  strict,
103563
104687
  asStatus,
103564
104688
  severityOverrides: planningFolders.severityOverrides,
103565
- accepted
104689
+ accepted,
104690
+ fix: options.fix === true
103566
104691
  });
103567
104692
  results.push(result);
103568
104693
  printResult(result);
@@ -103596,7 +104721,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103596
104721
  context4.setExitCode(1);
103597
104722
  }
103598
104723
  });
103599
- task.command("resolve").summary("Resolve a file path to its owning task WBS.").argument("<file-path>", "File path to resolve").option("--folder <path>", "Custom tasks folder").option("--strict", "Match only the exact corpus path (no basename-WBS fallback)").option("--json", "Output machine-readable JSON").action(async (filePath, options) => {
104724
+ task.command("resolve").summary("Resolve a file path to its owning task WBS.").argument("<file-path>", "File path to resolve").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.strictTaskPath).option(...SHARED_OPTIONS.json).action(async (filePath, options) => {
103600
104725
  const svc = await makeService2(context4, options.folder);
103601
104726
  try {
103602
104727
  const result = await svc.resolve(filePath, { strict: options.strict === true });
@@ -103615,7 +104740,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103615
104740
  context4.setExitCode(1);
103616
104741
  }
103617
104742
  });
103618
- task.command("path").summary("Resolve a WBS to its absolute task file path.").argument("<wbs>", "Task WBS number").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104743
+ task.command("path").summary("Resolve a WBS to its absolute task file path.").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103619
104744
  const svc = await makeService2(context4, options.folder);
103620
104745
  try {
103621
104746
  const filePath = await svc.getFilePath(wbs);
@@ -103634,7 +104759,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103634
104759
  context4.setExitCode(1);
103635
104760
  }
103636
104761
  });
103637
- task.command("run-link").summary("Record a pipeline provenance link for a task (used by --next auto chains to satisfy testing\u2192done guard).").argument("<wbs>", "Task WBS number").option("--source <source>", "Link source identifier (e.g. next-auto)", "chain").option("--run-id <id>", "Explicit run_id (auto-generated when omitted)").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104762
+ task.command("run-link").summary("Record a pipeline provenance link for a task (used by --next auto chains to satisfy testing\u2192done guard).").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.sourceLink, "chain").option(...SHARED_OPTIONS.runIdTask).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103638
104763
  try {
103639
104764
  const db2 = await context4.getDb();
103640
104765
  const ensured = await ensurePipelineRunLink(db2, wbs, {
@@ -103659,7 +104784,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
103659
104784
  context4.setExitCode(1);
103660
104785
  }
103661
104786
  });
103662
- task.command("scaffold-tests").summary("Generate BDD test stubs from task Acceptance Criteria.").argument("<wbs>", "Task WBS number").option("--file <path>", "Custom target test file path").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
104787
+ task.command("scaffold-tests").summary("Generate BDD test stubs from task Acceptance Criteria.").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.fileTaskTest).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
103663
104788
  const { TaskScaffoldService: TaskScaffoldService2, resolvePlanningFolders: resolvePlanningFolders2 } = await init_src3().then(() => exports_src2);
103664
104789
  const foldersConfig = (await resolvePlanningFolders2(context4.fs)).foldersConfig;
103665
104790
  const tasksDir = options.folder ?? context4.fs.resolve(foldersConfig.active_folder);
@@ -103711,15 +104836,15 @@ function loadTemplateBodies(projectRoot, variant) {
103711
104836
  if (cached2 !== undefined)
103712
104837
  return cached2;
103713
104838
  let bodies = {};
103714
- const localPath = join31(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
103715
- if (existsSync11(localPath)) {
103716
- bodies = extractTemplateBodies(readFileSync10(localPath, "utf8"));
104839
+ const localPath = join33(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
104840
+ if (existsSync13(localPath)) {
104841
+ bodies = extractTemplateBodies(readFileSync12(localPath, "utf8"));
103717
104842
  } else {
103718
104843
  const root = bundledConfigRoot();
103719
104844
  if (root !== null) {
103720
- const templatePath = join31(root, "templates", "task", `${variant}.md`);
103721
- if (existsSync11(templatePath)) {
103722
- bodies = extractTemplateBodies(readFileSync10(templatePath, "utf8"));
104845
+ const templatePath = join33(root, "templates", "task", `${variant}.md`);
104846
+ if (existsSync13(templatePath)) {
104847
+ bodies = extractTemplateBodies(readFileSync12(templatePath, "utf8"));
103723
104848
  }
103724
104849
  }
103725
104850
  }
@@ -103777,7 +104902,7 @@ async function loadSectionMatrixUncached(projectRoot) {
103777
104902
  }
103778
104903
  const root = bundledConfigRoot();
103779
104904
  if (root !== null) {
103780
- const matrixPath = join31(root, "tasks", "section-matrix.yaml");
104905
+ const matrixPath = join33(root, "tasks", "section-matrix.yaml");
103781
104906
  if (await fs3.exists(matrixPath)) {
103782
104907
  const data = await loadStructuredSpurConfig(matrixPath, {
103783
104908
  validateJsonSchema: true,
@@ -103788,7 +104913,7 @@ async function loadSectionMatrixUncached(projectRoot) {
103788
104913
  }
103789
104914
  throw new Error(`no canonical section-matrix found for task section authority (F92 R1); tried:
103790
104915
  ` + ` - ${localPath}
103791
- ` + (root !== null ? ` - ${join31(root, "tasks", "section-matrix.yaml")}
104916
+ ` + (root !== null ? ` - ${join33(root, "tasks", "section-matrix.yaml")}
103792
104917
  ` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
103793
104918
  }
103794
104919
 
@@ -103807,23 +104932,23 @@ function registerTeamCommand(program2, context4) {
103807
104932
  const code = await runTeamAssign(taskId, agentId, context4);
103808
104933
  context4.setExitCode(code);
103809
104934
  });
103810
- noun.command("status").description("List agent specs and their run status; --by-team groups by team (0258 R4).").option("--json", "Output machine-readable JSON").option("--by-team", "Group specs by their agent.team.<id> membership").option("--server <url>", "Server API URL for live run status", DEFAULT_SERVER).action(async (options) => {
104935
+ noun.command("status").description("List agent specs and their run status; --by-team groups by team (0258 R4).").option(...SHARED_OPTIONS.json).option("--by-team", "Group specs by their agent.team.<id> membership").option("--server <url>", "Server API URL for live run status", DEFAULT_SERVER).action(async (options) => {
103811
104936
  const code = options.byTeam ? await runTeamStatusGrouped(options, context4) : await runTeamStatus(options, context4);
103812
104937
  context4.setExitCode(code);
103813
104938
  });
103814
- noun.command("up").description("Materialize a team roster into agent specs; best-effort start when spur serve is reachable.").argument("<team>", "Team id (agent.team.<team>)").option("--check", "Dry-run: show the add/prune diff without writing").option("--server <url>", "Server API URL", DEFAULT_SERVER).option("--json", "Output machine-readable JSON").action(async (team, options) => {
104939
+ noun.command("up").description("Materialize a team roster into agent specs; best-effort start when spur serve is reachable.").argument("<team>", "Team id (agent.team.<team>)").option("--check", "Dry-run: show the add/prune diff without writing").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (team, options) => {
103815
104940
  const code = await runTeamUp(team, options, context4);
103816
104941
  context4.setExitCode(code);
103817
104942
  });
103818
- noun.command("down").description("Tear down a team: stop members; --purge also removes generated specs.").argument("<team>", "Team id").option("--purge", "Also delete spur:generated specs (never manual / ref:)").option("--server <url>", "Server API URL", DEFAULT_SERVER).option("--json", "Output machine-readable JSON").action(async (team, options) => {
104943
+ noun.command("down").description("Tear down a team: stop members; --purge also removes generated specs.").argument("<team>", "Team id").option("--purge", "Also delete spur:generated specs (never manual / ref:)").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (team, options) => {
103819
104944
  const code = await runTeamDown(team, options, context4);
103820
104945
  context4.setExitCode(code);
103821
104946
  });
103822
- noun.command("start").description("Start a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option("--json", "Output machine-readable JSON").action(async (agentId, options) => {
104947
+ noun.command("start").description("Start a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (agentId, options) => {
103823
104948
  const code = await runTeamStart(agentId, options, context4);
103824
104949
  context4.setExitCode(code);
103825
104950
  });
103826
- noun.command("stop").description("Stop a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option("--json", "Output machine-readable JSON").action(async (agentId, options) => {
104951
+ noun.command("stop").description("Stop a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (agentId, options) => {
103827
104952
  const code = await runTeamStop(agentId, options, context4);
103828
104953
  context4.setExitCode(code);
103829
104954
  });
@@ -104062,9 +105187,88 @@ init_dist6();
104062
105187
  init_dist5();
104063
105188
  await init_src3();
104064
105189
  import { closeSync as closeSync5, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync3 } from "fs";
104065
- import { join as join32, resolve as resolve19 } from "path";
105190
+ import { join as join34, resolve as resolve19 } from "path";
104066
105191
  import { createInterface } from "readline";
104067
105192
  import { setTimeout as sleep5 } from "timers/promises";
105193
+
105194
+ // src/workflow/mermaid-render.ts
105195
+ function esc2(text4) {
105196
+ return text4.replace(/"/g, "&quot;").replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
105197
+ }
105198
+ function classLine(id, cls) {
105199
+ return ` class ${esc2(id)} ${cls};`;
105200
+ }
105201
+ function renderWorkflowMermaid(def) {
105202
+ const lines = [];
105203
+ lines.push("```mermaid");
105204
+ lines.push("flowchart LR");
105205
+ lines.push(" classDef terminal fill:#d4edda,stroke:#1e7e34,color:#0a3d1f;");
105206
+ lines.push(" classDef failure fill:#f8d7da,stroke:#c62828,color:#5f1414;");
105207
+ lines.push(" classDef initial fill:#fff3cd,stroke:#b8860b,color:#5a4a00;");
105208
+ lines.push(" classDef gate fill:#e8e8f8,stroke:#5b5bd6,color:#1a1a5e;");
105209
+ lines.push(" classDef decision fill:#ffe9d1,stroke:#e07b00,color:#6b3a00;");
105210
+ lines.push(" classDef parallel fill:#f0f4f8,stroke:#2c7fb8,color:#123a55;");
105211
+ if (def.kind === "transition-flow") {
105212
+ const terminal = new Set(def.terminalNodes ?? []);
105213
+ const typeShape = {
105214
+ gate: '{{"',
105215
+ decision: '{"',
105216
+ parallel: '[("'
105217
+ };
105218
+ for (const node of def.nodes) {
105219
+ const id = node.id;
105220
+ const type = node.type ?? "action";
105221
+ const terminalNode = terminal.has(id);
105222
+ if (terminalNode) {
105223
+ lines.push(` ${esc2(id)}(["${esc2(id)}"])`);
105224
+ lines.push(classLine(id, "terminal"));
105225
+ } else if (type === "action") {
105226
+ lines.push(` ${esc2(id)}["${esc2(id)}"]`);
105227
+ } else {
105228
+ const open = typeShape[type] ?? '["';
105229
+ lines.push(` ${esc2(id)}${open}${esc2(id)}"]`);
105230
+ const cls = type === "gate" ? "gate" : type === "decision" ? "decision" : "parallel";
105231
+ lines.push(classLine(id, cls));
105232
+ }
105233
+ }
105234
+ lines.push(classLine(def.initialNode, "initial"));
105235
+ for (const edge of def.edges) {
105236
+ const label = [edge.condition !== undefined ? `cond:${edge.condition.kind}` : undefined, edge.description].filter((s3) => s3 !== undefined).join(" \xB7 ");
105237
+ lines.push(label.length > 0 ? ` ${esc2(edge.from)} -->|${esc2(label)}| ${esc2(edge.to)}` : ` ${esc2(edge.from)} --> ${esc2(edge.to)}`);
105238
+ }
105239
+ } else {
105240
+ const terminal = new Set(def.terminalStates ?? []);
105241
+ const failure = new Set(def.failureStates ?? []);
105242
+ for (const state of def.states) {
105243
+ const id = state.id;
105244
+ const isTerminal = terminal.has(id);
105245
+ const isFailure = failure.has(id);
105246
+ if (isFailure) {
105247
+ lines.push(` ${esc2(id)}["${esc2(id)}"]`);
105248
+ lines.push(classLine(id, "failure"));
105249
+ } else if (isTerminal) {
105250
+ lines.push(` ${esc2(id)}(["${esc2(id)}"])`);
105251
+ lines.push(classLine(id, "terminal"));
105252
+ } else {
105253
+ lines.push(` ${esc2(id)}["${esc2(id)}"]`);
105254
+ }
105255
+ }
105256
+ lines.push(classLine(def.initialState, "initial"));
105257
+ for (const t of def.transitions) {
105258
+ const label = [
105259
+ t.trigger !== undefined ? `trigger:${t.trigger}` : undefined,
105260
+ t.guard !== undefined ? `guard:${t.guard.kind}` : undefined,
105261
+ t.description
105262
+ ].filter((s3) => s3 !== undefined).join(" \xB7 ");
105263
+ lines.push(label.length > 0 ? ` ${esc2(t.from)} -->|${esc2(label)}| ${esc2(t.to)}` : ` ${esc2(t.from)} --> ${esc2(t.to)}`);
105264
+ }
105265
+ }
105266
+ lines.push("```");
105267
+ return lines.join(`
105268
+ `);
105269
+ }
105270
+
105271
+ // src/commands/workflow.ts
104068
105272
  function shQuote2(value2) {
104069
105273
  return `'${value2.replace(/'/g, `'\\''`)}'`;
104070
105274
  }
@@ -104160,12 +105364,22 @@ function registerWorkflowCommand(program2, context4) {
104160
105364
  } : {}
104161
105365
  });
104162
105366
  const workflow = program2.command("workflow").summary("validate and execute workflow YAML files");
104163
- workflow.command("validate").description("Validate a workflow definition.").argument("<file>", "Workflow YAML file").option("--no-schema", "Skip schema validation").option("--json", "Output machine-readable JSON where supported").action(async (file2, options) => {
105367
+ workflow.command("validate").description("Validate a workflow definition.").argument("<file>", "Workflow YAML file").option(...SHARED_OPTIONS.noSchema).option(...SHARED_OPTIONS.jsonSupported).action(async (file2, options) => {
104164
105368
  const result = await makeSvc().validate(file2, { validateSchema: options.schema });
104165
105369
  if (options.json) {
104166
105370
  context4.output.write(toJson2(result));
104167
105371
  } else if (result.valid) {
104168
105372
  context4.output.write(`workflow valid: ${result.workflow.name}`);
105373
+ const c3 = result.composition;
105374
+ if (c3 && (c3.findings.length > 0 || c3.suppressed > 0)) {
105375
+ for (const f of c3.findings) {
105376
+ const m = f.measure.kind === "shell-lines" ? `${f.measure.measured} shell lines (threshold ${f.measure.threshold})` : `${f.measure.measured} prompt chars (severity ${f.measure.severity})`;
105377
+ context4.output.error(`composition advisory: ${f.actionKey} \u2014 ${m} \u2014 ${f.recommendation}`);
105378
+ }
105379
+ if (c3.suppressed > 0) {
105380
+ context4.output.error(`composition advisory: ${c3.suppressed} shell finding(s) suppressed by baseline dispositions (guards excluded wholesale; docs/design/workflow-shell-ownership.md)`);
105381
+ }
105382
+ }
104169
105383
  } else {
104170
105384
  context4.output.error(`workflow invalid: ${result.file}
104171
105385
  ${result.errors.map((m) => ` - ${m}`).join(`
@@ -104173,7 +105387,7 @@ ${result.errors.map((m) => ` - ${m}`).join(`
104173
105387
  }
104174
105388
  context4.setExitCode(result.valid ? 0 : 1);
104175
105389
  });
104176
- workflow.command("run").description("Execute a workflow definition.").argument("<file>", "Workflow YAML file").option("--run-id <id>", "Persisted run id for workflow run").option("--vars <json>", `Per-run variable overrides as a JSON object, e.g. '{"taskId":"0042"}'`).option("--dry-run", "Validate and walk transitions without executing actions").option("--async", "Start the workflow in the background and exit immediately \u2014 monitor with `spur workflow trace <run-id>`").option("--no-plan", "Suppress the run-start plan preview (synchronous runs only)").option("--quiet", "Suppress plan and per-step progress; keep the final summary").option("--silent", "Suppress all routine output; errors still set a non-zero exit status").option("--verbose", "Include transitions and correlation diagnostics in human progress").option("--detail <level>", "Human detail level: minimal, invocation, or full").option("--trace-file", "Append a redacted schema-versioned JSONL trace under .spur/runs/workflow/").option("--no-log", "Opt out of writing the consolidated .spur/run/<RUNID>.log").option("--steer", "Accept local in-process steering commands on stdin at declared action boundaries").option("--json", "Output machine-readable JSON where supported").action(async (file2, options) => {
105390
+ workflow.command("run").description("Execute a workflow definition.").argument("<file>", "Workflow YAML file").option(...SHARED_OPTIONS.runIdWorkflow).option("--vars <json>", `Per-run variable overrides as a JSON object, e.g. '{"taskId":"0042"}'`).option(...SHARED_OPTIONS.dryRunWorkflowValidate).option("--async", "Start the workflow in the background and exit immediately \u2014 monitor with `spur workflow trace <run-id>`").option("--no-plan", "Suppress the run-start plan preview (synchronous runs only)").option("--quiet", "Suppress plan and per-step progress; keep the final summary").option("--silent", "Suppress all routine output; errors still set a non-zero exit status").option(...SHARED_OPTIONS.verboseWorkflow).option("--detail <level>", "Human detail level: minimal, invocation, or full").option("--trace-file", "Append a redacted schema-versioned JSONL trace under .spur/runs/workflow/").option("--no-log", "Opt out of writing the consolidated .spur/run/<RUNID>.log").option("--steer", "Accept local in-process steering commands on stdin at declared action boundaries").option(...SHARED_OPTIONS.jsonSupported).action(async (file2, options) => {
104177
105391
  const json3 = options.json === true;
104178
105392
  const silent = !json3 && options.silent === true;
104179
105393
  const quiet = !json3 && options.quiet === true;
@@ -104289,7 +105503,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
104289
105503
  }
104290
105504
  const runLog = options.log === false ? undefined : new WorkflowRunLogSink({
104291
105505
  bus,
104292
- dir: join32(context4.cwd, ".spur", "run"),
105506
+ dir: join34(context4.cwd, ".spur", "run"),
104293
105507
  runId,
104294
105508
  ...planPreview !== undefined ? { planPreview } : {},
104295
105509
  ...await resolveOutputLogConfig(context4.cwd)
@@ -104386,7 +105600,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
104386
105600
  context4.setExitCode(result.status === "done" ? 0 : 1);
104387
105601
  await maybeTriggerHistoryRefresh(context4, "pipeline-run", runId);
104388
105602
  });
104389
- workflow.command("continue").description("Resume a paused (HITL) workflow run. Omit run-id to resume the most recent paused run.").argument("[run-id]", "Run ID to resume (default: the most recent paused run)").option("--yes", "Skip the CLI resume confirmation (does not set the persisted HITL answer)").option("--answer <yes|no|cancel>", "Inject a HITL gate answer before guard re-evaluation (0433). Does not imply --yes.").option("--json", "Output machine-readable JSON where supported").action(async (runId, options) => {
105603
+ workflow.command("continue").description("Resume a paused (HITL) workflow run. Omit run-id to resume the most recent paused run.").argument("[run-id]", "Run ID to resume (default: the most recent paused run)").option("--yes", "Skip the CLI resume confirmation (does not set the persisted HITL answer)").option("--answer <yes|no|cancel>", "Inject a HITL gate answer before guard re-evaluation (0433). Does not imply --yes.").option(...SHARED_OPTIONS.jsonSupported).action(async (runId, options) => {
104390
105604
  const json3 = options.json === true;
104391
105605
  let hitlAnswer;
104392
105606
  if (options.answer !== undefined) {
@@ -104439,7 +105653,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
104439
105653
  ledger.unsubscribe();
104440
105654
  }
104441
105655
  });
104442
- workflow.command("clean").description("Housekeeping: finalize orphaned runs stuck in running/pending past a staleness threshold " + "(mark as failed) and reclaim retained run logs older than workflow.logRetentionDays. " + "`--logs` scopes to log reclamation only. To cancel a single live run by id, use " + "`spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes (stale-run scope only)", "30").option("--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)").option("--logs", "Scope to retained run-log reclamation only (skip stale-run finalization)").option("--dry-run", "List what would be cleaned without writing (applies to both scopes)").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
105656
+ workflow.command("clean").description("Housekeeping: finalize orphaned runs stuck in running/pending past a staleness threshold " + "(mark as failed) and reclaim retained run logs older than workflow.logRetentionDays. " + "`--logs` scopes to log reclamation only. To cancel a single live run by id, use " + "`spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes (stale-run scope only)", "30").option(...SHARED_OPTIONS.forceWorkflowClean).option("--logs", "Scope to retained run-log reclamation only (skip stale-run finalization)").option(...SHARED_OPTIONS.dryRunWorkflowClean).option(...SHARED_OPTIONS.jsonSupported).action(async (options) => {
104443
105657
  const dryRun = options.dryRun === true;
104444
105658
  const logsOnly = options.logs === true;
104445
105659
  const force = options.force === true;
@@ -104481,7 +105695,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
104481
105695
  }
104482
105696
  }
104483
105697
  });
104484
- workflow.command("cancel").description("Cancel a single non-terminal run by id (mark as failed). The bulk/stale variant is `spur workflow clean`.").argument("<run-id>", "Run id to cancel").option("--json", "Output machine-readable JSON where supported").action(async (runId, options) => {
105698
+ workflow.command("cancel").description("Cancel a single non-terminal run by id (mark as failed). The bulk/stale variant is `spur workflow clean`.").argument("<run-id>", "Run id to cancel").option(...SHARED_OPTIONS.jsonSupported).action(async (runId, options) => {
104485
105699
  const result = await makeSvc(options.json).cancel(runId);
104486
105700
  if (options.json) {
104487
105701
  context4.output.write(toJson2(result));
@@ -104499,7 +105713,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
104499
105713
  context4.output.write(`Run ${runId} already terminal (${result.status}) \u2014 no change.`);
104500
105714
  }
104501
105715
  });
104502
- workflow.command("list").description("List available workflow YAML files.").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
105716
+ workflow.command("list").description("List available workflow YAML files.").option(...SHARED_OPTIONS.jsonSupported).action(async (options) => {
104503
105717
  const paths = await resolveWorkflowPaths(context4.cwd);
104504
105718
  const result = await makeSvc().list(paths);
104505
105719
  if (options.json) {
@@ -104508,7 +105722,19 @@ Monitor with: spur workflow trace ${runId2} --follow`);
104508
105722
  context4.output.write(formatListHuman(result));
104509
105723
  }
104510
105724
  });
104511
- workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option("--status <status>", "Filter by status: done, failed, running").option("--since <iso-date>", "Filter runs started on or after this date").option("--last <n>", "Limit results (default 20)", "20").option("--follow", "Replay a run timeline and poll persisted state until it becomes terminal").option("--poll <ms>", "Follow polling interval in milliseconds", "1000").option("--output", "With --follow: stream .spur/run/<RUNID>.log instead of the DB timeline").option("--json", "Output machine-readable JSON where supported").action(async (runId, options) => {
105725
+ workflow.command("show").description("Render a workflow definition as a mermaid FSM diagram.").argument("<file>", "Workflow YAML file").action(async (file2) => {
105726
+ const filePath = resolve19(context4.cwd, file2);
105727
+ let def;
105728
+ try {
105729
+ def = await loadWorkflowDef(filePath, { validateSchema: true });
105730
+ } catch (err) {
105731
+ context4.output.error(`workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`);
105732
+ context4.setExitCode(1);
105733
+ return;
105734
+ }
105735
+ context4.output.write(renderWorkflowMermaid(def));
105736
+ });
105737
+ workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option(...SHARED_OPTIONS.statusDoneFailedRunning).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option("--follow", "Replay a run timeline and poll persisted state until it becomes terminal").option(...SHARED_OPTIONS.pollWorkflow, "1000").option("--output", "With --follow: stream .spur/run/<RUNID>.log instead of the DB timeline").option(...SHARED_OPTIONS.jsonSupported).action(async (runId, options) => {
104512
105738
  const svc = makeSvc();
104513
105739
  const last = parseInt(options.last, 10);
104514
105740
  if (Number.isNaN(last) || last < 1) {
@@ -104619,18 +105845,18 @@ function formatTraceList2(result) {
104619
105845
  `);
104620
105846
  }
104621
105847
  function formatTraceTimeline(result) {
104622
- const { run, events: events2 } = result;
104623
- const dryLabel = run.isDryRun ? " [DRY RUN]" : "";
104624
- const reasonLabel = run.failureReason ? ` \u2014 ${run.failureReason}` : "";
105848
+ const { run: run2, events: events2 } = result;
105849
+ const dryLabel = run2.isDryRun ? " [DRY RUN]" : "";
105850
+ const reasonLabel = run2.failureReason ? ` \u2014 ${run2.failureReason}` : "";
104625
105851
  const lines = [
104626
- `Run: ${run.runId} \u2014 ${run.workflowName} (${run.mode}) \u2014 ${run.status}${dryLabel}${reasonLabel}`,
104627
- `Project: ${run.project.name} (${run.project.root})`,
104628
- `Started: ${run.startedAt} Completed: ${run.completedAt ?? "unavailable"} Duration: ${run.durationMs === null ? "unavailable" : `${run.durationMs}ms`}`,
104629
- `Outcome: ${run.outcome} Events: ${events2.length}`,
105852
+ `Run: ${run2.runId} \u2014 ${run2.workflowName} (${run2.mode}) \u2014 ${run2.status}${dryLabel}${reasonLabel}`,
105853
+ `Project: ${run2.project.name} (${run2.project.root})`,
105854
+ `Started: ${run2.startedAt} Completed: ${run2.completedAt ?? "unavailable"} Duration: ${run2.durationMs === null ? "unavailable" : `${run2.durationMs}ms`}`,
105855
+ `Outcome: ${run2.outcome} Events: ${events2.length}`,
104630
105856
  ""
104631
105857
  ];
104632
- if (run.nextAction !== undefined)
104633
- lines.splice(4, 0, `Next: ${run.nextAction.label} \u2014 ${run.nextAction.value}`);
105858
+ if (run2.nextAction !== undefined)
105859
+ lines.splice(4, 0, `Next: ${run2.nextAction.label} \u2014 ${run2.nextAction.value}`);
104634
105860
  if (result.outputArtifact !== undefined) {
104635
105861
  lines.push(`Run log: ${result.outputArtifact} (tail -f for live view)`);
104636
105862
  lines.push("");
@@ -104748,7 +105974,7 @@ function readRunLogChunk(logPath, offset) {
104748
105974
  }
104749
105975
  }
104750
105976
  async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => sleep5(ms)) {
104751
- const logPath = join32(dir, ".spur", "run", `${runId}.log`);
105977
+ const logPath = join34(dir, ".spur", "run", `${runId}.log`);
104752
105978
  let offset = 0;
104753
105979
  let everRead = false;
104754
105980
  while (true) {
@@ -104811,7 +106037,7 @@ init_src();
104811
106037
  init_src2();
104812
106038
  init_dist5();
104813
106039
  await init_src3();
104814
- import { dirname as dirname22, join as join33, resolve as resolve20 } from "path";
106040
+ import { dirname as dirname22, join as join35, resolve as resolve20 } from "path";
104815
106041
  import { isatty as isatty4 } from "tty";
104816
106042
 
104817
106043
  // ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
@@ -106269,7 +107495,7 @@ function createCliContext(options) {
106269
107495
  function noopSetExitCode(_code) {}
106270
107496
  async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
106271
107497
  const config4 = buildConfigFromEnv(env);
106272
- const configuredUrl = env.DATABASE_URL === undefined ? join33(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
107498
+ const configuredUrl = env.DATABASE_URL === undefined ? join35(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
106273
107499
  const url2 = dbUrl ?? configuredUrl;
106274
107500
  if (url2 !== IN_MEMORY_DATABASE_URL) {
106275
107501
  await createNodeFileSystem3().ensureDir(dirname22(url2));
@@ -106329,15 +107555,21 @@ async function runCommandDispatch(argv, context4, output2) {
106329
107555
  });
106330
107556
  program2.option("-v, --cli-verbose", "Show internal diagnostics");
106331
107557
  registerAgentCommand(program2, context4);
107558
+ registerBuilderCommand(program2, context4);
106332
107559
  registerFeatureCommand(program2, context4);
106333
107560
  registerHistoryCommand(program2, context4);
106334
- registerInitCommand(program2, context4);
106335
107561
  registerMessageCommand(program2, context4);
106336
- registerMigrateCommand(program2, context4);
106337
107562
  registerProjectsCommand(program2, context4);
106338
107563
  registerRuleCommand(program2, context4);
106339
- registerStatusCommand(program2, context4);
106340
- registerServeCommand(program2, context4);
107564
+ const selfCommand = program2.command("self").summary("inspect and manage the Spur installation itself");
107565
+ registerInitCommand(selfCommand, context4);
107566
+ registerMigrateCommand(selfCommand, context4);
107567
+ registerServeCommand(selfCommand, context4);
107568
+ registerStatusCommand(selfCommand, context4);
107569
+ registerInitCommand(program2, context4, { hidden: true });
107570
+ registerMigrateCommand(program2, context4, { hidden: true });
107571
+ registerServeCommand(program2, context4, { hidden: true });
107572
+ registerStatusCommand(program2, context4, { hidden: true });
106341
107573
  registerTeamCommand(program2, context4);
106342
107574
  registerTaskCommand(program2, context4);
106343
107575
  registerWorkflowCommand(program2, context4);