@gobing-ai/spur 0.4.23 → 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();
@@ -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",
@@ -161004,6 +161008,98 @@ async function publishPackage(target, opts = {}) {
161004
161008
  }
161005
161009
  }
161006
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
+
161007
161103
  // ../../packages/app/src/services/pr-review/index.ts
161008
161104
  init_src();
161009
161105
  import { spawnSync as spawnSync7 } from "child_process";
@@ -161997,7 +162093,7 @@ function registerBuilderPrCommand(builder, context) {
161997
162093
  "returns PENDING. Exit codes: 0 ok \xB7 2 hard failure \xB7 3 wait timeout",
161998
162094
  "(verdict PENDING \u2014 pending, never failure)."
161999
162095
  ].join(`
162000
- `)).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) => {
162001
162097
  run("review", {
162002
162098
  ...options.force === true ? { force: true } : {},
162003
162099
  ...options.focus !== undefined ? { focus: options.focus } : {},
@@ -162010,6 +162106,27 @@ function registerBuilderPrCommand(builder, context) {
162010
162106
  });
162011
162107
  }
162012
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
+ }
162127
+ });
162128
+ }
162129
+
162013
162130
  // ../../packages/app/src/services/branch-topology/render.ts
162014
162131
  var INDENT = " ";
162015
162132
  var MERMAID_DEFAULT_BRANCH = "main";
@@ -162500,7 +162617,7 @@ function registerBuilderShowBranchesCommand(builder, context) {
162500
162617
  "Exit codes: 0 ok \xB7 1 usage (bad --format/--limit/--max-commits, unknown base ref,",
162501
162618
  "--local with --remote) \xB7 2 git failure (not a repository, git missing)."
162502
162619
  ].join(`
162503
- `)).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) => {
162504
162621
  const format = resolveFormat(options.format, command.getOptionValueSource("format") === "cli", options.json === true, context);
162505
162622
  if (format === null)
162506
162623
  return;
@@ -162575,110 +162692,12 @@ function usageError(message, context) {
162575
162692
  context.setExitCode(1);
162576
162693
  }
162577
162694
 
162578
- // src/commands/builder.ts
162695
+ // src/commands/builder/index.ts
162579
162696
  function registerBuilderCommand(program, context) {
162580
162697
  const noun = program.command("builder").summary("release plumbing: version bumps, tags, branch topology, and PR-review ops");
162581
- noun.command("bump-ver").summary("bump a package version, commit, tag, optionally push").description([
162582
- "Bump one workspace package (or every released package with --all),",
162583
- "rewrite workspace pins, commit, and tag. A bare version bumps all:",
162584
- " spur builder bump-ver 0.1.4 # every released package",
162585
- " spur builder bump-ver spur 0.1.4 # one package by id",
162586
- " spur builder bump-ver --check spur # pre-publish drift guard (read-only)",
162587
- "--check fails (exit 1) when .claude-plugin/marketplace.json or a listed plugin.json",
162588
- "drifted from the package version; target defaults to the aggregate package."
162589
- ].join(`
162590
- `)).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) => {
162591
- const args = [target, version].filter((value) => value !== undefined);
162592
- if (options.all === true)
162593
- args.push("--all");
162594
- if (options.push === true)
162595
- args.push("--push");
162596
- try {
162597
- if (options.check === true) {
162598
- if (version !== undefined)
162599
- throw new Error("bump-ver --check takes only a package id");
162600
- if (options.all === true || options.push === true)
162601
- throw new Error("bump-ver --check rejects incompatible write flags (--push/--all)");
162602
- const code = await checkVersionDrift(target, context.cwd, context.output, context.spurConfig);
162603
- if (options.json === true) {
162604
- context.output.write(toEnvelopeJson({ ok: code === 0, verb: "bump-ver", check: true, target: target ?? "aggregate" }, { enveloped: options.jsonEnvelope }));
162605
- }
162606
- if (code !== 0)
162607
- context.setExitCode(code);
162608
- return;
162609
- }
162610
- await bumpVer(args, context.cwd, context.output, context.spurConfig);
162611
- if (options.json === true) {
162612
- context.output.write(toEnvelopeJson({ ok: true, verb: "bump-ver", target: target ?? "all", version: version ?? "" }, { enveloped: options.jsonEnvelope }));
162613
- }
162614
- } catch (err) {
162615
- const message = err instanceof Error ? err.message : String(err);
162616
- if (options.json === true) {
162617
- context.output.write(toEnvelopeJson({ ok: false, verb: "bump-ver", error: message }, {
162618
- enveloped: options.jsonEnvelope,
162619
- error: {
162620
- code: "INTERNAL_ERROR",
162621
- message,
162622
- details: { cliCode: "bump-ver", verb: "bump-ver" }
162623
- }
162624
- }));
162625
- context.setExitCode(1);
162626
- return;
162627
- }
162628
- context.output.error(message);
162629
- context.setExitCode(1);
162630
- }
162631
- });
162632
- noun.command("drop-tags").summary("delete release tags locally and optionally on origin").description([
162633
- "Delete one package release tag (or every released tag plus the aggregate",
162634
- "with --all / a bare version). Use --remote to also delete on origin."
162635
- ].join(`
162636
- `)).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) => {
162637
- const args = [target, version].filter((value) => value !== undefined);
162638
- if (options.all === true)
162639
- args.push("--all");
162640
- if (options.remote === true)
162641
- args.push("--remote");
162642
- try {
162643
- await dropTags(args, context.cwd, context.output, context.spurConfig);
162644
- if (options.json === true) {
162645
- context.output.write(toEnvelopeJson({ ok: true, verb: "drop-tags", target: target ?? "all", version: version ?? "" }, { enveloped: options.jsonEnvelope }));
162646
- }
162647
- } catch (err) {
162648
- const message = err instanceof Error ? err.message : String(err);
162649
- if (options.json === true) {
162650
- context.output.write(toEnvelopeJson({ ok: false, verb: "drop-tags", error: message }, {
162651
- enveloped: options.jsonEnvelope,
162652
- error: {
162653
- code: "INTERNAL_ERROR",
162654
- message,
162655
- details: { cliCode: "drop-tags", verb: "drop-tags" }
162656
- }
162657
- }));
162658
- context.setExitCode(1);
162659
- return;
162660
- }
162661
- context.output.error(message);
162662
- context.setExitCode(1);
162663
- }
162664
- });
162665
- noun.command("publish").summary("resolve workspace/catalog ranges, build, and npm publish a package (CI)").description([
162666
- "CI publish for npm Trusted Publishing (OIDC): rewrite workspace:/catalog: ranges in",
162667
- "<dir>/package.json, run `bun run build:bundle`, `npm publish --access public`, and",
162668
- "restore the manifest on every exit path."
162669
- ].join(`
162670
- `)).option("--otp <code>", "npm one-time password (local publishes only)").argument("<dir>", "package directory, relative to the workspace root").action(async (dir, options) => {
162671
- try {
162672
- await publishPackage(dir, {
162673
- repoRoot: context.cwd,
162674
- output: context.output,
162675
- ...options.otp !== undefined ? { otp: options.otp } : {}
162676
- });
162677
- } catch (err) {
162678
- context.output.error(err instanceof Error ? err.message : String(err));
162679
- context.setExitCode(1);
162680
- }
162681
- });
162698
+ registerBuilderBumpVerCommand(noun, context);
162699
+ registerBuilderDropTagsCommand(noun, context);
162700
+ registerBuilderPublishCommand(noun, context);
162682
162701
  registerBuilderShowBranchesCommand(noun, context);
