@gobing-ai/spur 0.4.22 → 0.4.24

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.
package/spur.js CHANGED
@@ -124703,40 +124703,40 @@ var init_src3 = __esm(() => {
124703
124703
 
124704
124704
  // ../../packages/app/src/services/search-zg.ts
124705
124705
  function buildZgArgs(req, globs) {
124706
- return ["query", "--hybrid", req.query, "--limit", String(req.limit), ...globs.flatMap((glob) => ["-g", glob])];
124706
+ return [
124707
+ "--hybrid",
124708
+ req.query,
124709
+ "--limit",
124710
+ String(req.limit),
124711
+ "--preview",
124712
+ "short",
124713
+ ...globs.flatMap((glob) => ["-g", glob])
124714
+ ];
124707
124715
  }
124708
124716
  function parseZgQuery(stdout, zgVersion) {
124709
- if (!stdout.trim())
124710
- return [];
124711
- if (!stdout.includes("query groups (")) {
124712
- throw new ZgOutputError('zg query output is missing the "query groups (" header', zgVersion, stdout);
124713
- }
124714
- const countMatch = stdout.match(/^hits: (\d+)$/m);
124715
- if (!countMatch) {
124716
- throw new ZgOutputError('zg query output is missing the "hits:" count line', zgVersion, stdout);
124717
- }
124718
- const declaredCount = Number(countMatch[1] ?? "0");
124719
124717
  const hits = [];
124720
124718
  let current = null;
124721
124719
  const snippetLines = [];
124722
124720
  for (const line of stdout.split(`
124723
124721
  `)) {
124724
- if (line === "..." || !line.trim())
124722
+ const trimmed = line.trim();
124723
+ if (!trimmed || trimmed === "\u2026" || trimmed === "..." || GROUP_SEPARATOR_PATTERN.test(trimmed))
124724
+ continue;
124725
+ if (trimmed === "No matches." && hits.length === 0)
124725
124726
  continue;
124726
124727
  const header = line.match(HIT_HEADER_PATTERN);
124727
124728
  if (header) {
124728
124729
  if (current)
124729
124730
  flushSnippet(current, snippetLines);
124730
- const hitPath = header[3];
124731
- const start = header[4];
124732
- const end = header[5];
124733
- if (!hitPath || !start)
124734
- throw new ZgOutputError("zg query hit header is missing path or line fields", zgVersion, stdout);
124731
+ if (Number(header[1]) !== hits.length + 1) {
124732
+ throw new ZgOutputError(`zg query hit rank #${header[1]} is out of sequence`, zgVersion, stdout);
124733
+ }
124734
+ const start = Number(header[3]);
124735
124735
  current = {
124736
124736
  rank: hits.length + 1,
124737
- path: hitPath,
124738
- startLine: Number(start),
124739
- endLine: end ? Number(end) : Number(start),
124737
+ path: header[2] ?? "",
124738
+ startLine: start,
124739
+ endLine: header[4] ? Number(header[4]) : start,
124740
124740
  snippet: "",
124741
124741
  route: "fuzzy",
124742
124742
  matchedBy: ["fuzzy"]
@@ -124745,25 +124745,23 @@ function parseZgQuery(stdout, zgVersion) {
124745
124745
  snippetLines.length = 0;
124746
124746
  continue;
124747
124747
  }
124748
- if (!current)
124748
+ const source = current ? line.match(SOURCE_LINE_PATTERN) : null;
124749
+ if (source) {
124750
+ snippetLines.push(source[2] ?? "");
124749
124751
  continue;
124750
- const metadata = line.match(METADATA_PATTERN);
124751
- if (metadata) {
124752
+ }
124753
+ const metadata = current ? line.match(METADATA_PATTERN) : null;
124754
+ if (current && metadata) {
124752
124755
  if (metadata[1] === "heading")
124753
124756
  current.heading = metadata[2] ?? "";
124754
124757
  if (metadata[1] === "symbol")
124755
124758
  current.symbol = metadata[2] ?? "";
124756
124759
  continue;
124757
124760
  }
124758
- const source = line.match(SOURCE_LINE_PATTERN);
124759
- if (source)
124760
- snippetLines.push(source[2] ?? "");
124761
+ throw new ZgOutputError(`zg query output has an unrecognized line: ${line.slice(0, 80)}`, zgVersion, stdout);
124761
124762
  }
124762
124763
  if (current)
124763
124764
  flushSnippet(current, snippetLines);
124764
- if (hits.length !== declaredCount) {
124765
- throw new ZgOutputError(`zg query output declares hits: ${declaredCount} but ${hits.length} hit blocks were parsed`, zgVersion, stdout);
124766
- }
124767
124765
  return hits;
124768
124766
  }
124769
124767
  function flushSnippet(hit, snippetLines) {
@@ -124771,6 +124769,8 @@ function flushSnippet(hit, snippetLines) {
124771
124769
  `).slice(0, SNIPPET_MAX_CHARS);
124772
124770
  }
124773
124771
  function parseZgStatus(stdout) {
124772
+ if (/^\s*Coverage\s+Unavailable\b/m.test(stdout))
124773
+ return "missing";
124774
124774
  const firstLine = stdout.split(`
124775
124775
  `).find((line) => line.trim()) ?? "";
124776
124776
  const glyph = firstLine.trim().charAt(0);
@@ -124782,7 +124782,7 @@ function parseZgStatus(stdout) {
124782
124782
  return "unknown";
124783
124783
  return "ready";
124784
124784
  }
124785
- var ZgOutputError, SNIPPET_MAX_CHARS = 300, HIT_HEADER_PATTERN, METADATA_PATTERN, SOURCE_LINE_PATTERN;
124785
+ var ZgOutputError, SNIPPET_MAX_CHARS = 300, HIT_HEADER_PATTERN, METADATA_PATTERN, SOURCE_LINE_PATTERN, GROUP_SEPARATOR_PATTERN;
124786
124786
  var init_search_zg = __esm(() => {
124787
124787
  ZgOutputError = class ZgOutputError extends Error {
124788
124788
  zgVersion;
@@ -124794,9 +124794,10 @@ var init_search_zg = __esm(() => {
124794
124794
  this.sample = sample.slice(0, 400);
124795
124795
  }
124796
124796
  };
124797
- HIT_HEADER_PATTERN = /^#(\d+) matchedBy=(fts|vector|fts\+vector) (.+?):(\d+)(?:-(\d+))?$/;
124798
- METADATA_PATTERN = /^(heading|heading_level|scope|symbol): (.*)$/;
124799
- SOURCE_LINE_PATTERN = /^(\d+)\t(.*)$/;
124797
+ HIT_HEADER_PATTERN = /^#(\d+) (.+?):(\d+)(?:-(\d+))?$/;
124798
+ METADATA_PATTERN = /^([a-z_]+): (.*)$/;
124799
+ SOURCE_LINE_PATTERN = /^\s+(\d+): ?(.*)$/;
124800
+ GROUP_SEPARATOR_PATTERN = /^-- query \d+\/\d+: .* --$/;
124800
124801
  });
124801
124802
 
124802
124803
  // ../../packages/app/src/services/search.ts
@@ -124814,10 +124815,10 @@ async function searchStatus(root, deps = defaultSearchDeps) {
124814
124815
  if (!zgPath) {
124815
124816
  return { rg, zg: { available: false, index: "unknown", hint: ZG_INSTALL_HINT } };
124816
124817
  }
124817
- const version = await probeVersion(deps, zgPath, ["version"]);
124818
+ const version = await probeVersion(deps, zgPath, ["--version"]);
124818
124819
  let index = "unknown";
124819
124820
  try {
124820
- const result = await deps.run("zg", ["status"], { cwd: root, timeout: PROBE_TIMEOUT_MS });
124821
+ const result = await deps.run("zg", ["--status"], { cwd: root, timeout: PROBE_TIMEOUT_MS });
124821
124822
  if (result.exitCode === 0)
124822
124823
  index = parseZgStatus(result.stdout);
124823
124824
  } catch {}
@@ -124959,7 +124960,7 @@ async function runFuzzy(req, limit, globs, root, status, deps) {
124959
124960
  if (result.timedOut)
124960
124961
  return { hits: null, skipped: [{ route: "fuzzy", reason: "zg-timeout" }] };
124961
124962
  if (result.exitCode !== 0) {
124962
- if (result.stderr.includes("[INDEX_MISSING]")) {
124963
+ if (/ZG\.ENGINE\.NOT_FOUND|\[INDEX_MISSING\]/.test(result.stderr)) {
124963
124964
  return { hits: null, skipped: [{ route: "fuzzy", reason: "zg-index-missing", hint: ZG_INDEX_HINT }] };
124964
124965
  }
124965
124966
  return { hits: null, skipped: [{ route: "fuzzy", reason: "zg-failed" }] };
@@ -124968,13 +124969,13 @@ async function runFuzzy(req, limit, globs, root, status, deps) {
124968
124969
  return { hits: parseZgQuery(result.stdout, status.zg.version ?? "unknown").slice(0, limit), skipped: [] };
124969
124970
  } catch (error) {
124970
124971
  if (error instanceof ZgOutputError) {
124971
- const sample = `${error.zgVersion}: ${error.sample}`.slice(0, 300);
124972
+ const sample = `${error.zgVersion}: ${error.message} | ${error.sample}`.slice(0, 300);
124972
124973
  return { hits: null, skipped: [{ route: "fuzzy", reason: "zg-output-error", hint: sample }] };
124973
124974
  }
124974
124975
  throw error;
124975
124976
  }
124976
124977
  }
124977
- var RG_INSTALL_HINT = "brew install ripgrep", ZG_INSTALL_HINT = "bun add -g @zvec/zvec-grep", ZG_INDEX_HINT = "zg index --embedding local/potion-code-16m-v2", ZG_REFRESH_HINT = "zg index", SearchError, defaultSearchDeps, RG_TIMEOUT_MS = 1e4, ZG_QUERY_TIMEOUT_MS = 15000, PROBE_TIMEOUT_MS = 5000, SNIPPET_MAX_CHARS2 = 300;
124978
+ var RG_INSTALL_HINT = "brew install ripgrep", ZG_INSTALL_HINT = "bun add -g @zvec/zvec-grep", ZG_INDEX_HINT = "zg --index --embedding local/potion-code-16m-v2", ZG_REFRESH_HINT = "zg --index", SearchError, defaultSearchDeps, RG_TIMEOUT_MS = 1e4, ZG_QUERY_TIMEOUT_MS = 15000, PROBE_TIMEOUT_MS = 5000, SNIPPET_MAX_CHARS2 = 300;
124978
124979
  var init_search2 = __esm(() => {
124979
124980
  init_src3();
124980
124981
  init_dist5();
@@ -143843,7 +143844,7 @@ async function runBatchPlan(req, read) {
143843
143844
  }
143844
143845
  return usage();
143845
143846
  }
143846
- var BATCH_PLAN_USAGE = "usage: spur task batch-plan waves --tasks <tasks.json> | order --tasks <tasks.json> [--concurrency <n>] | task-children --letter <A-Z> --plan <plan.json>", usage = () => ({ exitCode: 2, stdout: "", stderr: `${BATCH_PLAN_USAGE}
143847
+ var BATCH_PLAN_USAGE = "usage: spur task batch-plan waves --tasks <tasks.json> | order --tasks <tasks.json> [--concurrency <n>]; spur task batch-plan-children --letter <A-Z> --plan <plan.json>", usage = () => ({ exitCode: 2, stdout: "", stderr: `${BATCH_PLAN_USAGE}
143847
143848
  ` }), invalid = (message) => ({
143848
143849
  exitCode: 1,
143849
143850
  stdout: "",
@@ -146370,7 +146371,7 @@ function runCacheCli(argv, deps = {}) {
146370
146371
  switch (cmd) {
146371
146372
  case "digest": {
146372
146373
  if (a === undefined) {
146373
- return { exitCode: 1, stdout: "", stderr: `usage: spur history anatomy digest <artifact.json>
146374
+ return { exitCode: 1, stdout: "", stderr: `usage: spur history anatomy-digest <artifact.json>
146374
146375
  ` };
146375
146376
  }
146376
146377
  let artifact;
@@ -146386,7 +146387,7 @@ function runCacheCli(argv, deps = {}) {
146386
146387
  }
146387
146388
  case "check": {
146388
146389
  if (a === undefined) {
146389
- return { exitCode: 1, stdout: "", stderr: `usage: spur history anatomy check <report.md>
146390
+ return { exitCode: 1, stdout: "", stderr: `usage: spur history anatomy-check <report.md>
146390
146391
  ` };
146391
146392
  }
146392
146393
  const result = checkReportStructure(readFileSync26(a, "utf8"));
@@ -146400,7 +146401,7 @@ ${result.problems.map((p) => `- ${p}
146400
146401
  return {
146401
146402
  exitCode: 1,
146402
146403
  stdout: "",
146403
- stderr: `usage: spur history anatomy publish <candidate.md> <target.md>
146404
+ stderr: `usage: spur history anatomy-publish <candidate.md> <target.md>
146404
146405
  `
146405
146406
  };
146406
146407
  }
@@ -146413,7 +146414,7 @@ ${result.problems.map((p) => `- ${p}
146413
146414
  return {
146414
146415
  exitCode: 1,
146415
146416
  stdout: "",
146416
- stderr: `usage: spur history anatomy assert-clean --baseline <porcelain.txt> [--expect <path>]...
146417
+ stderr: `usage: spur history anatomy-assert-clean --baseline <porcelain.txt> [--expect <path>]...
146417
146418
  `
146418
146419
  };
146419
146420
  }
@@ -146447,7 +146448,7 @@ ${result.problems.map((p) => `- ${p}
146447
146448
  return {
146448
146449
  exitCode: 1,
146449
146450
  stdout: "",
146450
- stderr: `usage: spur history anatomy paths --out <env> [--skill-dir <d>] [--helper <p>] [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>] [--focus <text>] [--recompute true|false] [--run-id <id>]
146451
+ stderr: `usage: spur history anatomy-paths --out <env> [--skill-dir <d>] [--helper <p>] [--report-dir <d>] [--date <d>] [--output <p>] [--mode <m>] [--since <s>] [--until <u>] [--focus <text>] [--recompute true|false] [--run-id <id>]
146451
146452
  `
146452
146453
  };
146453
146454
  }
@@ -146535,7 +146536,7 @@ ${reasons}`, stderr: "" };
146535
146536
  return {
146536
146537
  exitCode: 1,
146537
146538
  stdout: "",
146538
- stderr: `usage: spur history anatomy stamp --candidate <c.md> --provenance <p.json> --out <o.md>
146539
+ stderr: `usage: spur history anatomy-stamp --candidate <c.md> --provenance <p.json> --out <o.md>
146539
146540
  `
146540
146541
  };
146541
146542
  }
@@ -146555,7 +146556,7 @@ ${reasons}`, stderr: "" };
146555
146556
  return {
146556
146557
  exitCode: 1,
146557
146558
  stdout: "",
146558
- stderr: `usage: spur history anatomy refresh --report <published.md> --out <o.md> [--disposition hit]
146559
+ stderr: `usage: spur history anatomy-refresh --report <published.md> --out <o.md> [--disposition hit]
146559
146560
  `
146560
146561
  };
146561
146562
  }
@@ -146580,7 +146581,7 @@ var VALID_COMMANDS = "digest, check, paths, assert-clean, probe, stamp, refresh,
146580
146581
  var init_history_anatomy_cli = __esm(() => {
146581
146582
  init_dist5();
146582
146583
  init_history_anatomy();
146583
- PROBE_USAGE = "spur history anatomy probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]";
146584
+ PROBE_USAGE = "spur history anatomy-probe --artifact <a.json> --target <report.md> [--baseline <b.json>] [--mode daily|ad-hoc] " + "[--date <YYYY-MM-DD>] [--recompute true] [--out <prov.json>] [--skill-dir <d>] [--contract <f>] [--workflow <f>] [--helper <f>]";
146584
146585
  });
146585
146586
 
146586
146587
  // ../../packages/app/src/services/daily-summary.ts
@@ -147044,7 +147045,7 @@ async function buildDailySummary(options) {
147044
147045
  }
147045
147046
  return result;
147046
147047
  }
147047
- async function main2(argv, sink) {
147048
+ async function main(argv, sink) {
147048
147049
  const previous = io2;
147049
147050
  io2 = sink;
147050
147051
  try {
@@ -155545,6 +155546,7 @@ var SHARED_OPTIONS = {
155545
155546
  jsonProjectsResponse: ["--json", "Output JSON response"],
155546
155547
  jsonMessageStream: ["--json", "Output one JSON object per new message (machine-consumable)"],
155547
155548
  jsonServePortUrl: ["--json", "Output { port, url, pid } and exit"],
155549
+ jsonShowBranches: ["--json", "alias of --format json (prints the raw BranchTopology object)"],
155548
155550
  fromFileOutcomeRows: ["--from-file <path>", "Path to JSON array of {wbs,outcome[,reason]} rows"],
155549
155551
  asFeature0418: [
155550
155552
  "--as <status>",
@@ -155564,6 +155566,7 @@ var SHARED_OPTIONS = {
155564
155566
  dryRunWorkflowValidate: ["--dry-run", "Validate and walk transitions without executing actions"],
155565
155567
  execDryRun: ["--dry-run", "Print the resolved command(s) and run nothing"],
155566
155568
  dryRunWorkflowClean: ["--dry-run", "List what would be cleaned without writing (applies to both scopes)"],
155569
+ dryRunHistorySummary: ["--dry-run", "print the summary without writing the file"],
155567
155570
  featureTrace: ["--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation"],
155568
155571
  featureFrontmatter: ["--feature <id>", "Set the feature_id frontmatter field (traceability edge)"],
155569
155572
  featureFilterEdge: ["--feature <id>", "Filter by linked feature ID (feature_id edge)"],
@@ -155580,6 +155583,7 @@ var SHARED_OPTIONS = {
155580
155583
  "--force",
155581
155584
  "Force applying reopen proposals without confirmation; also bypasses repeated-BLOCKED suppression"
155582
155585
  ],
155586
+ forceBuilderPr: ["--force", "request a fresh review even when the current HEAD is already reviewed"],
155583
155587
  forceInitRecreate: ["--force", "Recreate files that already exist"],
155584
155588
  forceTaskPreflight: [
155585
155589
  "--force",
@@ -155765,7 +155769,7 @@ function parseDogfoodGateArgs(argv) {
155765
155769
  return { testee, maxRetryPresent, steps, json, help };
155766
155770
  }
155767
155771
  var DOGFOOD_GATE_USAGE = `Usage:
155768
- spur agent dogfood gate --testee "<testee string>" [--max-retry-present] [--steps "s1||s2"] [--json]
155772
+ spur agent dogfood-gate --testee "<testee string>" [--max-retry-present] [--steps "s1||s2"] [--json]
155769
155773
 
155770
155774
  Exit codes:
155771
155775
  0 proceed (stdout may carry implement-heavy advisory)
@@ -155999,7 +156003,7 @@ function parseDogfoodValidateArgs(argv) {
155999
156003
  return { file, json, help };
156000
156004
  }
156001
156005
  var DOGFOOD_VALIDATE_USAGE = `Usage:
156002
- spur agent dogfood validate-report --file <report.md> [--json]
156006
+ spur agent dogfood-validate-report --file <report.md> [--json]
156003
156007
 
156004
156008
  Exit codes:
156005
156009
  0 report validates clean (complete-report shape)
@@ -156058,8 +156062,7 @@ function writeResult(context, result) {
156058
156062
  context.setExitCode(result.exitCode);
156059
156063
  }
156060
156064
  function registerAgentDogfoodCommand(agent, context) {
156061
- const dogfood = agent.command("dogfood").summary("dogfood-testing gates: pipeline-driving refuse and report contract");
156062
- dogfood.command("gate").summary("Refuse a pipeline-driving testee without --max-retry (dogfood Phase 1.0 / W8)").description(DOGFOOD_GATE_USAGE).option("--testee <string>", "the testee invocation under evaluation").option("--max-retry-present", "operator already supplied an explicit --max-retry").option("--steps <labels>", "derived step labels joined by || (Phase 1 W8 re-check)").option(...SHARED_OPTIONS.json).argument("[testee]", "positional alias for --testee").action((positional, options) => {
156065
+ agent.command("dogfood-gate").summary("Refuse a pipeline-driving testee without --max-retry (dogfood Phase 1.0 / W8)").description(DOGFOOD_GATE_USAGE).option("--testee <string>", "the testee invocation under evaluation").option("--max-retry-present", "operator already supplied an explicit --max-retry").option("--steps <labels>", "derived step labels joined by || (Phase 1 W8 re-check)").option(...SHARED_OPTIONS.json).argument("[testee]", "positional alias for --testee").action((positional, options) => {
156063
156066
  const argv = [];
156064
156067
  const testee = options.testee ?? positional;
156065
156068
  if (testee)
@@ -156072,7 +156075,7 @@ function registerAgentDogfoodCommand(agent, context) {
156072
156075
  argv.push("--json");
156073
156076
  writeResult(context, runDogfoodGateCli(argv));
156074
156077
  });
156075
- dogfood.command("validate-report").summary("Validate a dogfood report against the @1.2 contract").description(DOGFOOD_VALIDATE_USAGE).option("--file <path>", "report markdown path").option(...SHARED_OPTIONS.json).argument("[file]", "positional alias for --file").action(async (positional, options) => {
156078
+ agent.command("dogfood-validate-report").summary("Validate a dogfood report against the @1.2 contract").description(DOGFOOD_VALIDATE_USAGE).option("--file <path>", "report markdown path").option(...SHARED_OPTIONS.json).argument("[file]", "positional alias for --file").action(async (positional, options) => {
156076
156079
  const argv = [];
156077
156080
  const file = options.file ?? positional;
156078
156081
  if (file)
@@ -157401,6 +157404,9 @@ Recover with: ${err.recoveryCommand}` : err.message;
157401
157404
  });
157402
157405
  }
157403
157406
 
157407
+ // src/commands/task/batch-plan.ts
157408
+ init_pipeline_steps();
157409
+
157404
157410
  // src/commands/task/step-output.ts
157405
157411
  function writeStepText(output, stream, text) {
157406
157412
  if (text === "")
@@ -157415,26 +157421,42 @@ function writeStepText(output, stream, text) {
157415
157421
 
157416
157422
  // src/commands/task/batch-plan.ts
157417
157423
  function registerTaskBatchPlanCommand(task, context) {
157418
- task.command("batch-plan").summary("Project a task batch: visible plan waves, dependency order, or a task row\u2019s phase digits.").description([
157424
+ const run = async (req) => {
157425
+ await Promise.resolve().then(() => init_pipeline_steps());
157426
+ const outcome = await runBatchPlan(req, (path) => context.fs.readFile(context.fs.resolve(path)));
157427
+ writeStepText(context.output, "stdout", outcome.stdout);
157428
+ writeStepText(context.output, "stderr", outcome.stderr);
157429
+ context.setExitCode(outcome.exitCode);
157430
+ };
157431
+ const childrenReq = (options) => ({
157432
+ mode: "task-children",
157433
+ ...options.letter !== undefined ? { letter: options.letter } : {},
157434
+ ...options.plan !== undefined ? { plan: options.plan } : {}
157435
+ });
157436
+ task.command("batch-plan").summary("Project a task batch: visible plan waves or dependency order.").description([
157419
157437
  "Batch plan projection (JSON on stdout):",
157420
157438
  " waves --tasks <file> {wbs,name}[] \u2192 { waves } (A Prepare batch, B..Y tasks, Z report)",
157421
157439
  " order --tasks <file> [--concurrency n] {wbs,dependencies?,files?}[] \u2192 { order, waves, cycle, overlaps, mode, reason }",
157422
- " task-children --letter <A-Z> --plan <file> todo --json payload (or bare plan) \u2192 phase digits",
157423
157440
  "Exit: 0 success, 1 unreadable/invalid input, 2 usage error."
157424
157441
  ].join(`
157425
- `)).argument("<mode>", "waves | order | task-children").option("--tasks <file>", "tasks JSON file (waves, order)").option("--concurrency <n>", "parallel slots for order (default 2)").option("--letter <letter>", "task row letter A-Z (task-children)").option("--plan <file>", "todo-format workflow plan payload or bare plan array (task-children)").action(async (mode, options) => {
157426
- await Promise.resolve().then(() => init_pipeline_steps());
157427
- const outcome = await runBatchPlan({
157442
+ `)).argument("<mode>", "waves | order").option("--tasks <file>", "tasks JSON file").option("--concurrency <n>", "parallel slots for order (default 2)").action(async (mode, options) => {
157443
+ if (mode !== "waves" && mode !== "order") {
157444
+ context.output.error(BATCH_PLAN_USAGE);
157445
+ context.setExitCode(2);
157446
+ return;
157447
+ }
157448
+ await run({
157428
157449
  mode,
157429
157450
  ...options.tasks !== undefined ? { tasks: options.tasks } : {},
157430
- ...options.concurrency !== undefined ? { concurrency: options.concurrency } : {},
157431
- ...options.letter !== undefined ? { letter: options.letter } : {},
157432
- ...options.plan !== undefined ? { plan: options.plan } : {}
157433
- }, (path) => context.fs.readFile(context.fs.resolve(path)));
157434
- writeStepText(context.output, "stdout", outcome.stdout);
157435
- writeStepText(context.output, "stderr", outcome.stderr);
157436
- context.setExitCode(outcome.exitCode);
157451
+ ...options.concurrency !== undefined ? { concurrency: options.concurrency } : {}
157452
+ });
157437
157453
  });
157454
+ task.command("batch-plan-children").summary("Project one task row\u2019s phase digits from the visible batch plan.").description([
157455
+ "Task-row phase digits (JSON on stdout) from a `spur workflow show --format todo --json`",
157456
+ "payload or a bare plan array.",
157457
+ "Exit: 0 success, 1 unreadable/invalid input, 2 usage error."
157458
+ ].join(`
157459
+ `)).option("--letter <letter>", "task row letter A-Z").option("--plan <file>", "todo-format workflow plan payload or bare plan array").allowExcessArguments(false).action((options) => run(childrenReq(options)));
157438
157460
  }
157439
157461
 
157440
157462
  // src/commands/task/check.ts
@@ -160986,6 +161008,98 @@ async function publishPackage(target, opts = {}) {
160986
161008
  }
160987
161009
  }
160988
161010
 
161011
+ // src/commands/builder/bump-ver.ts
161012
+ function registerBuilderBumpVerCommand(noun, context) {
161013
+ noun.command("bump-ver").summary("bump a package version, commit, tag, optionally push").description([
161014
+ "Bump one workspace package (or every released package with --all),",
161015
+ "rewrite workspace pins, commit, and tag. A bare version bumps all:",
161016
+ " spur builder bump-ver 0.1.4 # every released package",
161017
+ " spur builder bump-ver spur 0.1.4 # one package by id",
161018
+ " spur builder bump-ver --check spur # pre-publish drift guard (read-only)",
161019
+ "--check fails (exit 1) when .claude-plugin/marketplace.json or a listed plugin.json",
161020
+ "drifted from the package version; target defaults to the aggregate package."
161021
+ ].join(`
161022
+ `)).option("--check", "only verify marketplace/plugin versions match the package version (no writes)").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).option(...SHARED_OPTIONS.jsonEnvelope).argument("[target]", "package id, or the version itself when bumping all").argument("[version]", "target semver version").action(async (target, version, options) => {
161023
+ const args = [target, version].filter((value) => value !== undefined);
161024
+ if (options.all === true)
161025
+ args.push("--all");
161026
+ if (options.push === true)
161027
+ args.push("--push");
161028
+ try {
161029
+ if (options.check === true) {
161030
+ if (version !== undefined)
161031
+ throw new Error("bump-ver --check takes only a package id");
161032
+ if (options.all === true || options.push === true)
161033
+ throw new Error("bump-ver --check rejects incompatible write flags (--push/--all)");
161034
+ const code = await checkVersionDrift(target, context.cwd, context.output, context.spurConfig);
161035
+ if (options.json === true) {
161036
+ context.output.write(toEnvelopeJson({ ok: code === 0, verb: "bump-ver", check: true, target: target ?? "aggregate" }, { enveloped: options.jsonEnvelope }));
161037
+ }
161038
+ if (code !== 0)
161039
+ context.setExitCode(code);
161040
+ return;
161041
+ }
161042
+ await bumpVer(args, context.cwd, context.output, context.spurConfig);
161043
+ if (options.json === true) {
161044
+ context.output.write(toEnvelopeJson({ ok: true, verb: "bump-ver", target: target ?? "all", version: version ?? "" }, { enveloped: options.jsonEnvelope }));
161045
+ }
161046
+ } catch (err) {
161047
+ const message = err instanceof Error ? err.message : String(err);
161048
+ if (options.json === true) {
161049
+ context.output.write(toEnvelopeJson({ ok: false, verb: "bump-ver", error: message }, {
161050
+ enveloped: options.jsonEnvelope,
161051
+ error: {
161052
+ code: "INTERNAL_ERROR",
161053
+ message,
161054
+ details: { cliCode: "bump-ver", verb: "bump-ver" }
161055
+ }
161056
+ }));
161057
+ context.setExitCode(1);
161058
+ return;
161059
+ }
161060
+ context.output.error(message);
161061
+ context.setExitCode(1);
161062
+ }
161063
+ });
161064
+ }
161065
+
161066
+ // src/commands/builder/drop-tags.ts
161067
+ function registerBuilderDropTagsCommand(noun, context) {
161068
+ noun.command("drop-tags").summary("delete release tags locally and optionally on origin").description([
161069
+ "Delete one package release tag (or every released tag plus the aggregate",
161070
+ "with --all / a bare version). Use --remote to also delete on origin."
161071
+ ].join(`
161072
+ `)).option("--all", "drop every released tag plus the aggregate tag").option("--remote", "also delete the tag(s) on origin").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).argument("[target]", "package id, or the version itself when dropping all").argument("[version]", "semver version of the tags to drop").action(async (target, version, options) => {
161073
+ const args = [target, version].filter((value) => value !== undefined);
161074
+ if (options.all === true)
161075
+ args.push("--all");
161076
+ if (options.remote === true)
161077
+ args.push("--remote");
161078
+ try {
161079
+ await dropTags(args, context.cwd, context.output, context.spurConfig);
161080
+ if (options.json === true) {
161081
+ context.output.write(toEnvelopeJson({ ok: true, verb: "drop-tags", target: target ?? "all", version: version ?? "" }, { enveloped: options.jsonEnvelope }));
161082
+ }
161083
+ } catch (err) {
161084
+ const message = err instanceof Error ? err.message : String(err);
161085
+ if (options.json === true) {
161086
+ context.output.write(toEnvelopeJson({ ok: false, verb: "drop-tags", error: message }, {
161087
+ enveloped: options.jsonEnvelope,
161088
+ error: {
161089
+ code: "INTERNAL_ERROR",
161090
+ message,
161091
+ details: { cliCode: "drop-tags", verb: "drop-tags" }
161092
+ }
161093
+ }));
161094
+ context.setExitCode(1);
161095
+ return;
161096
+ }
161097
+ context.output.error(message);
161098
+ context.setExitCode(1);
161099
+ }
161100
+ });
161101
+ }
161102
+
160989
161103
  // ../../packages/app/src/services/pr-review/index.ts
160990
161104
  init_src();
160991
161105
  import { spawnSync as spawnSync7 } from "child_process";
@@ -161606,9 +161720,9 @@ function cmdStatus(rt, args) {
161606
161720
  ].join(`
161607
161721
  `));
161608
161722
  }
161609
- var PR_REVIEW_HELP = `spur builder pr \u2014 deterministic spine for /sp:dev-pr-review (sp:pr-reviewing)
161723
+ var PR_REVIEW_HELP = `pr-review spine \u2014 internal ops composed by the flat spur builder pr-status|pr-check|pr-open|pr-review verbs
161610
161724
 
161611
- Usage: spur builder pr <subcommand> [flags]
161725
+ Usage: main([<op>, ...flags]) \u2014 not a CLI verb (the builder pr <op> alias was removed in 1290)
161612
161726
 
161613
161727
  Subcommands:
161614
161728
  preflight git/gh/repo checks; FAILs on detached HEAD or a dirty tree
@@ -161625,15 +161739,6 @@ Subcommands:
161625
161739
 
161626
161740
  Global flags: --json (single JSON object) --status-file <path> (one-word verdict)
161627
161741
  Exit codes: 0 ok \xB7 1 usage \xB7 2 hard failure \xB7 3 wait timeout`;
161628
- function main(argv, sink, context = {}) {
161629
- const previous = io;
161630
- io = sink;
161631
- try {
161632
- return runSpine(argv, createRuntime(context));
161633
- } finally {
161634
- io = previous;
161635
- }
161636
- }
161637
161742
  function runSpine(argv, rt) {
161638
161743
  let args = null;
161639
161744
  try {
@@ -161929,22 +162034,7 @@ function runPrVerb(verb, opts, sink, context = {}) {
161929
162034
  }
161930
162035
 
161931
162036
  // src/commands/builder/pr.ts
161932
- var PR_VERB_FOR_OP = {
161933
- preflight: "pr-check",
161934
- hygiene: "pr-check",
161935
- push: "pr-open",
161936
- ensure: "pr-open",
161937
- "ensure-pr": "pr-open",
161938
- request: "pr-review",
161939
- wait: "pr-review",
161940
- collect: "pr-status",
161941
- status: "pr-status"
161942
- };
161943
- function prDeprecationLine(op) {
161944
- const replacement = op === undefined ? undefined : PR_VERB_FOR_OP[op];
161945
- return replacement !== undefined ? `spur builder pr ${op} is deprecated; use spur builder ${replacement}` : "spur builder pr is deprecated; use spur builder pr-status | pr-check | pr-open | pr-review";
161946
- }
161947
- function registerBuilderPrCommands(builder, context) {
162037
+ function registerBuilderPrCommand(builder, context) {
161948
162038
  const io = () => ({
161949
162039
  out: (line) => context.output.write(line),
161950
162040
  err: (line) => context.output.error(line)
@@ -162003,7 +162093,7 @@ function registerBuilderPrCommands(builder, context) {
162003
162093
  "returns PENDING. Exit codes: 0 ok \xB7 2 hard failure \xB7 3 wait timeout",
162004
162094
  "(verdict PENDING \u2014 pending, never failure)."
162005
162095
  ].join(`
162006
- `)).option("--force", "request a fresh review even when the current HEAD is already reviewed").option("--focus <text>", "extra review focus appended to the request").option("--no-wait", "post the request and return PENDING without polling").option("--timeout <s>", "wait budget in seconds (service default 600)").option("--interval <s>", "poll interval in seconds (service default 30)").option(...SHARED_OPTIONS.json).option("--status-file <path>", "write the one-word verdict (CLEAN | FINDINGS | PENDING | FAIL)").action((options) => {
162096
+ `)).option(...SHARED_OPTIONS.forceBuilderPr).option("--focus <text>", "extra review focus appended to the request").option("--no-wait", "post the request and return PENDING without polling").option("--timeout <s>", "wait budget in seconds (service default 600)").option("--interval <s>", "poll interval in seconds (service default 30)").option(...SHARED_OPTIONS.json).option("--status-file <path>", "write the one-word verdict (CLEAN | FINDINGS | PENDING | FAIL)").action((options) => {
162007
162097
  run("review", {
162008
162098
  ...options.force === true ? { force: true } : {},
162009
162099
  ...options.focus !== undefined ? { focus: options.focus } : {},
@@ -162015,13 +162105,25 @@ function registerBuilderPrCommands(builder, context) {
162015
162105
  });
162016
162106
  });
162017
162107
  }
162018
- function registerBuilderPrAliasCommand(builder, context) {
162019
- builder.command("pr", { hidden: true }).summary("deprecated alias for the flat pr-<verb> commands").description(PR_REVIEW_HELP).allowUnknownOption(true).allowExcessArguments(true).argument("[argv...]", "subcommand and flags (see description)").action((argv) => {
162020
- context.output.error(prDeprecationLine(argv[0]));
162021
- context.setExitCode(main(argv, {
162022
- out: (line) => context.output.write(line),
162023
- err: (line) => context.output.error(line)
162024
- }, { cwd: context.cwd, env: context.env }));
162108
+
162109
+ // src/commands/builder/publish.ts
162110
+ function registerBuilderPublishCommand(noun, context) {
162111
+ noun.command("publish").summary("resolve workspace/catalog ranges, build, and npm publish a package (CI)").description([
162112
+ "CI publish for npm Trusted Publishing (OIDC): rewrite workspace:/catalog: ranges in",
162113
+ "<dir>/package.json, run `bun run build:bundle`, `npm publish --access public`, and",
162114
+ "restore the manifest on every exit path."
162115
+ ].join(`
162116
+ `)).option("--otp <code>", "npm one-time password (local publishes only)").argument("<dir>", "package directory, relative to the workspace root").action(async (dir, options) => {
162117
+ try {
162118
+ await publishPackage(dir, {
162119
+ repoRoot: context.cwd,
162120
+ output: context.output,
162121
+ ...options.otp !== undefined ? { otp: options.otp } : {}
162122
+ });
162123
+ } catch (err) {
162124
+ context.output.error(err instanceof Error ? err.message : String(err));
162125
+ context.setExitCode(1);
162126
+ }
162025
162127
  });
162026
162128
  }
162027
162129
 
@@ -162515,7 +162617,7 @@ function registerBuilderShowBranchesCommand(builder, context) {
162515
162617
  "Exit codes: 0 ok \xB7 1 usage (bad --format/--limit/--max-commits, unknown base ref,",
162516
162618
  "--local with --remote) \xB7 2 git failure (not a repository, git missing)."
162517
162619
  ].join(`
162518
- `)).option("--format <format>", "output format: markdown | json | text (default markdown)", "markdown").option("--json", "alias of --format json (prints the raw BranchTopology object)").option("--base <ref>", "base ref to measure every branch against (default: origin/HEAD, else main)").option("--local", "only local branches").option("--remote", "only remote branches").option("--pr", "enrich rows with open pull-request state (one gh call; degrades with a warning)").option("--limit <n>", "maximum branch rows kept, most recent last-commit first (default 30)", "30").option("--max-commits <n>", "maximum commit nodes per branch line in the graph (default 3)", "3").option("--hide-contained", "drop contained branches from the graph instead of drawing their join").option("--theme <name>", "mermaid theme name for the graph init directive").action((options) => {
162620
+ `)).option("--format <format>", "output format: markdown | json | text (default markdown)", "markdown").option(...SHARED_OPTIONS.jsonShowBranches).option("--base <ref>", "base ref to measure every branch against (default: origin/HEAD, else main)").option("--local", "only local branches").option("--remote", "only remote branches").option("--pr", "enrich rows with open pull-request state (one gh call; degrades with a warning)").option("--limit <n>", "maximum branch rows kept, most recent last-commit first (default 30)", "30").option("--max-commits <n>", "maximum commit nodes per branch line in the graph (default 3)", "3").option("--hide-contained", "drop contained branches from the graph instead of drawing their join").option("--theme <name>", "mermaid theme name for the graph init directive").action((options) => {
162519
162621
  const format = resolveFormat(options.format, command.getOptionValueSource("format") === "cli", options.json === true, context);
162520
162622
  if (format === null)
162521
162623
  return;
@@ -162590,113 +162692,14 @@ function usageError(message, context) {
162590
162692
  context.setExitCode(1);
162591
162693
  }
162592
162694
 
162593
- // src/commands/builder.ts
162695
+ // src/commands/builder/index.ts
162594
162696
  function registerBuilderCommand(program, context) {
162595
162697
  const noun = program.command("builder").summary("release plumbing: version bumps, tags, branch topology, and PR-review ops");
162596
- noun.command("bump-ver").summary("bump a package version, commit, tag, optionally push").description([
162597
- "Bump one workspace package (or every released package with --all),",
162598
- "rewrite workspace pins, commit, and tag. A bare version bumps all:",
162599
- " spur builder bump-ver 0.1.4 # every released package",
162600
- " spur builder bump-ver spur 0.1.4 # one package by id",
162601
- " spur builder bump-ver --check spur # pre-publish drift guard (read-only)",
162602
- "--check fails (exit 1) when .claude-plugin/marketplace.json or a listed plugin.json",
162603
- "drifted from the package version; target defaults to the aggregate package."
162604
- ].join(`
162605
- `)).option("--check", "only verify marketplace/plugin versions match the package version (no writes)").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).option(...SHARED_OPTIONS.jsonEnvelope).argument("[target]", "package id, or the version itself when bumping all").argument("[version]", "target semver version").action(async (target, version, options) => {
162606
- const args = [target, version].filter((value) => value !== undefined);
162607
- if (options.all === true)
162608
- args.push("--all");
162609
- if (options.push === true)
162610
- args.push("--push");
162611
- try {
162612
- if (options.check === true) {
162613
- if (version !== undefined)
162614
- throw new Error("bump-ver --check takes only a package id");
162615
- if (options.all === true || options.push === true)
162616
- throw new Error("bump-ver --check rejects incompatible write flags (--push/--all)");
162617
- const code = await checkVersionDrift(target, context.cwd, context.output, context.spurConfig);
162618
- if (options.json === true) {
162619
- context.output.write(toEnvelopeJson({ ok: code === 0, verb: "bump-ver", check: true, target: target ?? "aggregate" }, { enveloped: options.jsonEnvelope }));
162620
- }
162621
- if (code !== 0)
162622
- context.setExitCode(code);
162623
- return;
162624
- }
162625
- await bumpVer(args, context.cwd, context.output, context.spurConfig);
162626
- if (options.json === true) {
162627
- context.output.write(toEnvelopeJson({ ok: true, verb: "bump-ver", target: target ?? "all", version: version ?? "" }, { enveloped: options.jsonEnvelope }));
162628
- }
162629
- } catch (err) {
162630
- const message = err instanceof Error ? err.message : String(err);
162631
- if (options.json === true) {
162632
- context.output.write(toEnvelopeJson({ ok: false, verb: "bump-ver", error: message }, {
162633
- enveloped: options.jsonEnvelope,
162634
- error: {
162635
- code: "INTERNAL_ERROR",
162636
- message,
162637
- details: { cliCode: "bump-ver", verb: "bump-ver" }
162638
- }
162639
- }));
162640
- context.setExitCode(1);
162641
- return;
162642
- }
162643
- context.output.error(message);
162644
- context.setExitCode(1);
162645
- }
162646
- });
162647
- noun.command("drop-tags").summary("delete release tags locally and optionally on origin").description([
162648
- "Delete one package release tag (or every released tag plus the aggregate",
162649
- "with --all / a bare version). Use --remote to also delete on origin."
162650
- ].join(`
162651
- `)).option("--all", "drop every released tag plus the aggregate tag").option("--remote", "also delete the tag(s) on origin").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).argument("[target]", "package id, or the version itself when dropping all").argument("[version]", "semver version of the tags to drop").action(async (target, version, options) => {
162652
- const args = [target, version].filter((value) => value !== undefined);
162653
- if (options.all === true)
162654
- args.push("--all");
162655
- if (options.remote === true)
162656
- args.push("--remote");
162657
- try {
162658
- await dropTags(args, context.cwd, context.output, context.spurConfig);
162659
- if (options.json === true) {
162660
- context.output.write(toEnvelopeJson({ ok: true, verb: "drop-tags", target: target ?? "all", version: version ?? "" }, { enveloped: options.jsonEnvelope }));
162661
- }
162662
- } catch (err) {
162663
- const message = err instanceof Error ? err.message : String(err);
162664
- if (options.json === true) {
162665
- context.output.write(toEnvelopeJson({ ok: false, verb: "drop-tags", error: message }, {
162666
- enveloped: options.jsonEnvelope,
162667
- error: {
162668
- code: "INTERNAL_ERROR",
162669
- message,
162670
- details: { cliCode: "drop-tags", verb: "drop-tags" }
162671
- }
162672
- }));
162673
- context.setExitCode(1);
162674
- return;
162675
- }
162676
- context.output.error(message);
162677
- context.setExitCode(1);
162678
- }
162679
- });
162680
- noun.command("publish").summary("resolve workspace/catalog ranges, build, and npm publish a package (CI)").description([
162681
- "CI publish for npm Trusted Publishing (OIDC): rewrite workspace:/catalog: ranges in",
162682
- "<dir>/package.json, run `bun run build:bundle`, `npm publish --access public`, and",
162683
- "restore the manifest on every exit path."
162684
- ].join(`
162685
- `)).option("--otp <code>", "npm one-time password (local publishes only)").argument("<dir>", "package directory, relative to the workspace root").action(async (dir, options) => {
162686
- try {
162687
- await publishPackage(dir, {
162688
- repoRoot: context.cwd,
162689
- output: context.output,
162690
- ...options.otp !== undefined ? { otp: options.otp } : {}
162691
- });
162692
- } catch (err) {
162693
- context.output.error(err instanceof Error ? err.message : String(err));
162694
- context.setExitCode(1);
162695
- }
162696
- });
162698
+ registerBuilderBumpVerCommand(noun, context);
162699
+ registerBuilderDropTagsCommand(noun, context);
162700
+ registerBuilderPublishCommand(noun, context);
162697
162701
  registerBuilderShowBranchesCommand(noun, context);
162698
- registerBuilderPrCommands(noun, context);
162699
- registerBuilderPrAliasCommand(noun, context);
162702
+ registerBuilderPrCommand(noun, context);
162700
162703
  }
162701
162704
 
162702
162705
  // src/commands/decision.ts
@@ -163130,161 +163133,35 @@ function fail2(context, options, error) {
163130
163133
  context.setExitCode(1);
163131
163134
  }
163132
163135
 
163133
- // src/commands/feature.ts
163136
+ // src/commands/feature/advance.ts
163134
163137
  init_src4();
163135
- init_src2();
163136
163138
 
163137
- // src/commands/feature/handoff.ts
163138
- function registerFeatureHandoffCommand(feature, context) {
163139
- feature.command("handoff").summary("Finalize the idea pipeline: task ordering, roster refresh and the handoff report.").description([
163140
- "Reads .spur/run/<runId>-idea-{task-batch,batch-create-result,task-order}.json (and the optional",
163141
- "-idea-ready.json), applies dependencies, refreshes the roster, checks each task and writes",
163142
- ".spur/run/<runId>-idea-handoff.md with one recommended next command.",
163143
- "Exit: 0 success; 1 failure (missing ids or inputs, a mapping or CLI error).",
163144
- "Flags fall back to the workflow env names: featureId, __runId, spurBin."
163145
- ].join(`
163146
- `)).argument("[id]", "Feature ID (env: featureId)").option("--run-id <id>", "idea-pipeline run id (env: __runId)").option("--spur-bin <cmd>", "spur invocation for the nested task/feature calls (env: spurBin; default: this spur)").action(async (id, options) => {
163147
- const runId = options.runId ?? context.env.__runId;
163148
- if (runId && !acceptArtifactId(runId, "run id", context))
163149
- return;
163150
- await Promise.resolve().then(() => init_src4());
163151
- const env = context.env;
163152
- const outcome = await runIdeaHandoffCli({
163153
- __runId: options.runId ?? env.__runId,
163154
- ...options.spurBin === undefined && env.spurBin === undefined ? { spurArgv: resolveSpurArgv() } : {},
163155
- featureId: id ?? env.featureId,
163156
- spurBin: options.spurBin ?? env.spurBin ?? resolveSpurBin()
163157
- });
163158
- context.setExitCode(outcome.exitCode);
163159
- });
163139
+ // src/commands/feature/shared.ts
163140
+ init_src4();
163141
+ async function makeReceiptRunPort(context) {
163142
+ return createFeatureReceiptRunPort(await context.getDb(), context.fs);
163160
163143
  }
163161
-
163162
- // src/commands/feature/verify.ts
163163
- function registerFeatureVerifyCommand(feature, context) {
163164
- feature.command("verify").summary("Run the feature-verification pass and record its bound PASS/FAIL evidence receipt.").description([
163165
- "Runs the selected feature-verification definition\u2019s command once (or --cmd) and writes the",
163166
- "run-scoped + feature-latest receipts and .spur/run/<runId>-feature-verification.status.",
163167
- "Exit: 0 after a completed pass (PASS or FAIL); 1 when the pass could not run.",
163168
- "Without --run-id the pass opens and finalizes its own feature-lifecycle run row.",
163169
- "Flags fall back to the workflow env names: featureId, __runId."
163170
- ].join(`
163171
- `)).argument("[id]", "Feature ID (env: featureId)").option("--run-id <id>", "workflow run id the receipt binds to (env: __runId)").option("--cmd <command>", "verification command override (env: verificationCmd)").action(async (id, options) => {
163172
- await Promise.resolve().then(() => init_src4());
163173
- const env = context.env;
163174
- const featureId = id || env.featureId || "";
163175
- try {
163176
- const result = await runFeatureVerification({
163177
- workdir: context.cwd,
163178
- featureId,
163179
- runId: options.runId || env.__runId || "",
163180
- cmdOverride: options.cmd || env.verificationCmd || ""
163181
- });
163182
- context.output.write(`feature verify: ${featureId} ${result.message}`);
163183
- } catch (err) {
163184
- context.output.error(`feature verify: ${String(err)}`);
163185
- context.setExitCode(1);
163186
- }
163144
+ async function makeService2(context, folderOverride) {
163145
+ const resolved = await resolvePlanningFolders(context.fs);
163146
+ const featuresDir = folderOverride ?? context.fs.resolve(resolved.featuresDir);
163147
+ const tasksDir = context.fs.resolve(resolved.tasksDir);
163148
+ const lifecycle = makeLifecycleAdapter(context, FEATURE_LIFECYCLE_PROFILE);
163149
+ const writeService = new PlanningWriteService({
163150
+ fs: context.fs,
163151
+ ...lifecycle ? { lifecycle } : {},
163152
+ emitter: makePlanningEmitter(context)
163153
+ });
163154
+ return new FeatureService({
163155
+ fs: context.fs,
163156
+ writeService,
163157
+ featuresDir,
163158
+ tasksDir,
163159
+ foldersConfig: resolved.foldersConfig
163187
163160
  });
163188
163161
  }
163189
163162
 
163190
- // src/commands/feature.ts
163191
- function registerFeatureCommand(program, context) {
163192
- const feature = program.command("feature").summary("manage features (hierarchical IDs)");
163193
- 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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (name, options) => {
163194
- const svc = await makeService2(context, options.folder);
163195
- try {
163196
- const result = await svc.create(name, options.parent);
163197
- if (options.json) {
163198
- context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
163199
- } else {
163200
- context.output.write(`Created feature ${result.ref.id}: ${result.ref.filePath}`);
163201
- }
163202
- } catch (err) {
163203
- writeJsonError(context.output, options, String(err));
163204
- context.setExitCode(1);
163205
- }
163206
- });
163207
- feature.command("show").alias("get").summary("Show a feature by ID.").argument("<id>", "Feature ID").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
163208
- const svc = await makeService2(context, options.folder);
163209
- try {
163210
- const result = await svc.show(id);
163211
- if (result === null) {
163212
- writeJsonError(context.output, options, `Feature ${id} not found`, "NOT_FOUND");
163213
- context.setExitCode(1);
163214
- return;
163215
- }
163216
- if (options.json) {
163217
- const { content, ...rest } = result;
163218
- context.output.write(toEnvelopeJson({ ...rest, content }, { enveloped: options.jsonEnvelope }));
163219
- } else {
163220
- context.output.write(result.content);
163221
- }
163222
- } catch (err) {
163223
- writeJsonError(context.output, options, String(err));
163224
- context.setExitCode(1);
163225
- }
163226
- });
163227
- feature.command("update").summary("Update a feature status, scalar frontmatter field, or section body.").argument("<id>", "Feature ID").argument("[status]", "New lifecycle status").addHelpText("after", [
163228
- "Section names are validated against the closed-world canonical set; a rejected",
163229
- "`--section` costs a failed write. List valid names first:",
163230
- "`spur task sections <wbs> list`."
163231
- ].join(`
163232
- `)).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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, status, options) => {
163233
- const svc = await makeService2(context, options.folder);
163234
- try {
163235
- let result;
163236
- if (options.section !== undefined) {
163237
- if (options.fromFile === undefined) {
163238
- writeJsonError(context.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
163239
- context.setExitCode(2);
163240
- return;
163241
- }
163242
- result = await svc.updateSection(id, options.section, options.fromFile);
163243
- if (!options.json) {
163244
- for (const warning of result.warnings ?? []) {
163245
- context.output.error(warning);
163246
- }
163247
- context.output.write(`Updated section '${options.section}' in feature ${result.ref.id}`);
163248
- }
163249
- } else if (options.fromFile !== undefined) {
163250
- writeJsonError(context.output, options, "--section is required with --from-file", "VALIDATION_FAILED");
163251
- context.setExitCode(2);
163252
- return;
163253
- }
163254
- if (options.field !== undefined) {
163255
- if (options.value === undefined) {
163256
- writeJsonError(context.output, options, "--value is required with --field", "VALIDATION_FAILED");
163257
- context.setExitCode(2);
163258
- return;
163259
- }
163260
- result = await svc.update(id, options.field, options.value);
163261
- if (!options.json) {
163262
- context.output.write(`Updated ${options.field} on feature ${result.ref.id}`);
163263
- }
163264
- } else if (options.value !== undefined) {
163265
- writeJsonError(context.output, options, "--field is required with --value", "VALIDATION_FAILED");
163266
- context.setExitCode(2);
163267
- return;
163268
- }
163269
- if (status !== undefined) {
163270
- result = await svc.transition(id, status);
163271
- if (!options.json) {
163272
- context.output.write(`${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
163273
- }
163274
- }
163275
- if (result === undefined) {
163276
- writeJsonError(context.output, options, "Either <status>, --field/--value, or --section/--from-file is required", "VALIDATION_FAILED");
163277
- context.setExitCode(2);
163278
- return;
163279
- }
163280
- if (options.json) {
163281
- context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
163282
- }
163283
- } catch (err) {
163284
- writeJsonError(context.output, options, String(err));
163285
- context.setExitCode(1);
163286
- }
163287
- });
163163
+ // src/commands/feature/advance.ts
163164
+ function registerFeatureAdvanceCommand(feature, context) {
163288
163165
  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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
163289
163166
  const svc = await makeService2(context, options.folder);
163290
163167
  const target = options.to ?? "done";
@@ -163349,6 +163226,148 @@ function registerFeatureCommand(program, context) {
163349
163226
  context.setExitCode(1);
163350
163227
  }
163351
163228
  });
163229
+ }
163230
+ async function assertFeatureCheckPass(context, id, folderOverride, strict, asStatus) {
163231
+ const resolved = await resolvePlanningFolders(context.fs);
163232
+ const featuresDir = folderOverride ?? context.fs.resolve(resolved.featuresDir);
163233
+ const tasksDir = context.fs.resolve(resolved.tasksDir);
163234
+ const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context.fs.resolve(p));
163235
+ if (!tasksDirs.includes(tasksDir))
163236
+ tasksDirs.unshift(tasksDir);
163237
+ const entries = await context.fs.readDir(featuresDir);
163238
+ const fileName = entries.find((name) => isFeatureFile(name, id));
163239
+ if (fileName === undefined) {
163240
+ throw new Error(`Feature ${id} not found`);
163241
+ }
163242
+ const result = await new FeatureCheckService(context.fs).check(`${featuresDir}/${fileName}`, id, {
163243
+ strict,
163244
+ featuresDir,
163245
+ tasksDir,
163246
+ tasksDirs,
163247
+ runDir: context.fs.resolve(".spur/run"),
163248
+ receiptRunPort: await makeReceiptRunPort(context),
163249
+ asStatus
163250
+ });
163251
+ if (!result.pass) {
163252
+ const details = result.findings.map((f) => `${f.layer} ${f.section}: ${f.message}`).join("; ");
163253
+ throw new Error(`Feature ${id} check failed before advance${strict ? " (strict)" : ""}: ${details}`);
163254
+ }
163255
+ }
163256
+
163257
+ // src/commands/feature/check.ts
163258
+ init_src4();
163259
+ function registerFeatureCheckCommand(feature, context) {
163260
+ 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("--inventory <report>", "Cross-check the report requirement inventory against the AC covers lines (errors on uncovered items)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
163261
+ const resolved = await resolvePlanningFolders(context.fs);
163262
+ const featuresDir = options.folder ?? context.fs.resolve(resolved.featuresDir);
163263
+ const tasksDir = context.fs.resolve(resolved.tasksDir);
163264
+ const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context.fs.resolve(p));
163265
+ if (!tasksDirs.includes(tasksDir))
163266
+ tasksDirs.unshift(tasksDir);
163267
+ const svc = new FeatureCheckService(context.fs);
163268
+ const json = options.json === true;
163269
+ const strict = options.strict === true;
163270
+ const inventoryText = typeof options.inventory === "string" ? await Promise.resolve(context.fs.readFile(context.fs.resolve(options.inventory))).catch(() => "") : undefined;
163271
+ try {
163272
+ const entries = await context.fs.readDir(featuresDir);
163273
+ const ids = id ? [id] : entries.map((n) => n.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1]).filter((n) => n !== undefined);
163274
+ const results = [];
163275
+ for (const fid of ids) {
163276
+ const fileName = entries.find((n) => isFeatureFile(n, fid));
163277
+ if (!fileName) {
163278
+ writeJsonError(context.output, options, `Feature ${fid} not found`, "NOT_FOUND");
163279
+ context.setExitCode(1);
163280
+ return;
163281
+ }
163282
+ const result = await svc.check(`${featuresDir}/${fileName}`, fid, {
163283
+ strict,
163284
+ featuresDir,
163285
+ tasksDir,
163286
+ tasksDirs,
163287
+ runDir: context.fs.resolve(".spur/run"),
163288
+ receiptRunPort: await makeReceiptRunPort(context),
163289
+ severityOverrides: resolved.severityOverrides,
163290
+ asStatus: options.as,
163291
+ fix: options.fix === true,
163292
+ inventory: inventoryText,
163293
+ transitionPort: options.fix === true ? async (targetId, to) => {
163294
+ const featureSvc = await makeService2(context, options.folder);
163295
+ return featureSvc.transition(targetId, to);
163296
+ } : undefined
163297
+ });
163298
+ results.push(result);
163299
+ if (!json) {
163300
+ if (result.repairs !== undefined && result.repairs.length > 0) {
163301
+ for (const r of result.repairs) {
163302
+ context.output.write(` [FIX] ${r.kind} ${r.section}: ${r.detail}`);
163303
+ }
163304
+ }
163305
+ context.output.write(`
163306
+ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
163307
+ for (const f of result.findings) {
163308
+ const tag = f.severity === "error" ? "ERR" : "WARN";
163309
+ context.output.write(` [${tag}] ${f.layer} ${f.section}: ${f.message}`);
163310
+ }
163311
+ }
163312
+ }
163313
+ if (json) {
163314
+ context.output.write(toEnvelopeJson(results, { enveloped: options.jsonEnvelope, kind: "list" }));
163315
+ }
163316
+ if (results.some((r) => !r.pass))
163317
+ context.setExitCode(1);
163318
+ } catch (err) {
163319
+ writeJsonError(context.output, options, String(err));
163320
+ context.setExitCode(1);
163321
+ }
163322
+ });
163323
+ }
163324
+
163325
+ // src/commands/feature/create.ts
163326
+ function registerFeatureCreateCommand(feature, context) {
163327
+ 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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (name, options) => {
163328
+ const svc = await makeService2(context, options.folder);
163329
+ try {
163330
+ const result = await svc.create(name, options.parent);
163331
+ if (options.json) {
163332
+ context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
163333
+ } else {
163334
+ context.output.write(`Created feature ${result.ref.id}: ${result.ref.filePath}`);
163335
+ }
163336
+ } catch (err) {
163337
+ writeJsonError(context.output, options, String(err));
163338
+ context.setExitCode(1);
163339
+ }
163340
+ });
163341
+ }
163342
+
163343
+ // src/commands/feature/handoff.ts
163344
+ function registerFeatureHandoffCommand(feature, context) {
163345
+ feature.command("handoff").summary("Finalize the idea pipeline: task ordering, roster refresh and the handoff report.").description([
163346
+ "Reads .spur/run/<runId>-idea-{task-batch,batch-create-result,task-order}.json (and the optional",
163347
+ "-idea-ready.json), applies dependencies, refreshes the roster, checks each task and writes",
163348
+ ".spur/run/<runId>-idea-handoff.md with one recommended next command.",
163349
+ "Exit: 0 success; 1 failure (missing ids or inputs, a mapping or CLI error).",
163350
+ "Flags fall back to the workflow env names: featureId, __runId, spurBin."
163351
+ ].join(`
163352
+ `)).argument("[id]", "Feature ID (env: featureId)").option("--run-id <id>", "idea-pipeline run id (env: __runId)").option("--spur-bin <cmd>", "spur invocation for the nested task/feature calls (env: spurBin; default: this spur)").action(async (id, options) => {
163353
+ const runId = options.runId ?? context.env.__runId;
163354
+ if (runId && !acceptArtifactId(runId, "run id", context))
163355
+ return;
163356
+ await Promise.resolve().then(() => init_src4());
163357
+ const env = context.env;
163358
+ const outcome = await runIdeaHandoffCli({
163359
+ __runId: options.runId ?? env.__runId,
163360
+ ...options.spurBin === undefined && env.spurBin === undefined ? { spurArgv: resolveSpurArgv() } : {},
163361
+ featureId: id ?? env.featureId,
163362
+ spurBin: options.spurBin ?? env.spurBin ?? resolveSpurBin()
163363
+ });
163364
+ context.setExitCode(outcome.exitCode);
163365
+ });
163366
+ }
163367
+
163368
+ // src/commands/feature/list.ts
163369
+ init_src2();
163370
+ function registerFeatureListCommand(feature, context) {
163352
163371
  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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
163353
163372
  const svc = await makeService2(context, options.folder);
163354
163373
  try {
@@ -163375,6 +163394,10 @@ function registerFeatureCommand(program, context) {
163375
163394
  context.setExitCode(1);
163376
163395
  }
163377
163396
  });
163397
+ }
163398
+
163399
+ // src/commands/feature/move.ts
163400
+ function registerFeatureMoveCommand(feature, context) {
163378
163401
  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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
163379
163402
  const svc = await makeService2(context, options.folder);
163380
163403
  try {
@@ -163395,6 +163418,11 @@ function registerFeatureCommand(program, context) {
163395
163418
  context.setExitCode(1);
163396
163419
  }
163397
163420
  });
163421
+ }
163422
+
163423
+ // src/commands/feature/refresh.ts
163424
+ init_src4();
163425
+ function registerFeatureRefreshCommand(feature, context) {
163398
163426
  feature.command("refresh").summary("Rebuild INDEX.md and each feature ## Tasks table from task edges (docs only \u2014 no status change).").description([
163399
163427
  "Regenerate derived views so feature markdown matches the task graph:",
163400
163428
  " \u2022 docs/features/INDEX.md (ID-encoded tree)",
@@ -163440,69 +163468,34 @@ function registerFeatureCommand(program, context) {
163440
163468
  context.setExitCode(1);
163441
163469
  }
163442
163470
  });
163443
- 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("--inventory <report>", "Cross-check the report requirement inventory against the AC covers lines (errors on uncovered items)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
163444
- const resolved = await resolvePlanningFolders(context.fs);
163445
- const featuresDir = options.folder ?? context.fs.resolve(resolved.featuresDir);
163446
- const tasksDir = context.fs.resolve(resolved.tasksDir);
163447
- const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context.fs.resolve(p));
163448
- if (!tasksDirs.includes(tasksDir))
163449
- tasksDirs.unshift(tasksDir);
163450
- const svc = new FeatureCheckService(context.fs);
163451
- const json = options.json === true;
163452
- const strict = options.strict === true;
163453
- const inventoryText = typeof options.inventory === "string" ? await Promise.resolve(context.fs.readFile(context.fs.resolve(options.inventory))).catch(() => "") : undefined;
163471
+ }
163472
+
163473
+ // src/commands/feature/show.ts
163474
+ function registerFeatureShowCommand(feature, context) {
163475
+ feature.command("show").alias("get").summary("Show a feature by ID.").argument("<id>", "Feature ID").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, options) => {
163476
+ const svc = await makeService2(context, options.folder);
163454
163477
  try {
163455
- const entries = await context.fs.readDir(featuresDir);
163456
- const ids = id ? [id] : entries.map((n) => n.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1]).filter((n) => n !== undefined);
163457
- const results = [];
163458
- for (const fid of ids) {
163459
- const fileName = entries.find((n) => isFeatureFile(n, fid));
163460
- if (!fileName) {
163461
- writeJsonError(context.output, options, `Feature ${fid} not found`, "NOT_FOUND");
163462
- context.setExitCode(1);
163463
- return;
163464
- }
163465
- const result = await svc.check(`${featuresDir}/${fileName}`, fid, {
163466
- strict,
163467
- featuresDir,
163468
- tasksDir,
163469
- tasksDirs,
163470
- runDir: context.fs.resolve(".spur/run"),
163471
- receiptRunPort: await makeReceiptRunPort(context),
163472
- severityOverrides: resolved.severityOverrides,
163473
- asStatus: options.as,
163474
- fix: options.fix === true,
163475
- inventory: inventoryText,
163476
- transitionPort: options.fix === true ? async (targetId, to) => {
163477
- const featureSvc = await makeService2(context, options.folder);
163478
- return featureSvc.transition(targetId, to);
163479
- } : undefined
163480
- });
163481
- results.push(result);
163482
- if (!json) {
163483
- if (result.repairs !== undefined && result.repairs.length > 0) {
163484
- for (const r of result.repairs) {
163485
- context.output.write(` [FIX] ${r.kind} ${r.section}: ${r.detail}`);
163486
- }
163487
- }
163488
- context.output.write(`
163489
- ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
163490
- for (const f of result.findings) {
163491
- const tag = f.severity === "error" ? "ERR" : "WARN";
163492
- context.output.write(` [${tag}] ${f.layer} ${f.section}: ${f.message}`);
163493
- }
163494
- }
163478
+ const result = await svc.show(id);
163479
+ if (result === null) {
163480
+ writeJsonError(context.output, options, `Feature ${id} not found`, "NOT_FOUND");
163481
+ context.setExitCode(1);
163482
+ return;
163495
163483
  }
163496
- if (json) {
163497
- context.output.write(toEnvelopeJson(results, { enveloped: options.jsonEnvelope, kind: "list" }));
163484
+ if (options.json) {
163485
+ const { content, ...rest } = result;
163486
+ context.output.write(toEnvelopeJson({ ...rest, content }, { enveloped: options.jsonEnvelope }));
163487
+ } else {
163488
+ context.output.write(result.content);
163498
163489
  }
163499
- if (results.some((r) => !r.pass))
163500
- context.setExitCode(1);
163501
163490
  } catch (err) {
163502
163491
  writeJsonError(context.output, options, String(err));
163503
163492
  context.setExitCode(1);
163504
163493
  }
163505
163494
  });
163495
+ }
163496
+
163497
+ // src/commands/feature/sync.ts
163498
+ function registerFeatureSyncCommand(feature, context) {
163506
163499
  feature.command("sync").summary("Align feature lifecycle status with linked task states (status only \u2014 runs transition guards).").description([
163507
163500
  "Derive a feature status proposal from linked task statuses and apply legal hops",
163508
163501
  "(e.g. all tasks done \u2192 advance toward done; reopened work \u2192 reopen the feature).",
@@ -163561,163 +163554,134 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
163561
163554
  context.setExitCode(1);
163562
163555
  }
163563
163556
  });
163564
- registerFeatureVerifyCommand(feature, context);
163565
- registerFeatureHandoffCommand(feature, context);
163566
- }
163567
- async function makeReceiptRunPort(context) {
163568
- return createFeatureReceiptRunPort(await context.getDb(), context.fs);
163569
163557
  }
163570
- async function makeService2(context, folderOverride) {
163571
- const resolved = await resolvePlanningFolders(context.fs);
163572
- const featuresDir = folderOverride ?? context.fs.resolve(resolved.featuresDir);
163573
- const tasksDir = context.fs.resolve(resolved.tasksDir);
163574
- const lifecycle = makeLifecycleAdapter(context, FEATURE_LIFECYCLE_PROFILE);
163575
- const writeService = new PlanningWriteService({
163576
- fs: context.fs,
163577
- ...lifecycle ? { lifecycle } : {},
163578
- emitter: makePlanningEmitter(context)
163579
- });
163580
- return new FeatureService({
163581
- fs: context.fs,
163582
- writeService,
163583
- featuresDir,
163584
- tasksDir,
163585
- foldersConfig: resolved.foldersConfig
163558
+
163559
+ // src/commands/feature/update.ts
163560
+ function registerFeatureUpdateCommand(feature, context) {
163561
+ feature.command("update").summary("Update a feature status, scalar frontmatter field, or section body.").argument("<id>", "Feature ID").argument("[status]", "New lifecycle status").addHelpText("after", [
163562
+ "Section names are validated against the closed-world canonical set; a rejected",
163563
+ "`--section` costs a failed write. List valid names first:",
163564
+ "`spur task sections <wbs> list`."
163565
+ ].join(`
163566
+ `)).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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (id, status, options) => {
163567
+ const svc = await makeService2(context, options.folder);
163568
+ try {
163569
+ let result;
163570
+ if (options.section !== undefined) {
163571
+ if (options.fromFile === undefined) {
163572
+ writeJsonError(context.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
163573
+ context.setExitCode(2);
163574
+ return;
163575
+ }
163576
+ result = await svc.updateSection(id, options.section, options.fromFile);
163577
+ if (!options.json) {
163578
+ for (const warning of result.warnings ?? []) {
163579
+ context.output.error(warning);
163580
+ }
163581
+ context.output.write(`Updated section '${options.section}' in feature ${result.ref.id}`);
163582
+ }
163583
+ } else if (options.fromFile !== undefined) {
163584
+ writeJsonError(context.output, options, "--section is required with --from-file", "VALIDATION_FAILED");
163585
+ context.setExitCode(2);
163586
+ return;
163587
+ }
163588
+ if (options.field !== undefined) {
163589
+ if (options.value === undefined) {
163590
+ writeJsonError(context.output, options, "--value is required with --field", "VALIDATION_FAILED");
163591
+ context.setExitCode(2);
163592
+ return;
163593
+ }
163594
+ result = await svc.update(id, options.field, options.value);
163595
+ if (!options.json) {
163596
+ context.output.write(`Updated ${options.field} on feature ${result.ref.id}`);
163597
+ }
163598
+ } else if (options.value !== undefined) {
163599
+ writeJsonError(context.output, options, "--field is required with --value", "VALIDATION_FAILED");
163600
+ context.setExitCode(2);
163601
+ return;
163602
+ }
163603
+ if (status !== undefined) {
163604
+ result = await svc.transition(id, status);
163605
+ if (!options.json) {
163606
+ context.output.write(`${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
163607
+ }
163608
+ }
163609
+ if (result === undefined) {
163610
+ writeJsonError(context.output, options, "Either <status>, --field/--value, or --section/--from-file is required", "VALIDATION_FAILED");
163611
+ context.setExitCode(2);
163612
+ return;
163613
+ }
163614
+ if (options.json) {
163615
+ context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
163616
+ }
163617
+ } catch (err) {
163618
+ writeJsonError(context.output, options, String(err));
163619
+ context.setExitCode(1);
163620
+ }
163586
163621
  });
163587
163622
  }
163588
- async function assertFeatureCheckPass(context, id, folderOverride, strict, asStatus) {
163589
- const resolved = await resolvePlanningFolders(context.fs);
163590
- const featuresDir = folderOverride ?? context.fs.resolve(resolved.featuresDir);
163591
- const tasksDir = context.fs.resolve(resolved.tasksDir);
163592
- const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context.fs.resolve(p));
163593
- if (!tasksDirs.includes(tasksDir))
163594
- tasksDirs.unshift(tasksDir);
163595
- const entries = await context.fs.readDir(featuresDir);
163596
- const fileName = entries.find((name) => isFeatureFile(name, id));
163597
- if (fileName === undefined) {
163598
- throw new Error(`Feature ${id} not found`);
163599
- }
163600
- const result = await new FeatureCheckService(context.fs).check(`${featuresDir}/${fileName}`, id, {
163601
- strict,
163602
- featuresDir,
163603
- tasksDir,
163604
- tasksDirs,
163605
- runDir: context.fs.resolve(".spur/run"),
163606
- receiptRunPort: await makeReceiptRunPort(context),
163607
- asStatus
163623
+
163624
+ // src/commands/feature/verify.ts
163625
+ function registerFeatureVerifyCommand(feature, context) {
163626
+ feature.command("verify").summary("Run the feature-verification pass and record its bound PASS/FAIL evidence receipt.").description([
163627
+ "Runs the selected feature-verification definition\u2019s command once (or --cmd) and writes the",
163628
+ "run-scoped + feature-latest receipts and .spur/run/<runId>-feature-verification.status.",
163629
+ "Exit: 0 after a completed pass (PASS or FAIL); 1 when the pass could not run.",
163630
+ "Without --run-id the pass opens and finalizes its own feature-lifecycle run row.",
163631
+ "Flags fall back to the workflow env names: featureId, __runId."
163632
+ ].join(`
163633
+ `)).argument("[id]", "Feature ID (env: featureId)").option("--run-id <id>", "workflow run id the receipt binds to (env: __runId)").option("--cmd <command>", "verification command override (env: verificationCmd)").action(async (id, options) => {
163634
+ await Promise.resolve().then(() => init_src4());
163635
+ const env = context.env;
163636
+ const featureId = id || env.featureId || "";
163637
+ try {
163638
+ const result = await runFeatureVerification({
163639
+ workdir: context.cwd,
163640
+ featureId,
163641
+ runId: options.runId || env.__runId || "",
163642
+ cmdOverride: options.cmd || env.verificationCmd || ""
163643
+ });
163644
+ context.output.write(`feature verify: ${featureId} ${result.message}`);
163645
+ } catch (err) {
163646
+ context.output.error(`feature verify: ${String(err)}`);
163647
+ context.setExitCode(1);
163648
+ }
163608
163649
  });
163609
- if (!result.pass) {
163610
- const details = result.findings.map((f) => `${f.layer} ${f.section}: ${f.message}`).join("; ");
163611
- throw new Error(`Feature ${id} check failed before advance${strict ? " (strict)" : ""}: ${details}`);
163612
- }
163613
163650
  }
163614
163651
 
163615
- // src/commands/history.ts
163616
- init_src4();
163617
- init_src();
163652
+ // src/commands/feature/index.ts
163653
+ function registerFeatureCommand(program, context) {
163654
+ const feature = program.command("feature").summary("manage features (hierarchical IDs)");
163655
+ registerFeatureCreateCommand(feature, context);
163656
+ registerFeatureShowCommand(feature, context);
163657
+ registerFeatureUpdateCommand(feature, context);
163658
+ registerFeatureAdvanceCommand(feature, context);
163659
+ registerFeatureListCommand(feature, context);
163660
+ registerFeatureMoveCommand(feature, context);
163661
+ registerFeatureRefreshCommand(feature, context);
163662
+ registerFeatureCheckCommand(feature, context);
163663
+ registerFeatureSyncCommand(feature, context);
163664
+ registerFeatureVerifyCommand(feature, context);
163665
+ registerFeatureHandoffCommand(feature, context);
163666
+ }
163667
+
163668
+ // src/commands/history/analyze.ts
163618
163669
  init_src2();
163619
- init_dist28();
163620
- init_dist7();
163621
- import { createRequire as createRequire3 } from "module";
163622
163670
 
163623
163671
  // src/config.ts
163624
163672
  var CLI_CONFIG = {
163625
163673
  binaryName: "spur",
163626
163674
  binaryLabel: "spur",
163627
- binaryVersion: "0.4.22",
163675
+ binaryVersion: "0.4.24",
163628
163676
  configDir: ".spur",
163629
163677
  configFile: ".spur/config.yaml",
163630
163678
  databaseFile: ".spur/spur.db"
163631
163679
  };
163632
163680
 
163633
- // src/commands/history/anatomy.ts
163634
- function spurEntryPath(argv = process.argv, execPath = process.execPath) {
163635
- const entry = argv[1];
163636
- return entry !== undefined && /\.(?:[cm]?[jt]s)$/.test(entry) ? entry : execPath;
163637
- }
163638
- function registerHistoryAnatomyCommand(history, context) {
163639
- history.command("anatomy").summary("history-anatomy report cache ops (digest | check | paths | assert-clean | probe | stamp | refresh | publish)").description([
163640
- "Deterministic cache ops behind the history-anatomy workflow (ADR-079):",
163641
- " digest <artifact.json> semantic artifact digest",
163642
- " check <report.md> report-contract structure gate (PASS/FAIL)",
163643
- " paths --out <env> [--skill-dir d] ... resolve the run paths into an env file",
163644
- " assert-clean --baseline <porcelain> fail on undeclared writes (--expect=<path>)",
163645
- " probe --artifact a --target t ... cache disposition + provenance",
163646
- " stamp --candidate c --provenance p --out o stamp provenance frontmatter",
163647
- " refresh --report r --out o refresh a cache-hit report",
163648
- " publish <candidate.md> <target.md> atomic publish",
163649
- "Exit: 0 ok, 1 usage or op failure."
163650
- ].join(`
163651
- `)).allowUnknownOption(true).allowExcessArguments(true).argument("[argv...]", "op and its flags (see description)").action(async (argv) => {
163652
- for (let i = 0;i < argv.length; i++) {
163653
- if (argv[i] === "--run-id" && !acceptArtifactId(argv[i + 1] ?? "", "run id", context))
163654
- return;
163655
- }
163656
- await Promise.resolve().then(() => init_history_anatomy_cli());
163657
- const result = runCacheCli(argv, {
163658
- helper: spurEntryPath(),
163659
- cwd: context.cwd,
163660
- ...context.env.HOME !== undefined ? { home: context.env.HOME } : {}
163661
- });
163662
- writeStepText(context.output, "stdout", result.stdout);
163663
- writeStepText(context.output, "stderr", result.stderr);
163664
- context.setExitCode(result.exitCode);
163665
- });
163666
- }
163667
-
163668
- // src/commands/history/summary.ts
163669
- function registerHistorySummaryCommand(history, context) {
163670
- history.command("summary").summary("Daily summary report: tokens, git activity, history health, annotations").description([
163671
- "Generate the day\u2019s Markdown summary and write it to docs/daily/summary_<YYYYMMDD>.md",
163672
- "(or --output). Prompts for learnings / issues / pending unless SP_DAILY_SUMMARY_NO_PROMPT=1",
163673
- "(non-TTY stdin supplies the three answers as lines). Exit: 0 written, 1 generation failed."
163674
- ].join(`
163675
- `)).option("--date <date>", "YYYY-MM-DD, today or yesterday (default: today)").option("--dry-run", "print the summary without writing the file").option("--output <path>", "write to a custom path").option("--no-git", "skip git history collection").option("--no-ccusage", "skip token usage collection").action(async (options) => {
163676
- await Promise.resolve().then(() => init_daily_summary());
163677
- const argv = [];
163678
- if (options.date !== undefined)
163679
- argv.push("--date", options.date);
163680
- if (options.dryRun === true)
163681
- argv.push("--dry-run");
163682
- if (options.output !== undefined)
163683
- argv.push("--output", options.output);
163684
- if (options.git === false)
163685
- argv.push("--no-git");
163686
- if (options.ccusage === false)
163687
- argv.push("--no-ccusage");
163688
- const code = await main2(argv, {
163689
- out: (line) => context.output.write(line),
163690
- err: (line) => context.output.error(line)
163691
- });
163692
- context.setExitCode(code);
163693
- });
163694
- }
163695
-
163696
- // src/commands/history/timeline.ts
163697
- function registerHistoryTimelineCommand(history, context) {
163698
- history.command("timeline").summary("Per-prompt work/wait time and tokens from the host transcript (JSON)").description([
163699
- "Read the active host transcript read-only (CLAUDE_CODE_SESSION_ID under ~/.claude/projects,",
163700
- "PI_SESSION_FILE, or --transcript) and print one JSON timeline: a segment per operator prompt,",
163701
- 'optional --group stages, and totals. No transcript \u2192 {"available":false,...}, exit 0.',
163702
- "Exit: 0 timeline (or unavailable), 2 invalid --group or unreadable transcript."
163703
- ].join(`
163704
- `)).option("--transcript <path>", "transcript JSONL path (default: the active host session)").option("--group <spec>", 'sum segments into stages, e.g. "1-3,4"').action(async (options) => {
163705
- await Promise.resolve().then(() => init_session_timeline());
163706
- const argv = [];
163707
- if (options.transcript !== undefined)
163708
- argv.push("--transcript", options.transcript);
163709
- if (options.group !== undefined)
163710
- argv.push("--group", options.group);
163711
- const code = runSessionTimelineCli(argv, {
163712
- env: context.env,
163713
- write: (s) => writeStepText(context.output, "stdout", s),
163714
- writeErr: (s) => writeStepText(context.output, "stderr", s)
163715
- });
163716
- context.setExitCode(code);
163717
- });
163718
- }
163719
-
163720
- // src/commands/history.ts
163681
+ // src/commands/history/shared.ts
163682
+ init_src4();
163683
+ init_dist7();
163684
+ import { createRequire as createRequire3 } from "module";
163721
163685
  function resolveCliSourceTimeout(explicit, env) {
163722
163686
  if (explicit !== undefined)
163723
163687
  return parseTimeoutInput(explicit);
@@ -163736,10 +163700,6 @@ async function resolveImportProvenance(fs) {
163736
163700
  } catch {}
163737
163701
  return { binary, importer };
163738
163702
  }
163739
- function formatProvenance(provenance) {
163740
- return `binary: ${provenance.binary}
163741
- importer: @gobing-ai/ts-llm-jsonl-importer@${provenance.importer}`;
163742
- }
163743
163703
  var CAPABILITY_KINDS2 = new Set(["command", "subagent", "skill"]);
163744
163704
  function parseCapabilityOriginSpec(spec) {
163745
163705
  const parts = spec.split(":").map((part) => part.trim());
@@ -163763,132 +163723,42 @@ function parseCapabilityOriginSpec(spec) {
163763
163723
  originIdentity
163764
163724
  };
163765
163725
  }
163766
- function registerHistoryCommand(program, context) {
163767
- const noun = program.command("history").summary("import and analyze coding-agent history");
163768
- const makeService = async (capabilityOrigins) => {
163769
- let taskLocator;
163770
- try {
163771
- const { foldersConfig } = await resolvePlanningFolders(context.fs);
163772
- taskLocator = TaskLocator.forDirs(context.fs, [
163773
- context.fs.resolve(foldersConfig.active_folder),
163774
- ...Object.keys(foldersConfig.folders ?? {}).map((f) => context.fs.resolve(f))
163775
- ]);
163776
- } catch {
163777
- taskLocator = undefined;
163778
- }
163779
- const provenance = await resolveImportProvenance(context.fs);
163780
- return new HistoryService({
163781
- getDb: () => context.getDb(),
163782
- agentConfig: context.agentConfig,
163783
- ...taskLocator ? { taskLocator } : {},
163784
- importerVersion: provenance.importer,
163785
- ...capabilityOrigins ? { capabilityOrigins } : {},
163786
- fs: context.fs,
163787
- cwd: context.cwd
163788
- });
163789
- };
163790
- 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. For one session's evidence, use " + "`--file`; a full replay can exceed the per-source budget.").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|none>", `Per-source timeout in milliseconds or 'none' (default ${DEFAULT_SOURCE_TIMEOUT_MS} = 10 min)`).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).option("--capability-origin <spec>", "Classify skill calls against a released artifact: " + "source:skillName:artifactDigest:capabilityKind:originIdentity (repeatable).", (value, previous) => [...previous, value], []).action(async (options) => {
163791
- const source = options.source ?? "all";
163792
- if (options.file && source === "all") {
163793
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: '--file requires a single --source, not "all".' }, {
163794
- enveloped: options.jsonEnvelope,
163795
- error: {
163796
- code: "INTERNAL_ERROR",
163797
- message: '--file requires a single --source, not "all".',
163798
- details: { cliCode: "usage" }
163799
- }
163800
- }) : 'spur history import: --file requires a single --source, not "all".');
163801
- context.setExitCode(1);
163802
- return;
163803
- }
163804
- let sourceTimeout;
163805
- try {
163806
- sourceTimeout = resolveCliSourceTimeout(options.sourceTimeout, getEnvVars());
163807
- } catch (e) {
163808
- const timeoutMsg = `spur history import: ${e instanceof Error ? e.message : String(e)}`;
163809
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: timeoutMsg }, {
163810
- enveloped: options.jsonEnvelope,
163811
- error: { code: "INTERNAL_ERROR", message: timeoutMsg, details: { cliCode: "usage" } }
163812
- }) : timeoutMsg);
163813
- context.setExitCode(1);
163814
- return;
163815
- }
163816
- const mode = options.mode ?? (options.file ? "force-file" : "incremental");
163817
- if (mode !== "full" && mode !== "incremental" && mode !== "force-file") {
163818
- const modeMsg = `Invalid history import mode "${mode}". Expected one of: full, incremental, force-file`;
163819
- if (options.json) {
163820
- context.output.write(toEnvelopeJson({ status: "error", message: modeMsg }, {
163821
- enveloped: options.jsonEnvelope,
163822
- error: { code: "INTERNAL_ERROR", message: modeMsg, details: { cliCode: "usage" } }
163823
- }));
163824
- } else {
163825
- context.output.error(modeMsg);
163826
- }
163827
- context.setExitCode(1);
163828
- return;
163829
- }
163830
- if (options.file && mode === "full" && options.dryRun !== true) {
163831
- const unsafeMsg = "spur history import: --file <path> --mode full without --dry-run is unsafe \u2014 full mode " + "treats the file as the authoritative source for reconciliation. Preview with --dry-run, " + "or import a single file with --mode force-file.";
163832
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: unsafeMsg }, {
163833
- enveloped: options.jsonEnvelope,
163834
- error: { code: "INTERNAL_ERROR", message: unsafeMsg, details: { cliCode: "usage" } }
163835
- }) : unsafeMsg);
163836
- context.setExitCode(1);
163837
- return;
163838
- }
163839
- let capabilityOrigins;
163840
- if (options.capabilityOrigin.length > 0) {
163841
- try {
163842
- capabilityOrigins = options.capabilityOrigin.map(parseCapabilityOriginSpec);
163843
- } catch (err) {
163844
- const message = err instanceof Error ? err.message : String(err);
163845
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message }, {
163846
- enveloped: options.jsonEnvelope,
163847
- error: { code: "INTERNAL_ERROR", message, details: { cliCode: "usage" } }
163848
- }) : `spur history import: ${message}`);
163849
- context.setExitCode(1);
163850
- return;
163851
- }
163852
- }
163853
- const svc = await makeService(capabilityOrigins);
163854
- let fanOut;
163855
- try {
163856
- fanOut = await svc.importAll({
163857
- sources: source === "all" ? undefined : [source],
163858
- file: options.file || undefined,
163859
- root: options.root || undefined,
163860
- mode,
163861
- dryRun: options.dryRun === true,
163862
- sourceTimeout,
163863
- onScope: (line) => context.output.error(line)
163864
- });
163865
- } catch (e) {
163866
- if (e instanceof UnsafeHistoryImporterError) {
163867
- const msg = `spur history import: ${e.code}: ${e.message}`;
163868
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: msg }, {
163869
- enveloped: options.jsonEnvelope,
163870
- error: {
163871
- code: "INTERNAL_ERROR",
163872
- message: msg,
163873
- details: {
163874
- cliCode: e.code,
163875
- installedVersion: e.installedVersion,
163876
- minSafeVersion: e.minSafeVersion
163877
- }
163878
- }
163879
- }) : msg);
163880
- context.setExitCode(1);
163881
- return;
163882
- }
163883
- throw e;
163884
- }
163885
- const provenance = await resolveImportProvenance(context.fs);
163886
- context.output.write(options.json ? toEnvelopeJson({ ...fanOut, provenance }, { enveloped: options.jsonEnvelope }) : `${formatProvenance(provenance)}
163887
- ${formatFanOutResult(fanOut)}`);
163888
- context.setExitCode(fanOut.exitCode);
163726
+ async function makeHistoryService(context, capabilityOrigins) {
163727
+ let taskLocator;
163728
+ try {
163729
+ const { foldersConfig } = await resolvePlanningFolders(context.fs);
163730
+ taskLocator = TaskLocator.forDirs(context.fs, [
163731
+ context.fs.resolve(foldersConfig.active_folder),
163732
+ ...Object.keys(foldersConfig.folders ?? {}).map((f) => context.fs.resolve(f))
163733
+ ]);
163734
+ } catch {
163735
+ taskLocator = undefined;
163736
+ }
163737
+ const provenance = await resolveImportProvenance(context.fs);
163738
+ return new HistoryService({
163739
+ getDb: () => context.getDb(),
163740
+ agentConfig: context.agentConfig,
163741
+ ...taskLocator ? { taskLocator } : {},
163742
+ importerVersion: provenance.importer,
163743
+ ...capabilityOrigins ? { capabilityOrigins } : {},
163744
+ fs: context.fs,
163745
+ cwd: context.cwd
163889
163746
  });
163747
+ }
163748
+ function parseTopOrThrow(raw) {
163749
+ if (raw === undefined)
163750
+ return;
163751
+ const n = Number(raw);
163752
+ if (!Number.isInteger(n) || n < 1) {
163753
+ throw new Error(`--top must be a positive integer (got "${raw}")`);
163754
+ }
163755
+ return n;
163756
+ }
163757
+
163758
+ // src/commands/history/analyze.ts
163759
+ function registerHistoryAnalyzeCommand(noun, context) {
163890
163760
  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).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
163891
- const svc = await makeService();
163761
+ const svc = await makeHistoryService(context);
163892
163762
  const source = options.source ?? "all";
163893
163763
  const selector = {
163894
163764
  since: options.since || null,
@@ -163914,64 +163784,50 @@ ${formatFanOutResult(fanOut)}`);
163914
163784
  context.setExitCode(2);
163915
163785
  }
163916
163786
  });
163917
- noun.command("reset").description("Destructively wipe every history_* table: normalized rows, per-source ETL, board rollups, " + "daily stats, and importer checkpoints/ledger. Requires --yes. Task corpus and run " + "provenance (task_run_links) are untouched; a full `spur history import` rebuilds everything.").option("--yes", "Confirm the destructive wipe", false).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
163918
- if (!options.yes) {
163919
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: "Refusing to wipe history tables without --yes." }, {
163920
- enveloped: options.jsonEnvelope,
163921
- error: {
163922
- code: "INTERNAL_ERROR",
163923
- message: "Refusing to wipe history tables without --yes.",
163924
- details: { cliCode: "usage" }
163925
- }
163926
- }) : "spur history reset: refusing to wipe history tables without --yes.");
163927
- context.setExitCode(1);
163928
- return;
163929
- }
163930
- const svc = await makeService();
163931
- const result = await svc.resetHistory();
163932
- const note = result.unknown.length > 0 ? ` WARNING: ${result.unknown.length} unlisted history_* table(s) left intact: ${result.unknown.join(", ")}` : "";
163933
- context.output.write(options.json ? toEnvelopeJson({
163934
- cleared: result.cleared,
163935
- skipped: result.skipped,
163936
- unknown: result.unknown,
163937
- clearedCount: result.cleared.length
163938
- }, { enveloped: options.jsonEnvelope }) : `cleared ${result.cleared.length} history tables` + (result.skipped.length > 0 ? ` (${result.skipped.length} not present)` : "") + note);
163939
- });
163940
- 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(...SHARED_OPTIONS.jsonEnvelope).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) => {
163941
- try {
163942
- const top = parseTopOrThrow(options.top);
163943
- const { report, artifactPath, resolution, artifact, banner } = runHistoryReport({
163944
- path: pathArg,
163945
- cwd: context.cwd,
163946
- now: new Date,
163947
- mode: options.mode,
163948
- task: options.task,
163949
- top
163950
- });
163951
- if (options.json) {
163952
- context.output.write(toEnvelopeJson(artifact, { enveloped: options.jsonEnvelope }));
163953
- context.setExitCode(0);
163787
+ }
163788
+
163789
+ // src/commands/history/anatomy.ts
163790
+ function spurEntryPath(argv = process.argv, execPath = process.execPath) {
163791
+ const entry = argv[1];
163792
+ return entry !== undefined && /\.(?:[cm]?[jt]s)$/.test(entry) ? entry : execPath;
163793
+ }
163794
+ var OPS = {
163795
+ digest: "<artifact.json> \u2014 semantic artifact digest",
163796
+ check: "<report.md> \u2014 report-contract structure gate (PASS/FAIL)",
163797
+ paths: "--out <env> [--skill-dir d] ... \u2014 resolve the run paths into an env file",
163798
+ "assert-clean": "--baseline <porcelain> [--expect <path>]... \u2014 fail on undeclared writes",
163799
+ probe: "--artifact a --target t ... \u2014 cache disposition + provenance",
163800
+ stamp: "--candidate c --provenance p --out o \u2014 stamp provenance frontmatter",
163801
+ refresh: "--report r --out o \u2014 refresh a cache-hit report",
163802
+ publish: "<candidate.md> <target.md> \u2014 atomic publish"
163803
+ };
163804
+ function registerHistoryAnatomyCommand(history, context) {
163805
+ const run = async (argv) => {
163806
+ for (let i = 0;i < argv.length; i++) {
163807
+ if (argv[i] === "--run-id" && !acceptArtifactId(argv[i + 1] ?? "", "run id", context))
163954
163808
  return;
163955
- }
163956
- if (resolution === "pointer") {
163957
- const staleBanner = stalenessBanner(artifact.generatedAt, new Date);
163958
- if (staleBanner !== null)
163959
- context.output.write(staleBanner);
163960
- }
163961
- if (banner !== null)
163962
- context.output.write(`${banner}
163963
- `);
163964
- context.output.write(report);
163965
- context.output.write(`
163966
- (artifact: ${artifactPath})
163967
- `);
163968
- context.setExitCode(0);
163969
- } catch (e) {
163970
- const message = `spur history report failed: ${e.message}`;
163971
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message }, { enveloped: options.jsonEnvelope, error: { code: "INTERNAL_ERROR", message } }) : message);
163972
- context.setExitCode(1);
163973
163809
  }
163974
- });
163810
+ await Promise.resolve().then(() => init_history_anatomy_cli());
163811
+ const result = runCacheCli(argv, {
163812
+ helper: spurEntryPath(),
163813
+ cwd: context.cwd,
163814
+ ...context.env.HOME !== undefined ? { home: context.env.HOME } : {}
163815
+ });
163816
+ writeStepText(context.output, "stdout", result.stdout);
163817
+ writeStepText(context.output, "stderr", result.stderr);
163818
+ context.setExitCode(result.exitCode);
163819
+ };
163820
+ for (const [op, synopsis] of Object.entries(OPS)) {
163821
+ history.command(`anatomy-${op}`).summary(`history-anatomy cache op: ${synopsis}`).description(`history-anatomy cache op (ADR-079): ${op} ${synopsis}
163822
+ Exit: 0 ok, 1 usage or op failure.`).allowUnknownOption(true).allowExcessArguments(true).argument("[argv...]", "the op flags and operands (see description)").action((argv) => run([op, ...argv]));
163823
+ }
163824
+ }
163825
+
163826
+ // src/commands/history/daily.ts
163827
+ init_src4();
163828
+ init_src();
163829
+ init_dist28();
163830
+ function registerHistoryDailyCommand(noun, context) {
163975
163831
  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|none>", `Per-source import timeout in milliseconds or 'none' (default ${DEFAULT_SOURCE_TIMEOUT_MS} = 10 min)`).option("--root <path>", "History root override (default: per-source platform dir)").option(...SHARED_OPTIONS.jsonDaily).option(...SHARED_OPTIONS.jsonEnvelope).option("--mode <name>", "Render the artifact as a .md sidecar in this mode after analyze (e.g. forensics)").action(async (options) => {
163976
163832
  let sourceTimeout;
163977
163833
  try {
@@ -163985,7 +163841,7 @@ ${formatFanOutResult(fanOut)}`);
163985
163841
  context.setExitCode(1);
163986
163842
  return;
163987
163843
  }
163988
- const svc = await makeService();
163844
+ const svc = await makeHistoryService(context);
163989
163845
  let refresh;
163990
163846
  try {
163991
163847
  refresh = parseHistoryRefreshContext(getEnvVar(HISTORY_REFRESH_CONTEXT_ENV));
@@ -164107,35 +163963,6 @@ ${formatFanOutResult(fanOut)}`);
164107
163963
  }
164108
163964
  context.output.write(options.json ? toEnvelopeJson(result, { enveloped: options.jsonEnvelope }) : formatDailyResult(result));
164109
163965
  });
164110
- registerHistoryAnatomyCommand(noun, context);
164111
- registerHistoryTimelineCommand(noun, context);
164112
- registerHistorySummaryCommand(noun, context);
164113
- }
164114
- function parseTopOrThrow(raw) {
164115
- if (raw === undefined)
164116
- return;
164117
- const n = Number(raw);
164118
- if (!Number.isInteger(n) || n < 1) {
164119
- throw new Error(`--top must be a positive integer (got "${raw}")`);
164120
- }
164121
- return n;
164122
- }
164123
- function formatFanOutResult(r) {
164124
- const lines = ["history import (fan-out)", "sources:"];
164125
- for (const e of r.entries) {
164126
- lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
164127
- }
164128
- const a = r.attribution;
164129
- lines.push(`attribution: sessions=${a.sessionsEvaluated} links-created=${a.linksCreated} ` + `links-present=${a.linksAlreadyPresent} skipped=${a.skippedEvidence} ambiguous=${a.ambiguousEvidence}`);
164130
- if (r.warnings.length > 0) {
164131
- lines.push("warnings:");
164132
- for (const w of r.warnings) {
164133
- lines.push(` [${w.code}] ${w.source ?? "-"}: ${w.detail}`);
164134
- }
164135
- }
164136
- lines.push(`exit_code: ${r.exitCode}`);
164137
- return lines.join(`
164138
- `);
164139
163966
  }
164140
163967
  function formatDailyResult(r) {
164141
163968
  const lines = ["history daily"];
@@ -164162,6 +163989,266 @@ function formatDailyResult(r) {
164162
163989
  `);
164163
163990
  }
164164
163991
 
163992
+ // src/commands/history/import.ts
163993
+ init_src4();
163994
+ init_src();
163995
+ function registerHistoryImportCommand(noun, context) {
163996
+ 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. For one session's evidence, use " + "`--file`; a full replay can exceed the per-source budget.").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|none>", `Per-source timeout in milliseconds or 'none' (default ${DEFAULT_SOURCE_TIMEOUT_MS} = 10 min)`).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).option("--capability-origin <spec>", "Classify skill calls against a released artifact: " + "source:skillName:artifactDigest:capabilityKind:originIdentity (repeatable).", (value, previous) => [...previous, value], []).action(async (options) => {
163997
+ const source = options.source ?? "all";
163998
+ if (options.file && source === "all") {
163999
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message: '--file requires a single --source, not "all".' }, {
164000
+ enveloped: options.jsonEnvelope,
164001
+ error: {
164002
+ code: "INTERNAL_ERROR",
164003
+ message: '--file requires a single --source, not "all".',
164004
+ details: { cliCode: "usage" }
164005
+ }
164006
+ }) : 'spur history import: --file requires a single --source, not "all".');
164007
+ context.setExitCode(1);
164008
+ return;
164009
+ }
164010
+ let sourceTimeout;
164011
+ try {
164012
+ sourceTimeout = resolveCliSourceTimeout(options.sourceTimeout, getEnvVars());
164013
+ } catch (e) {
164014
+ const timeoutMsg = `spur history import: ${e instanceof Error ? e.message : String(e)}`;
164015
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message: timeoutMsg }, {
164016
+ enveloped: options.jsonEnvelope,
164017
+ error: { code: "INTERNAL_ERROR", message: timeoutMsg, details: { cliCode: "usage" } }
164018
+ }) : timeoutMsg);
164019
+ context.setExitCode(1);
164020
+ return;
164021
+ }
164022
+ const mode = options.mode ?? (options.file ? "force-file" : "incremental");
164023
+ if (mode !== "full" && mode !== "incremental" && mode !== "force-file") {
164024
+ const modeMsg = `Invalid history import mode "${mode}". Expected one of: full, incremental, force-file`;
164025
+ if (options.json) {
164026
+ context.output.write(toEnvelopeJson({ status: "error", message: modeMsg }, {
164027
+ enveloped: options.jsonEnvelope,
164028
+ error: { code: "INTERNAL_ERROR", message: modeMsg, details: { cliCode: "usage" } }
164029
+ }));
164030
+ } else {
164031
+ context.output.error(modeMsg);
164032
+ }
164033
+ context.setExitCode(1);
164034
+ return;
164035
+ }
164036
+ if (options.file && mode === "full" && options.dryRun !== true) {
164037
+ const unsafeMsg = "spur history import: --file <path> --mode full without --dry-run is unsafe \u2014 full mode " + "treats the file as the authoritative source for reconciliation. Preview with --dry-run, " + "or import a single file with --mode force-file.";
164038
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message: unsafeMsg }, {
164039
+ enveloped: options.jsonEnvelope,
164040
+ error: { code: "INTERNAL_ERROR", message: unsafeMsg, details: { cliCode: "usage" } }
164041
+ }) : unsafeMsg);
164042
+ context.setExitCode(1);
164043
+ return;
164044
+ }
164045
+ let capabilityOrigins;
164046
+ if (options.capabilityOrigin.length > 0) {
164047
+ try {
164048
+ capabilityOrigins = options.capabilityOrigin.map(parseCapabilityOriginSpec);
164049
+ } catch (err) {
164050
+ const message = err instanceof Error ? err.message : String(err);
164051
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message }, {
164052
+ enveloped: options.jsonEnvelope,
164053
+ error: { code: "INTERNAL_ERROR", message, details: { cliCode: "usage" } }
164054
+ }) : `spur history import: ${message}`);
164055
+ context.setExitCode(1);
164056
+ return;
164057
+ }
164058
+ }
164059
+ const svc = await makeHistoryService(context, capabilityOrigins);
164060
+ let fanOut;
164061
+ try {
164062
+ fanOut = await svc.importAll({
164063
+ sources: source === "all" ? undefined : [source],
164064
+ file: options.file || undefined,
164065
+ root: options.root || undefined,
164066
+ mode,
164067
+ dryRun: options.dryRun === true,
164068
+ sourceTimeout,
164069
+ onScope: (line) => context.output.error(line)
164070
+ });
164071
+ } catch (e) {
164072
+ if (e instanceof UnsafeHistoryImporterError) {
164073
+ const msg = `spur history import: ${e.code}: ${e.message}`;
164074
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message: msg }, {
164075
+ enveloped: options.jsonEnvelope,
164076
+ error: {
164077
+ code: "INTERNAL_ERROR",
164078
+ message: msg,
164079
+ details: {
164080
+ cliCode: e.code,
164081
+ installedVersion: e.installedVersion,
164082
+ minSafeVersion: e.minSafeVersion
164083
+ }
164084
+ }
164085
+ }) : msg);
164086
+ context.setExitCode(1);
164087
+ return;
164088
+ }
164089
+ throw e;
164090
+ }
164091
+ const provenance = await resolveImportProvenance(context.fs);
164092
+ context.output.write(options.json ? toEnvelopeJson({ ...fanOut, provenance }, { enveloped: options.jsonEnvelope }) : `${formatProvenance(provenance)}
164093
+ ${formatFanOutResult(fanOut)}`);
164094
+ context.setExitCode(fanOut.exitCode);
164095
+ });
164096
+ }
164097
+ function formatProvenance(provenance) {
164098
+ return `binary: ${provenance.binary}
164099
+ importer: @gobing-ai/ts-llm-jsonl-importer@${provenance.importer}`;
164100
+ }
164101
+ function formatFanOutResult(r) {
164102
+ const lines = ["history import (fan-out)", "sources:"];
164103
+ for (const e of r.entries) {
164104
+ lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
164105
+ }
164106
+ const a = r.attribution;
164107
+ lines.push(`attribution: sessions=${a.sessionsEvaluated} links-created=${a.linksCreated} ` + `links-present=${a.linksAlreadyPresent} skipped=${a.skippedEvidence} ambiguous=${a.ambiguousEvidence}`);
164108
+ if (r.warnings.length > 0) {
164109
+ lines.push("warnings:");
164110
+ for (const w of r.warnings) {
164111
+ lines.push(` [${w.code}] ${w.source ?? "-"}: ${w.detail}`);
164112
+ }
164113
+ }
164114
+ lines.push(`exit_code: ${r.exitCode}`);
164115
+ return lines.join(`
164116
+ `);
164117
+ }
164118
+
164119
+ // src/commands/history/report.ts
164120
+ init_src4();
164121
+ init_src2();
164122
+ function registerHistoryReportCommand(noun, context) {
164123
+ 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(...SHARED_OPTIONS.jsonEnvelope).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) => {
164124
+ try {
164125
+ const top = parseTopOrThrow(options.top);
164126
+ const { report, artifactPath, resolution, artifact, banner } = runHistoryReport({
164127
+ path: pathArg,
164128
+ cwd: context.cwd,
164129
+ now: new Date,
164130
+ mode: options.mode,
164131
+ task: options.task,
164132
+ top
164133
+ });
164134
+ if (options.json) {
164135
+ context.output.write(toEnvelopeJson(artifact, { enveloped: options.jsonEnvelope }));
164136
+ context.setExitCode(0);
164137
+ return;
164138
+ }
164139
+ if (resolution === "pointer") {
164140
+ const staleBanner = stalenessBanner(artifact.generatedAt, new Date);
164141
+ if (staleBanner !== null)
164142
+ context.output.write(staleBanner);
164143
+ }
164144
+ if (banner !== null)
164145
+ context.output.write(`${banner}
164146
+ `);
164147
+ context.output.write(report);
164148
+ context.output.write(`
164149
+ (artifact: ${artifactPath})
164150
+ `);
164151
+ context.setExitCode(0);
164152
+ } catch (e) {
164153
+ const message = `spur history report failed: ${e.message}`;
164154
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message }, { enveloped: options.jsonEnvelope, error: { code: "INTERNAL_ERROR", message } }) : message);
164155
+ context.setExitCode(1);
164156
+ }
164157
+ });
164158
+ }
164159
+
164160
+ // src/commands/history/reset.ts
164161
+ function registerHistoryResetCommand(noun, context) {
164162
+ noun.command("reset").description("Destructively wipe every history_* table: normalized rows, per-source ETL, board rollups, " + "daily stats, and importer checkpoints/ledger. Requires --yes. Task corpus and run " + "provenance (task_run_links) are untouched; a full `spur history import` rebuilds everything.").option("--yes", "Confirm the destructive wipe", false).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
164163
+ if (!options.yes) {
164164
+ context.output.write(options.json ? toEnvelopeJson({ status: "error", message: "Refusing to wipe history tables without --yes." }, {
164165
+ enveloped: options.jsonEnvelope,
164166
+ error: {
164167
+ code: "INTERNAL_ERROR",
164168
+ message: "Refusing to wipe history tables without --yes.",
164169
+ details: { cliCode: "usage" }
164170
+ }
164171
+ }) : "spur history reset: refusing to wipe history tables without --yes.");
164172
+ context.setExitCode(1);
164173
+ return;
164174
+ }
164175
+ const svc = await makeHistoryService(context);
164176
+ const result = await svc.resetHistory();
164177
+ const note = result.unknown.length > 0 ? ` WARNING: ${result.unknown.length} unlisted history_* table(s) left intact: ${result.unknown.join(", ")}` : "";
164178
+ context.output.write(options.json ? toEnvelopeJson({
164179
+ cleared: result.cleared,
164180
+ skipped: result.skipped,
164181
+ unknown: result.unknown,
164182
+ clearedCount: result.cleared.length
164183
+ }, { enveloped: options.jsonEnvelope }) : `cleared ${result.cleared.length} history tables` + (result.skipped.length > 0 ? ` (${result.skipped.length} not present)` : "") + note);
164184
+ });
164185
+ }
164186
+
164187
+ // src/commands/history/summary.ts
164188
+ function registerHistorySummaryCommand(history, context) {
164189
+ history.command("summary").summary("Daily summary report: tokens, git activity, history health, annotations").description([
164190
+ "Generate the day\u2019s Markdown summary and write it to docs/daily/summary_<YYYYMMDD>.md",
164191
+ "(or --output). Prompts for learnings / issues / pending unless SP_DAILY_SUMMARY_NO_PROMPT=1",
164192
+ "(non-TTY stdin supplies the three answers as lines). Exit: 0 written, 1 generation failed."
164193
+ ].join(`
164194
+ `)).option("--date <date>", "YYYY-MM-DD, today or yesterday (default: today)").option(...SHARED_OPTIONS.dryRunHistorySummary).option("--output <path>", "write to a custom path").option("--no-git", "skip git history collection").option("--no-ccusage", "skip token usage collection").action(async (options) => {
164195
+ await Promise.resolve().then(() => init_daily_summary());
164196
+ const argv = [];
164197
+ if (options.date !== undefined)
164198
+ argv.push("--date", options.date);
164199
+ if (options.dryRun === true)
164200
+ argv.push("--dry-run");
164201
+ if (options.output !== undefined)
164202
+ argv.push("--output", options.output);
164203
+ if (options.git === false)
164204
+ argv.push("--no-git");
164205
+ if (options.ccusage === false)
164206
+ argv.push("--no-ccusage");
164207
+ const code = await main(argv, {
164208
+ out: (line) => context.output.write(line),
164209
+ err: (line) => context.output.error(line)
164210
+ });
164211
+ context.setExitCode(code);
164212
+ });
164213
+ }
164214
+
164215
+ // src/commands/history/timeline.ts
164216
+ function registerHistoryTimelineCommand(history, context) {
164217
+ history.command("timeline").summary("Per-prompt work/wait time and tokens from the host transcript (JSON)").description([
164218
+ "Read the active host transcript read-only (CLAUDE_CODE_SESSION_ID under ~/.claude/projects,",
164219
+ "PI_SESSION_FILE, or --transcript) and print one JSON timeline: a segment per operator prompt,",
164220
+ 'optional --group stages, and totals. No transcript \u2192 {"available":false,...}, exit 0.',
164221
+ "Exit: 0 timeline (or unavailable), 2 invalid --group or unreadable transcript."
164222
+ ].join(`
164223
+ `)).option("--transcript <path>", "transcript JSONL path (default: the active host session)").option("--group <spec>", 'sum segments into stages, e.g. "1-3,4"').action(async (options) => {
164224
+ await Promise.resolve().then(() => init_session_timeline());
164225
+ const argv = [];
164226
+ if (options.transcript !== undefined)
164227
+ argv.push("--transcript", options.transcript);
164228
+ if (options.group !== undefined)
164229
+ argv.push("--group", options.group);
164230
+ const code = runSessionTimelineCli(argv, {
164231
+ env: context.env,
164232
+ write: (s) => writeStepText(context.output, "stdout", s),
164233
+ writeErr: (s) => writeStepText(context.output, "stderr", s)
164234
+ });
164235
+ context.setExitCode(code);
164236
+ });
164237
+ }
164238
+
164239
+ // src/commands/history/index.ts
164240
+ function registerHistoryCommand(program, context) {
164241
+ const noun = program.command("history").summary("import and analyze coding-agent history");
164242
+ registerHistoryImportCommand(noun, context);
164243
+ registerHistoryAnalyzeCommand(noun, context);
164244
+ registerHistoryResetCommand(noun, context);
164245
+ registerHistoryReportCommand(noun, context);
164246
+ registerHistoryDailyCommand(noun, context);
164247
+ registerHistoryAnatomyCommand(noun, context);
164248
+ registerHistoryTimelineCommand(noun, context);
164249
+ registerHistorySummaryCommand(noun, context);
164250
+ }
164251
+
164165
164252
  // src/commands/init.ts
164166
164253
  init_src4();
164167
164254
  init_src();
@@ -164542,13 +164629,13 @@ ${entry}
164542
164629
  }
164543
164630
  function describeZgReadiness(status) {
164544
164631
  if (!status.zg.available)
164545
- return `zg not installed (optional) \u2014 install: bun add -g @zvec/zvec-grep`;
164632
+ return `zg not installed (optional) \u2014 install: ${ZG_INSTALL_HINT}`;
164546
164633
  if (status.zg.index === "ready")
164547
164634
  return "zg ready";
164548
164635
  if (status.zg.index === "stale")
164549
- return "zg index stale \u2014 run: zg index";
164636
+ return `zg index stale \u2014 run: ${ZG_REFRESH_HINT}`;
164550
164637
  if (status.zg.index === "missing")
164551
- return "zg index missing \u2014 run: zg index --embedding local/potion-code-16m-v2";
164638
+ return `zg index missing \u2014 run: ${ZG_INDEX_HINT}`;
164552
164639
  return "zg status unknown";
164553
164640
  }
164554
164641
 
@@ -171148,7 +171235,7 @@ init_dist5();
171148
171235
  import { homedir as homedir17 } from "os";
171149
171236
  import { basename as basename21, join as join94, relative as relative19, resolve as resolve37 } from "path";
171150
171237
  // ../server/package.json
171151
- var version4 = "0.4.22";
171238
+ var version4 = "0.4.24";
171152
171239
 
171153
171240
  // ../server/src/modules/health/index.ts
171154
171241
  var startedAt = Date.now();
@@ -176358,7 +176445,7 @@ init_src();
176358
176445
  init_dist5();
176359
176446
  import { mkdirSync as mkdirSync23, readFileSync as readFileSync28, renameSync as renameSync10, writeFileSync as writeFileSync20 } from "fs";
176360
176447
  import { join as join100 } from "path";
176361
- var COMMIT_GUARD_USAGE = "usage: spur workflow commit-guard start --run <id> | stage --run <id> [--base <sha>] -- <paths\u2026> | check --run <id> [--base <sha>]";
176448
+ var COMMIT_GUARD_USAGE = "usage: spur workflow commit-guard-start --run <id> | commit-guard-stage --run <id> [--base <sha>] -- <paths\u2026> | commit-guard-check --run <id> [--base <sha>]";
176362
176449
  var isBookkeeping = (path) => path === ".spur" || path.startsWith(".spur/");
176363
176450
  var at2 = (options, rel) => options.cwd ? join100(options.cwd, rel) : rel;
176364
176451
  var artifactPath = (run) => join100(".spur", "run", `${run}-tree-start.json`);
@@ -176449,7 +176536,7 @@ var report = (exitCode, stdout = [], stderr = []) => ({
176449
176536
  stderr
176450
176537
  });
176451
176538
  function missingStart(run) {
176452
- return report(1, [], [`commit-guard: no ${artifactPath(run)} \u2014 run \`spur workflow commit-guard start --run ${run}\` first`]);
176539
+ return report(1, [], [`commit-guard: no ${artifactPath(run)} \u2014 run \`spur workflow commit-guard-start --run ${run}\` first`]);
176453
176540
  }