162683
162702
  registerBuilderPrCommand(noun, context);
162684
162703
  }
@@ -163114,161 +163133,35 @@ function fail2(context, options, error) {
163114
163133
  context.setExitCode(1);
163115
163134
  }
163116
163135
 
163117
- // src/commands/feature.ts
163136
+ // src/commands/feature/advance.ts
163118
163137
  init_src4();
163119
- init_src2();
163120
163138
 
163121
- // src/commands/feature/handoff.ts
163122
- function registerFeatureHandoffCommand(feature, context) {
163123
- feature.command("handoff").summary("Finalize the idea pipeline: task ordering, roster refresh and the handoff report.").description([
163124
- "Reads .spur/run/<runId>-idea-{task-batch,batch-create-result,task-order}.json (and the optional",
163125
- "-idea-ready.json), applies dependencies, refreshes the roster, checks each task and writes",
163126
- ".spur/run/<runId>-idea-handoff.md with one recommended next command.",
163127
- "Exit: 0 success; 1 failure (missing ids or inputs, a mapping or CLI error).",
163128
- "Flags fall back to the workflow env names: featureId, __runId, spurBin."
163129
- ].join(`
163130
- `)).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) => {
163131
- const runId = options.runId ?? context.env.__runId;
163132
- if (runId && !acceptArtifactId(runId, "run id", context))
163133
- return;
163134
- await Promise.resolve().then(() => init_src4());
163135
- const env = context.env;
163136
- const outcome = await runIdeaHandoffCli({
163137
- __runId: options.runId ?? env.__runId,
163138
- ...options.spurBin === undefined && env.spurBin === undefined ? { spurArgv: resolveSpurArgv() } : {},
163139
- featureId: id ?? env.featureId,
163140
- spurBin: options.spurBin ?? env.spurBin ?? resolveSpurBin()
163141
- });
163142
- context.setExitCode(outcome.exitCode);
163143
- });
163139
+ // src/commands/feature/shared.ts
163140
+ init_src4();
163141
+ async function makeReceiptRunPort(context) {
163142
+ return createFeatureReceiptRunPort(await context.getDb(), context.fs);
163144
163143
  }
163145
-
163146
- // src/commands/feature/verify.ts
163147
- function registerFeatureVerifyCommand(feature, context) {
163148
- feature.command("verify").summary("Run the feature-verification pass and record its bound PASS/FAIL evidence receipt.").description([
163149
- "Runs the selected feature-verification definition\u2019s command once (or --cmd) and writes the",
163150
- "run-scoped + feature-latest receipts and .spur/run/<runId>-feature-verification.status.",
163151
- "Exit: 0 after a completed pass (PASS or FAIL); 1 when the pass could not run.",
163152
- "Without --run-id the pass opens and finalizes its own feature-lifecycle run row.",
163153
- "Flags fall back to the workflow env names: featureId, __runId."
163154
- ].join(`
163155
- `)).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) => {
163156
- await Promise.resolve().then(() => init_src4());
163157
- const env = context.env;
163158
- const featureId = id || env.featureId || "";
163159
- try {
163160
- const result = await runFeatureVerification({
163161
- workdir: context.cwd,
163162
- featureId,
163163
- runId: options.runId || env.__runId || "",
163164
- cmdOverride: options.cmd || env.verificationCmd || ""
163165
- });
163166
- context.output.write(`feature verify: ${featureId} ${result.message}`);
163167
- } catch (err) {
163168
- context.output.error(`feature verify: ${String(err)}`);
163169
- context.setExitCode(1);
163170
- }
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
163171
163160
  });
163172
163161
  }
163173
163162
 
163174
- // src/commands/feature.ts
163175
- function registerFeatureCommand(program, context) {
163176
- const feature = program.command("feature").summary("manage features (hierarchical IDs)");
163177
- 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) => {
163178
- const svc = await makeService2(context, options.folder);
163179
- try {
163180
- const result = await svc.create(name, options.parent);
163181
- if (options.json) {
163182
- context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
163183
- } else {
163184
- context.output.write(`Created feature ${result.ref.id}: ${result.ref.filePath}`);
163185
- }
163186
- } catch (err) {
163187
- writeJsonError(context.output, options, String(err));
163188
- context.setExitCode(1);
163189
- }
163190
- });
163191
- 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) => {
163192
- const svc = await makeService2(context, options.folder);
163193
- try {
163194
- const result = await svc.show(id);
163195
- if (result === null) {
163196
- writeJsonError(context.output, options, `Feature ${id} not found`, "NOT_FOUND");
163197
- context.setExitCode(1);
163198
- return;
163199
- }
163200
- if (options.json) {
163201
- const { content, ...rest } = result;
163202
- context.output.write(toEnvelopeJson({ ...rest, content }, { enveloped: options.jsonEnvelope }));
163203
- } else {
163204
- context.output.write(result.content);
163205
- }
163206
- } catch (err) {
163207
- writeJsonError(context.output, options, String(err));
163208
- context.setExitCode(1);
163209
- }
163210
- });
163211
- 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", [
163212
- "Section names are validated against the closed-world canonical set; a rejected",
163213
- "`--section` costs a failed write. List valid names first:",
163214
- "`spur task sections <wbs> list`."
163215
- ].join(`
163216
- `)).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) => {
163217
- const svc = await makeService2(context, options.folder);
163218
- try {
163219
- let result;
163220
- if (options.section !== undefined) {
163221
- if (options.fromFile === undefined) {
163222
- writeJsonError(context.output, options, "--from-file is required with --section", "VALIDATION_FAILED");
163223
- context.setExitCode(2);
163224
- return;
163225
- }
163226
- result = await svc.updateSection(id, options.section, options.fromFile);
163227
- if (!options.json) {
163228
- for (const warning of result.warnings ?? []) {
163229
- context.output.error(warning);
163230
- }
163231
- context.output.write(`Updated section '${options.section}' in feature ${result.ref.id}`);
163232
- }
163233
- } else if (options.fromFile !== undefined) {
163234
- writeJsonError(context.output, options, "--section is required with --from-file", "VALIDATION_FAILED");
163235
- context.setExitCode(2);
163236
- return;
163237
- }
163238
- if (options.field !== undefined) {
163239
- if (options.value === undefined) {
163240
- writeJsonError(context.output, options, "--value is required with --field", "VALIDATION_FAILED");
163241
- context.setExitCode(2);
163242
- return;
163243
- }
163244
- result = await svc.update(id, options.field, options.value);
163245
- if (!options.json) {
163246
- context.output.write(`Updated ${options.field} on feature ${result.ref.id}`);
163247
- }
163248
- } else if (options.value !== undefined) {
163249
- writeJsonError(context.output, options, "--field is required with --value", "VALIDATION_FAILED");
163250
- context.setExitCode(2);
163251
- return;
163252
- }
163253
- if (status !== undefined) {
163254
- result = await svc.transition(id, status);
163255
- if (!options.json) {
163256
- context.output.write(`${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
163257
- }
163258
- }
163259
- if (result === undefined) {
163260
- writeJsonError(context.output, options, "Either <status>, --field/--value, or --section/--from-file is required", "VALIDATION_FAILED");
163261
- context.setExitCode(2);
163262
- return;
163263
- }
163264
- if (options.json) {
163265
- context.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
163266
- }
163267
- } catch (err) {
163268
- writeJsonError(context.output, options, String(err));
163269
- context.setExitCode(1);
163270
- }
163271
- });
163163
+ // src/commands/feature/advance.ts
163164
+ function registerFeatureAdvanceCommand(feature, context) {
163272
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) => {
163273
163166
  const svc = await makeService2(context, options.folder);
163274
163167
  const target = options.to ?? "done";
@@ -163333,6 +163226,148 @@ function registerFeatureCommand(program, context) {
163333
163226
  context.setExitCode(1);
163334
163227
  }
163335
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) {
163336
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) => {
163337
163372
  const svc = await makeService2(context, options.folder);
163338
163373
  try {
@@ -163359,6 +163394,10 @@ function registerFeatureCommand(program, context) {
163359
163394
  context.setExitCode(1);
163360
163395
  }
163361
163396
  });
163397
+ }
163398
+
163399
+ // src/commands/feature/move.ts
163400
+ function registerFeatureMoveCommand(feature, context) {
163362
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) => {
163363
163402
  const svc = await makeService2(context, options.folder);
163364
163403
  try {
@@ -163379,6 +163418,11 @@ function registerFeatureCommand(program, context) {
163379
163418
  context.setExitCode(1);
163380
163419
  }
163381
163420
  });
163421
+ }
163422
+
163423
+ // src/commands/feature/refresh.ts
163424
+ init_src4();
163425
+ function registerFeatureRefreshCommand(feature, context) {
163382
163426
  feature.command("refresh").summary("Rebuild INDEX.md and each feature ## Tasks table from task edges (docs only \u2014 no status change).").description([
163383
163427
  "Regenerate derived views so feature markdown matches the task graph:",
163384
163428
  " \u2022 docs/features/INDEX.md (ID-encoded tree)",
@@ -163424,69 +163468,34 @@ function registerFeatureCommand(program, context) {
163424
163468
  context.setExitCode(1);
163425
163469
  }
163426
163470
  });
163427
- 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) => {
163428
- const resolved = await resolvePlanningFolders(context.fs);
163429
- const featuresDir = options.folder ?? context.fs.resolve(resolved.featuresDir);
163430
- const tasksDir = context.fs.resolve(resolved.tasksDir);
163431
- const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context.fs.resolve(p));
163432
- if (!tasksDirs.includes(tasksDir))
163433
- tasksDirs.unshift(tasksDir);
163434
- const svc = new FeatureCheckService(context.fs);
163435
- const json = options.json === true;
163436
- const strict = options.strict === true;
163437
- 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);
163438
163477
  try {
163439
- const entries = await context.fs.readDir(featuresDir);
163440
- const ids = id ? [id] : entries.map((n) => n.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1]).filter((n) => n !== undefined);
163441
- const results = [];
163442
- for (const fid of ids) {
163443
- const fileName = entries.find((n) => isFeatureFile(n, fid));
163444
- if (!fileName) {
163445
- writeJsonError(context.output, options, `Feature ${fid} not found`, "NOT_FOUND");
163446
- context.setExitCode(1);
163447
- return;
163448
- }
163449
- const result = await svc.check(`${featuresDir}/${fileName}`, fid, {
163450
- strict,
163451
- featuresDir,
163452
- tasksDir,
163453
- tasksDirs,
163454
- runDir: context.fs.resolve(".spur/run"),
163455
- receiptRunPort: await makeReceiptRunPort(context),
163456
- severityOverrides: resolved.severityOverrides,
163457
- asStatus: options.as,
163458
- fix: options.fix === true,
163459
- inventory: inventoryText,
163460
- transitionPort: options.fix === true ? async (targetId, to) => {
163461
- const featureSvc = await makeService2(context, options.folder);
163462
- return featureSvc.transition(targetId, to);
163463
- } : undefined
163464
- });
163465
- results.push(result);
163466
- if (!json) {
163467
- if (result.repairs !== undefined && result.repairs.length > 0) {
163468
- for (const r of result.repairs) {
163469
- context.output.write(` [FIX] ${r.kind} ${r.section}: ${r.detail}`);
163470
- }
163471
- }
163472
- context.output.write(`
163473
- ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
163474
- for (const f of result.findings) {
163475
- const tag = f.severity === "error" ? "ERR" : "WARN";
163476
- context.output.write(` [${tag}] ${f.layer} ${f.section}: ${f.message}`);
163477
- }
163478
- }
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;
163479
163483
  }
163480
- if (json) {
163481
- 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);
163482
163489
  }
163483
- if (results.some((r) => !r.pass))
163484
- context.setExitCode(1);
163485
163490
  } catch (err) {
163486
163491
  writeJsonError(context.output, options, String(err));
163487
163492
  context.setExitCode(1);
163488
163493
  }
163489
163494
  });