176454
176541
  function commitGuardUsage() {
176455
176542
  return report(1, [], [COMMIT_GUARD_USAGE]);
@@ -176580,21 +176667,21 @@ function registerWorkflowCommitGuardCommand(workflow, context) {
176580
176667
  return;
176581
176668
  emit(guarded(run));
176582
176669
  };
176583
- const guard = workflow.command("commit-guard").summary("stage only the paths this run wrote; report the foreign ones").description([
176584
- "Run-scoped commit guard for the inline driver: `start` fingerprints the tree before the",
176585
- "run writes, `stage` stages only listed paths the run wrote, `check` reports foreign changes.",
176586
- "Exit codes: 0 ok, 1 usage or failed probe (nothing staged), 2 stage refused a path."
176587
- ].join(`
176588
- `));
176589
- guard.command("start").description("Fingerprint HEAD and the dirty working files into .spur/run/<run>-tree-start.json").option("--run <id>", "Run id the snapshot belongs to").allowExcessArguments(false).action((options) => withRun(options.run, (run) => commitGuardStart(run, { cwd: context.cwd })));
176590
- guard.command("stage").description("Stage only the listed paths this run wrote; print {staged, foreign}").argument("[paths...]", "Candidate paths (after `--`)").option("--run <id>", "Run id whose tree-start snapshot partitions the paths").option("--base <sha>", "Diff base (default: the snapshot HEAD)").action((paths, options) => withRun(options.run, (run) => commitGuardStage(run, paths, {
176591
- cwd: context.cwd,
176592
- ...options.base === undefined ? {} : { base: options.base }
176593
- })));
176594
- guard.command("check").description("Report working-tree changes this run did not write; stage nothing").option("--run <id>", 'Run id whose tree-start snapshot defines "written by this run"').option("--base <sha>", "Diff base (default: the snapshot HEAD)").allowExcessArguments(false).action((options) => withRun(options.run, (run) => commitGuardCheck(run, {
176595
- cwd: context.cwd,
176596
- ...options.base === undefined ? {} : { base: options.base }
176597
- })));
176670
+ workflow.command("commit-guard-start").description("Fingerprint HEAD and the dirty working files into .spur/run/<run>-tree-start.json").option("--run <id>", "Run id the snapshot belongs to").allowExcessArguments(false).action((options) => {
176671
+ withRun(options.run, (run) => commitGuardStart(run, { cwd: context.cwd }));
176672
+ });
176673
+ workflow.command("commit-guard-stage").description("Stage only the listed paths this run wrote; print {staged, foreign}").argument("[paths...]", "Candidate paths (after `--`)").option("--run <id>", "Run id whose tree-start snapshot partitions the paths").option("--base <sha>", "Diff base (default: the snapshot HEAD)").action((paths, options) => {
176674
+ withRun(options.run, (run) => commitGuardStage(run, paths, {
176675
+ cwd: context.cwd,
176676
+ ...options.base === undefined ? {} : { base: options.base }
176677
+ }));
176678
+ });
176679
+ workflow.command("commit-guard-check").description("Report working-tree changes this run did not write; stage nothing").option("--run <id>", 'Run id whose tree-start snapshot defines "written by this run"').option("--base <sha>", "Diff base (default: the snapshot HEAD)").allowExcessArguments(false).action((options) => {
176680
+ withRun(options.run, (run) => commitGuardCheck(run, {
176681
+ cwd: context.cwd,
176682
+ ...options.base === undefined ? {} : { base: options.base }
176683
+ }));
176684
+ });
176598
176685
  }
176599
176686
 
176600
176687
  // src/commands/workflow/continue.ts
@@ -177089,7 +177176,7 @@ async function runBudgetGate(opts) {
177089
177176
  // src/commands/workflow/cost.ts
177090
177177
  var collect3 = (value, previous) => [...previous, value];
177091
177178
  function registerWorkflowCostCommand(workflow, context, deps = {}) {
177092
- workflow.command("cost").description([
177179
+ workflow.command("cost").summary("Report per-workflow wall-clock and cost from real runs (or --budget gate)").description([
177093
177180
  "Report per-workflow wall-clock and cost from REAL (terminal, non-dry) runs in .spur/spur.db.",
177094
177181
  "The cohort defaults to every definition in .spur/workflows/ (bookkeeping workflows excluded).",
177095
177182
  "--json emits the byte-stable baseline payload (workflow + state rows, key-sorted).",
@@ -178503,7 +178590,13 @@ function registerWorkflowCommand(program, context) {
178503
178590
  }
178504
178591
 
178505
178592
  // src/workflow-transport-argv.ts
178506
- var WORKFLOW_TRANSPORT_VERBS = ["commit-guard", "persist-out", "persist-check"];
178593
+ var WORKFLOW_TRANSPORT_VERBS = [
178594
+ "commit-guard-start",
178595
+ "commit-guard-stage",
178596
+ "commit-guard-check",
178597
+ "persist-out",
178598
+ "persist-check"
178599
+ ];
178507
178600
  function isWorkflowTransportInvocation(argv) {
178508
178601
  const tokens = argv.filter((token) => !["--no-logo", "--cli-verbose", "-v"].includes(token));
178509
178602
  return tokens[0] === "workflow" && WORKFLOW_TRANSPORT_VERBS.includes(tokens[1] ?? "");
@@ -178516,7 +178609,7 @@ function registerWorkflowTransportCommands(workflow, context) {
178516
178609
  }
178517
178610
 
178518
178611
  // src/index.ts
178519
- async function main3(argv = process.argv.slice(2), options = {}) {
178612
+ async function main2(argv = process.argv.slice(2), options = {}) {
178520
178613
  const output = options.output ?? consoleOutput;
178521
178614
  let exitCode = 0;
178522
178615
  const cwd = options.cwd ?? process.cwd();
@@ -178713,7 +178806,7 @@ async function runCli() {
178713
178806
  consoleOutput.write(bannerText());
178714
178807
  }
178715
178808
  try {
178716
- return await main3();
178809
+ return await main2();
178717
178810
  } finally {
178718
178811
  await disposeDecisionServices();
178719
178812
  }
@@ -178724,7 +178817,7 @@ if (import.meta.main) {
178724
178817
  export {
178725
178818
  bannerText,
178726
178819
  buildProgram,
178727
- main3 as main,
178820
+ main2 as main,
178728
178821
  runCli,
178729
178822
  shouldRenderBanner
178730
178823
  };