163495
+ }
163496
+
163497
+ // src/commands/feature/sync.ts
163498
+ function registerFeatureSyncCommand(feature, context) {
163490
163499
  feature.command("sync").summary("Align feature lifecycle status with linked task states (status only \u2014 runs transition guards).").description([
163491
163500
  "Derive a feature status proposal from linked task statuses and apply legal hops",
163492
163501
  "(e.g. all tasks done \u2192 advance toward done; reopened work \u2192 reopen the feature).",
@@ -163545,165 +163554,134 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
163545
163554
  context.setExitCode(1);
163546
163555
  }
163547
163556
  });
163548
- registerFeatureVerifyCommand(feature, context);
163549
- registerFeatureHandoffCommand(feature, context);
163550
163557
  }
163551
- async function makeReceiptRunPort(context) {
163552
- return createFeatureReceiptRunPort(await context.getDb(), context.fs);
163553
- }
163554
- async function makeService2(context, folderOverride) {
163555
- const resolved = await resolvePlanningFolders(context.fs);
163556
- const featuresDir = folderOverride ?? context.fs.resolve(resolved.featuresDir);
163557
- const tasksDir = context.fs.resolve(resolved.tasksDir);
163558
- const lifecycle = makeLifecycleAdapter(context, FEATURE_LIFECYCLE_PROFILE);
163559
- const writeService = new PlanningWriteService({
163560
- fs: context.fs,
163561
- ...lifecycle ? { lifecycle } : {},
163562
- emitter: makePlanningEmitter(context)
163563
- });
163564
- return new FeatureService({
163565
- fs: context.fs,
163566
- writeService,
163567
- featuresDir,
163568
- tasksDir,
163569
- 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
+ }
163570
163621
  });
163571
163622
  }
163572
- async function assertFeatureCheckPass(context, id, folderOverride, strict, asStatus) {
163573
- const resolved = await resolvePlanningFolders(context.fs);
163574
- const featuresDir = folderOverride ?? context.fs.resolve(resolved.featuresDir);
163575
- const tasksDir = context.fs.resolve(resolved.tasksDir);
163576
- const tasksDirs = Object.keys(resolved.foldersConfig.folders).map((p) => context.fs.resolve(p));
163577
- if (!tasksDirs.includes(tasksDir))
163578
- tasksDirs.unshift(tasksDir);
163579
- const entries = await context.fs.readDir(featuresDir);
163580
- const fileName = entries.find((name) => isFeatureFile(name, id));
163581
- if (fileName === undefined) {
163582
- throw new Error(`Feature ${id} not found`);
163583
- }
163584
- const result = await new FeatureCheckService(context.fs).check(`${featuresDir}/${fileName}`, id, {
163585
- strict,
163586
- featuresDir,
163587
- tasksDir,
163588
- tasksDirs,
163589
- runDir: context.fs.resolve(".spur/run"),
163590
- receiptRunPort: await makeReceiptRunPort(context),
163591
- 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
+ }
163592
163649
  });
163593
- if (!result.pass) {
163594
- const details = result.findings.map((f) => `${f.layer} ${f.section}: ${f.message}`).join("; ");
163595
- throw new Error(`Feature ${id} check failed before advance${strict ? " (strict)" : ""}: ${details}`);
163596
- }
163597
163650
  }
163598
163651
 
163599
- // src/commands/history.ts
163600
- init_src4();
163601
- 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
163602
163669
  init_src2();
163603
- init_dist28();
163604
- init_dist7();
163605
- import { createRequire as createRequire3 } from "module";
163606
163670
 
163607
163671
  // src/config.ts
163608
163672
  var CLI_CONFIG = {
163609
163673
  binaryName: "spur",
163610
163674
  binaryLabel: "spur",
163611
- binaryVersion: "0.4.23",
163675
+ binaryVersion: "0.4.24",
163612
163676
  configDir: ".spur",
163613
163677
  configFile: ".spur/config.yaml",
163614
163678
  databaseFile: ".spur/spur.db"
163615
163679
  };
163616
163680
 
163617
- // src/commands/history/anatomy.ts
163618
- function spurEntryPath(argv = process.argv, execPath = process.execPath) {
163619
- const entry = argv[1];
163620
- return entry !== undefined && /\.(?:[cm]?[jt]s)$/.test(entry) ? entry : execPath;
163621
- }
163622
- var OPS = {
163623
- digest: "<artifact.json> \u2014 semantic artifact digest",
163624
- check: "<report.md> \u2014 report-contract structure gate (PASS/FAIL)",
163625
- paths: "--out <env> [--skill-dir d] ... \u2014 resolve the run paths into an env file",
163626
- "assert-clean": "--baseline <porcelain> [--expect <path>]... \u2014 fail on undeclared writes",
163627
- probe: "--artifact a --target t ... \u2014 cache disposition + provenance",
163628
- stamp: "--candidate c --provenance p --out o \u2014 stamp provenance frontmatter",
163629
- refresh: "--report r --out o \u2014 refresh a cache-hit report",
163630
- publish: "<candidate.md> <target.md> \u2014 atomic publish"
163631
- };
163632
- function registerHistoryAnatomyCommand(history, context) {
163633
- const run = async (argv) => {
163634
- for (let i = 0;i < argv.length; i++) {
163635
- if (argv[i] === "--run-id" && !acceptArtifactId(argv[i + 1] ?? "", "run id", context))
163636
- return;
163637
- }
163638
- await Promise.resolve().then(() => init_history_anatomy_cli());
163639
- const result = runCacheCli(argv, {
163640
- helper: spurEntryPath(),
163641
- cwd: context.cwd,
163642
- ...context.env.HOME !== undefined ? { home: context.env.HOME } : {}
163643
- });
163644
- writeStepText(context.output, "stdout", result.stdout);
163645
- writeStepText(context.output, "stderr", result.stderr);
163646
- context.setExitCode(result.exitCode);
163647
- };
163648
- for (const [op, synopsis] of Object.entries(OPS)) {
163649
- history.command(`anatomy-${op}`).summary(`history-anatomy cache op: ${synopsis}`).description(`history-anatomy cache op (ADR-079): ${op} ${synopsis}
163650
- 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]));
163651
- }
163652
- }
163653
-
163654
- // src/commands/history/summary.ts
163655
- function registerHistorySummaryCommand(history, context) {
163656
- history.command("summary").summary("Daily summary report: tokens, git activity, history health, annotations").description([
163657
- "Generate the day\u2019s Markdown summary and write it to docs/daily/summary_<YYYYMMDD>.md",
163658
- "(or --output). Prompts for learnings / issues / pending unless SP_DAILY_SUMMARY_NO_PROMPT=1",
163659
- "(non-TTY stdin supplies the three answers as lines). Exit: 0 written, 1 generation failed."
163660
- ].join(`
163661
- `)).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) => {
163662
- await Promise.resolve().then(() => init_daily_summary());
163663
- const argv = [];
163664
- if (options.date !== undefined)
163665
- argv.push("--date", options.date);
163666
- if (options.dryRun === true)
163667
- argv.push("--dry-run");
163668
- if (options.output !== undefined)
163669
- argv.push("--output", options.output);
163670
- if (options.git === false)
163671
- argv.push("--no-git");
163672
- if (options.ccusage === false)
163673
- argv.push("--no-ccusage");
163674
- const code = await main(argv, {
163675
- out: (line) => context.output.write(line),
163676
- err: (line) => context.output.error(line)
163677
- });
163678
- context.setExitCode(code);
163679
- });
163680
- }
163681
-
163682
- // src/commands/history/timeline.ts
163683
- function registerHistoryTimelineCommand(history, context) {
163684
- history.command("timeline").summary("Per-prompt work/wait time and tokens from the host transcript (JSON)").description([
163685
- "Read the active host transcript read-only (CLAUDE_CODE_SESSION_ID under ~/.claude/projects,",
163686
- "PI_SESSION_FILE, or --transcript) and print one JSON timeline: a segment per operator prompt,",
163687
- 'optional --group stages, and totals. No transcript \u2192 {"available":false,...}, exit 0.',
163688
- "Exit: 0 timeline (or unavailable), 2 invalid --group or unreadable transcript."
163689
- ].join(`
163690
- `)).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) => {
163691
- await Promise.resolve().then(() => init_session_timeline());
163692
- const argv = [];
163693
- if (options.transcript !== undefined)
163694
- argv.push("--transcript", options.transcript);
163695
- if (options.group !== undefined)
163696
- argv.push("--group", options.group);
163697
- const code = runSessionTimelineCli(argv, {
163698
- env: context.env,
163699
- write: (s) => writeStepText(context.output, "stdout", s),
163700
- writeErr: (s) => writeStepText(context.output, "stderr", s)
163701
- });
163702
- context.setExitCode(code);
163703
- });
163704
- }
163705
-
163706
- // src/commands/history.ts
163681
+ // src/commands/history/shared.ts
163682
+ init_src4();
163683
+ init_dist7();
163684
+ import { createRequire as createRequire3 } from "module";
163707
163685
  function resolveCliSourceTimeout(explicit, env) {
163708
163686
  if (explicit !== undefined)
163709
163687
  return parseTimeoutInput(explicit);
@@ -163722,10 +163700,6 @@ async function resolveImportProvenance(fs) {
163722
163700
  } catch {}
163723
163701
  return { binary, importer };
163724
163702
  }
163725
- function formatProvenance(provenance) {
163726
- return `binary: ${provenance.binary}
163727
- importer: @gobing-ai/ts-llm-jsonl-importer@${provenance.importer}`;
163728
- }
163729
163703
  var CAPABILITY_KINDS2 = new Set(["command", "subagent", "skill"]);
163730
163704
  function parseCapabilityOriginSpec(spec) {
163731
163705
  const parts = spec.split(":").map((part) => part.trim());
@@ -163749,132 +163723,42 @@ function parseCapabilityOriginSpec(spec) {
163749
163723
  originIdentity
163750
163724
  };
163751
163725
  }
163752
- function registerHistoryCommand(program, context) {
163753
- const noun = program.command("history").summary("import and analyze coding-agent history");
163754
- const makeService = async (capabilityOrigins) => {
163755
- let taskLocator;
163756
- try {
163757
- const { foldersConfig } = await resolvePlanningFolders(context.fs);
163758
- taskLocator = TaskLocator.forDirs(context.fs, [
163759
- context.fs.resolve(foldersConfig.active_folder),
163760
- ...Object.keys(foldersConfig.folders ?? {}).map((f) => context.fs.resolve(f))
163761
- ]);
163762
- } catch {
163763
- taskLocator = undefined;
163764
- }
163765
- const provenance = await resolveImportProvenance(context.fs);
163766
- return new HistoryService({
163767
- getDb: () => context.getDb(),
163768
- agentConfig: context.agentConfig,
163769
- ...taskLocator ? { taskLocator } : {},
163770
- importerVersion: provenance.importer,
163771
- ...capabilityOrigins ? { capabilityOrigins } : {},
163772
- fs: context.fs,
163773
- cwd: context.cwd
163774
- });
163775
- };
163776
- 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) => {
163777
- const source = options.source ?? "all";
163778
- if (options.file && source === "all") {
163779
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: '--file requires a single --source, not "all".' }, {
163780
- enveloped: options.jsonEnvelope,
163781
- error: {
163782
- code: "INTERNAL_ERROR",
163783
- message: '--file requires a single --source, not "all".',
163784
- details: { cliCode: "usage" }
163785
- }
163786
- }) : 'spur history import: --file requires a single --source, not "all".');
163787
- context.setExitCode(1);
163788
- return;
163789
- }
163790
- let sourceTimeout;
163791
- try {
163792
- sourceTimeout = resolveCliSourceTimeout(options.sourceTimeout, getEnvVars());
163793
- } catch (e) {
163794
- const timeoutMsg = `spur history import: ${e instanceof Error ? e.message : String(e)}`;
163795
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: timeoutMsg }, {
163796
- enveloped: options.jsonEnvelope,
163797
- error: { code: "INTERNAL_ERROR", message: timeoutMsg, details: { cliCode: "usage" } }
163798
- }) : timeoutMsg);
163799
- context.setExitCode(1);
163800
- return;
163801
- }
163802
- const mode = options.mode ?? (options.file ? "force-file" : "incremental");
163803
- if (mode !== "full" && mode !== "incremental" && mode !== "force-file") {
163804
- const modeMsg = `Invalid history import mode "${mode}". Expected one of: full, incremental, force-file`;
163805
- if (options.json) {
163806
- context.output.write(toEnvelopeJson({ status: "error", message: modeMsg }, {
163807
- enveloped: options.jsonEnvelope,
163808
- error: { code: "INTERNAL_ERROR", message: modeMsg, details: { cliCode: "usage" } }
163809
- }));
163810
- } else {
163811
- context.output.error(modeMsg);
163812
- }
163813
- context.setExitCode(1);
163814
- return;
163815
- }
163816
- if (options.file && mode === "full" && options.dryRun !== true) {
163817
- 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.";
163818
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: unsafeMsg }, {
163819
- enveloped: options.jsonEnvelope,
163820
- error: { code: "INTERNAL_ERROR", message: unsafeMsg, details: { cliCode: "usage" } }
163821
- }) : unsafeMsg);
163822
- context.setExitCode(1);
163823
- return;
163824
- }
163825
- let capabilityOrigins;
163826
- if (options.capabilityOrigin.length > 0) {
163827
- try {
163828
- capabilityOrigins = options.capabilityOrigin.map(parseCapabilityOriginSpec);
163829
- } catch (err) {
163830
- const message = err instanceof Error ? err.message : String(err);
163831
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message }, {
163832
- enveloped: options.jsonEnvelope,
163833
- error: { code: "INTERNAL_ERROR", message, details: { cliCode: "usage" } }
163834
- }) : `spur history import: ${message}`);
163835
- context.setExitCode(1);
163836
- return;
163837
- }
163838
- }
163839
- const svc = await makeService(capabilityOrigins);
163840
- let fanOut;
163841
- try {
163842
- fanOut = await svc.importAll({
163843
- sources: source === "all" ? undefined : [source],
163844
- file: options.file || undefined,
163845
- root: options.root || undefined,
163846
- mode,
163847
- dryRun: options.dryRun === true,
163848
- sourceTimeout,
163849
- onScope: (line) => context.output.error(line)
163850
- });
163851
- } catch (e) {
163852
- if (e instanceof UnsafeHistoryImporterError) {
163853
- const msg = `spur history import: ${e.code}: ${e.message}`;
163854
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: msg }, {
163855
- enveloped: options.jsonEnvelope,
163856
- error: {
163857
- code: "INTERNAL_ERROR",
163858
- message: msg,
163859
- details: {
163860
- cliCode: e.code,
163861
- installedVersion: e.installedVersion,
163862
- minSafeVersion: e.minSafeVersion
163863
- }
163864
- }
163865
- }) : msg);
163866
- context.setExitCode(1);
163867
- return;
163868
- }
163869
- throw e;
163870
- }
163871
- const provenance = await resolveImportProvenance(context.fs);
163872
- context.output.write(options.json ? toEnvelopeJson({ ...fanOut, provenance }, { enveloped: options.jsonEnvelope }) : `${formatProvenance(provenance)}
163873
- ${formatFanOutResult(fanOut)}`);
163874
- 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
163875
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) {
163876
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) => {
163877
- const svc = await makeService();
163761
+ const svc = await makeHistoryService(context);
163878
163762
  const source = options.source ?? "all";
163879
163763
  const selector = {
163880
163764
  since: options.since || null,
@@ -163900,64 +163784,50 @@ ${formatFanOutResult(fanOut)}`);
163900
163784
  context.setExitCode(2);
163901
163785
  }
163902
163786
  });
163903
- 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) => {
163904
- if (!options.yes) {
163905
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message: "Refusing to wipe history tables without --yes." }, {
163906
- enveloped: options.jsonEnvelope,
163907
- error: {
163908
- code: "INTERNAL_ERROR",
163909
- message: "Refusing to wipe history tables without --yes.",
163910
- details: { cliCode: "usage" }
163911
- }
163912
- }) : "spur history reset: refusing to wipe history tables without --yes.");
163913
- context.setExitCode(1);
163914
- return;
163915
- }
163916
- const svc = await makeService();
163917
- const result = await svc.resetHistory();
163918
- const note = result.unknown.length > 0 ? ` WARNING: ${result.unknown.length} unlisted history_* table(s) left intact: ${result.unknown.join(", ")}` : "";
163919
- context.output.write(options.json ? toEnvelopeJson({
163920
- cleared: result.cleared,
163921
- skipped: result.skipped,
163922
- unknown: result.unknown,
163923
- clearedCount: result.cleared.length
163924
- }, { enveloped: options.jsonEnvelope }) : `cleared ${result.cleared.length} history tables` + (result.skipped.length > 0 ? ` (${result.skipped.length} not present)` : "") + note);
163925
- });
163926
- 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) => {
163927
- try {
163928
- const top = parseTopOrThrow(options.top);
163929
- const { report, artifactPath, resolution, artifact, banner } = runHistoryReport({
163930
- path: pathArg,
163931
- cwd: context.cwd,
163932
- now: new Date,
163933
- mode: options.mode,
163934
- task: options.task,
163935
- top
163936
- });
163937
- if (options.json) {
163938
- context.output.write(toEnvelopeJson(artifact, { enveloped: options.jsonEnvelope }));
163939
- 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))
163940
163808
  return;
163941
- }
163942
- if (resolution === "pointer") {
163943
- const staleBanner = stalenessBanner(artifact.generatedAt, new Date);
163944
- if (staleBanner !== null)
163945
- context.output.write(staleBanner);
163946
- }
163947
- if (banner !== null)
163948
- context.output.write(`${banner}
163949
- `);
163950
- context.output.write(report);
163951
- context.output.write(`
163952
- (artifact: ${artifactPath})
163953
- `);
163954
- context.setExitCode(0);
163955
- } catch (e) {
163956
- const message = `spur history report failed: ${e.message}`;
163957
- context.output.write(options.json ? toEnvelopeJson({ status: "error", message }, { enveloped: options.jsonEnvelope, error: { code: "INTERNAL_ERROR", message } }) : message);
163958
- context.setExitCode(1);
163959
163809
  }
163960
- });
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) {
163961
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) => {
163962
163832
  let sourceTimeout;
163963
163833
  try {
@@ -163971,7 +163841,7 @@ ${formatFanOutResult(fanOut)}`);
163971
163841
  context.setExitCode(1);
163972
163842
  return;
163973
163843
  }
163974
- const svc = await makeService();
163844
+ const svc = await makeHistoryService(context);
163975
163845
  let refresh;
163976
163846
  try {
163977
163847
  refresh = parseHistoryRefreshContext(getEnvVar(HISTORY_REFRESH_CONTEXT_ENV));
@@ -164093,35 +163963,6 @@ ${formatFanOutResult(fanOut)}`);
164093
163963
  }
164094
163964
  context.output.write(options.json ? toEnvelopeJson(result, { enveloped: options.jsonEnvelope }) : formatDailyResult(result));
164095
163965
  });
164096
- registerHistoryAnatomyCommand(noun, context);
164097
- registerHistoryTimelineCommand(noun, context);
164098
- registerHistorySummaryCommand(noun, context);
164099
- }
164100
- function parseTopOrThrow(raw) {
164101
- if (raw === undefined)
164102
- return;
164103
- const n = Number(raw);
164104
- if (!Number.isInteger(n) || n < 1) {
164105
- throw new Error(`--top must be a positive integer (got "${raw}")`);
164106
- }
164107
- return n;
164108
- }
164109
- function formatFanOutResult(r) {
164110
- const lines = ["history import (fan-out)", "sources:"];
164111
- for (const e of r.entries) {
164112
- lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
164113
- }
164114
- const a = r.attribution;
164115
- lines.push(`attribution: sessions=${a.sessionsEvaluated} links-created=${a.linksCreated} ` + `links-present=${a.linksAlreadyPresent} skipped=${a.skippedEvidence} ambiguous=${a.ambiguousEvidence}`);
164116
- if (r.warnings.length > 0) {
164117
- lines.push("warnings:");
164118
- for (const w of r.warnings) {
164119
- lines.push(` [${w.code}] ${w.source ?? "-"}: ${w.detail}`);
164120
- }
164121
- }
164122
- lines.push(`exit_code: ${r.exitCode}`);
164123
- return lines.join(`
164124
- `);
164125
163966
  }
164126
163967
  function formatDailyResult(r) {
164127
163968
  const lines = ["history daily"];
@@ -164148,6 +163989,266 @@ function formatDailyResult(r) {
164148
163989
  `);
164149
163990
  }
164150
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
+
164151
164252
  // src/commands/init.ts
164152
164253
  init_src4();
164153
164254
  init_src();
@@ -164528,13 +164629,13 @@ ${entry}
164528
164629
  }
164529
164630
  function describeZgReadiness(status) {
164530
164631
  if (!status.zg.available)
164531
- return `zg not installed (optional) \u2014 install: bun add -g @zvec/zvec-grep`;
164632
+ return `zg not installed (optional) \u2014 install: ${ZG_INSTALL_HINT}`;
164532
164633
  if (status.zg.index === "ready")
164533
164634
  return "zg ready";
164534
164635
  if (status.zg.index === "stale")
164535
- return "zg index stale \u2014 run: zg index";
164636
+ return `zg index stale \u2014 run: ${ZG_REFRESH_HINT}`;
164536
164637
  if (status.zg.index === "missing")
164537
- return "zg index missing \u2014 run: zg index --embedding local/potion-code-16m-v2";
164638
+ return `zg index missing \u2014 run: ${ZG_INDEX_HINT}`;
164538
164639
  return "zg status unknown";
164539
164640
  }
164540
164641
 
@@ -171134,7 +171235,7 @@ init_dist5();
171134
171235
  import { homedir as homedir17 } from "os";
171135
171236
  import { basename as basename21, join as join94, relative as relative19, resolve as resolve37 } from "path";
171136
171237
  // ../server/package.json
171137
- var version4 = "0.4.23";
171238
+ var version4 = "0.4.24";
171138
171239
 
171139
171240
  // ../server/src/modules/health/index.ts
171140
171241
  var startedAt = Date.now();