@gobing-ai/spur 0.2.12 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/spur.js CHANGED
@@ -49160,8 +49160,9 @@ class AgentService {
49160
49160
  const doctorRunner = deps?.doctorRunner ?? new DoctorRunner({ agentDetector: detector, runner, env: this.ctx.env });
49161
49161
  return this.resolveAgent(undefined, flags, doctorRunner);
49162
49162
  }
49163
- async list(opts) {
49164
- const agents = await new AgentDetector().detectAll();
49163
+ async list(opts, deps) {
49164
+ const detector = deps?.detector ?? new AgentDetector;
49165
+ const agents = await detector.detectAll();
49165
49166
  if (opts.json) {
49166
49167
  this.ctx.output.write(toJson({ agents }));
49167
49168
  } else {
@@ -49205,7 +49206,13 @@ class AgentService {
49205
49206
  }
49206
49207
  const result = outcome.result;
49207
49208
  const exitCode = result.exitCode === 0 ? 0 : 3;
49208
- return { exitCode, answer: result.stdout };
49209
+ return {
49210
+ exitCode,
49211
+ answer: result.stdout,
49212
+ durationMs: result.durationMs,
49213
+ ...result.signal !== undefined ? { signal: result.signal } : {},
49214
+ stderr: result.stderr
49215
+ };
49209
49216
  }
49210
49217
  async executeRun(prompt, flags, deps, silent) {
49211
49218
  const mode = stringFlag(flags, "mode", "text");
@@ -57319,6 +57326,16 @@ var init_feature_check = __esm(() => {
57319
57326
  });
57320
57327
 
57321
57328
  // ../../packages/app/src/services/feature-service.ts
57329
+ function stripLeadingSectionHeader(body, sectionName) {
57330
+ const match = body.match(/^(\s*)#{1,6}\s+(.+?)\s*(?:\n|$)/);
57331
+ if (match === null)
57332
+ return body;
57333
+ const headingText = (match[2] ?? "").trim();
57334
+ if (headingText.toLowerCase() !== sectionName.trim().toLowerCase())
57335
+ return body;
57336
+ return body.slice(match[0].length).replace(/^\n+/, "");
57337
+ }
57338
+
57322
57339
  class FeatureService {
57323
57340
  ctx;
57324
57341
  constructor(ctx) {
@@ -57350,6 +57367,17 @@ class FeatureService {
57350
57367
  const ref = await this.refFor(id);
57351
57368
  return this.ctx.writeService.transition(ref, toStatus, actor ?? this.ctx.actor ?? "system");
57352
57369
  }
57370
+ async updateSection(id, sectionName, sourceFile) {
57371
+ const ref = await this.refFor(id);
57372
+ const current = await this.ctx.fs.readFile(ref.filePath);
57373
+ const doc2 = MarkdownDocument.parse(current, "feature");
57374
+ if (!doc2.sectionNames.includes(sectionName)) {
57375
+ throw new Error(`Feature ${id} does not contain section "${sectionName}". Available sections: ${doc2.sectionNames.join(", ")}`);
57376
+ }
57377
+ const raw = await this.ctx.fs.readFile(sourceFile);
57378
+ const body = stripLeadingSectionHeader(raw, sectionName);
57379
+ return this.ctx.writeService.updateSection(ref, sectionName, body);
57380
+ }
57353
57381
  async list() {
57354
57382
  const results = [];
57355
57383
  try {
@@ -58655,14 +58683,14 @@ var init_task_check = __esm(() => {
58655
58683
  const variant = fm.template ?? DEFAULT_TASK_VARIANT;
58656
58684
  const entry = this.resolveMatrixEntry(variant, status);
58657
58685
  this.runL2(doc2, entry, findings);
58658
- this.runL3(doc2, entry, findings);
58686
+ this.runL3(doc2, entry, status, findings);
58659
58687
  const tasksDir = dirname7(filePath);
58660
58688
  const featuresDir = join8(dirname7(tasksDir), "features");
58661
58689
  await this.runL4(doc2, fm, findings, featuresDir, tasksDir);
58662
58690
  await this.runL4Rollup(doc2, wbs, status, findings, tasksDir);
58663
58691
  return { wbs, ...this.summarizeWithStatus(status, findings, strict) };
58664
58692
  }
58665
- runL3(doc2, entry, findings) {
58693
+ runL3(doc2, entry, status, findings) {
58666
58694
  const reqBody = doc2.getSection("Requirements");
58667
58695
  if (reqBody !== null && !isPlaceholderBody(reqBody)) {
58668
58696
  const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
@@ -58735,6 +58763,18 @@ var init_task_check = __esm(() => {
58735
58763
  });
58736
58764
  }
58737
58765
  }
58766
+ if (status === "done" || status === "cancelled") {
58767
+ const fullBody = doc2.bodyWithoutFrontmatter;
58768
+ const openBoxes = (fullBody.match(/^\s*[-*]\s\[ \]\s/gm) ?? []).length;
58769
+ if (openBoxes > 0) {
58770
+ findings.push({
58771
+ layer: "L3",
58772
+ severity: "warning",
58773
+ section: "",
58774
+ message: `Task is ${status} but carries ${openBoxes} unchecked checklist box(es) \u2014 flip to [x] or remove before closing`
58775
+ });
58776
+ }
58777
+ }
58738
58778
  }
58739
58779
  async runL4(doc2, fm, findings, featuresDir, tasksDir) {
58740
58780
  const featureId2 = fm.feature_id ?? fm["feature-id"];
@@ -58937,8 +58977,9 @@ function parseVerdict(raw, fallbackWbs) {
58937
58977
  const wbs = typeof obj.wbs === "string" ? obj.wbs : fallbackWbs ?? "";
58938
58978
  const verdict = normalizeVerdict(obj.verdict);
58939
58979
  const requirements = normalizeRequirements(obj.requirements);
58980
+ const acceptanceCriteria = normalizeAcceptanceCriteria(obj.acceptanceCriteria);
58940
58981
  const checks4 = normalizeChecks(obj.checks);
58941
- return { wbs, verdict, requirements, checks: checks4 };
58982
+ return { wbs, verdict, requirements, acceptanceCriteria, checks: checks4 };
58942
58983
  }
58943
58984
  } catch {}
58944
58985
  return { wbs: fallbackWbs ?? "", verdict: "UNKNOWN", requirements: [], checks: [] };
@@ -58973,6 +59014,16 @@ function normalizeRequirements(raw) {
58973
59014
  evidence: typeof r.evidence === "string" ? r.evidence : ""
58974
59015
  }));
58975
59016
  }
59017
+ function normalizeAcceptanceCriteria(raw) {
59018
+ if (!Array.isArray(raw))
59019
+ return [];
59020
+ return raw.filter((ac) => ac !== null && typeof ac === "object").map((ac) => ({
59021
+ id: typeof ac.id === "string" ? ac.id : "",
59022
+ status: typeof ac.status === "string" ? ac.status : "",
59023
+ evidenceType: typeof ac.evidenceType === "string" ? ac.evidenceType : "",
59024
+ evidence: typeof ac.evidence === "string" ? ac.evidence : ""
59025
+ }));
59026
+ }
58976
59027
  function normalizeChecks(raw) {
58977
59028
  if (!Array.isArray(raw))
58978
59029
  return [];
@@ -59000,6 +59051,15 @@ function renderTesting(v) {
59000
59051
  lines.push(`| ${req.id} | ${req.status} | ${evidence} |`);
59001
59052
  }
59002
59053
  }
59054
+ if ((v.acceptanceCriteria ?? []).length > 0) {
59055
+ lines.push("");
59056
+ lines.push("| Acceptance Criteria | Status | Evidence Type | Evidence |");
59057
+ lines.push("|---------------------|--------|---------------|----------|");
59058
+ for (const ac of v.acceptanceCriteria ?? []) {
59059
+ const evidence = ac.evidence.replace(/\n/g, " ");
59060
+ lines.push(`| ${ac.id} | ${ac.status} | ${ac.evidenceType} | ${evidence} |`);
59061
+ }
59062
+ }
59003
59063
  lines.push("- Coverage: N/A (verdict-based; verify pipeline does not measure code coverage)");
59004
59064
  return lines.join(`
59005
59065
  `);
@@ -59140,7 +59200,7 @@ function bulletizeRequirements(raw) {
59140
59200
  return parts.map((p) => `- ${p}`).join(`
59141
59201
  `);
59142
59202
  }
59143
- function stripLeadingSectionHeader(body, sectionName) {
59203
+ function stripLeadingSectionHeader2(body, sectionName) {
59144
59204
  const match = body.match(/^(\s*)#{1,6}\s+(.+?)\s*(?:\n|$)/);
59145
59205
  if (match === null)
59146
59206
  return body;
@@ -59279,7 +59339,7 @@ class TaskService {
59279
59339
  async updateSection(wbs, sectionName, sourceFile) {
59280
59340
  const filePath = await this.resolveTaskFile(wbs);
59281
59341
  const raw = await this.ctx.fs.readFile(sourceFile);
59282
- const body = stripLeadingSectionHeader(raw, sectionName);
59342
+ const body = stripLeadingSectionHeader2(raw, sectionName);
59283
59343
  const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
59284
59344
  return this.writeService.updateSection(ref, sectionName, body);
59285
59345
  }
@@ -59347,7 +59407,49 @@ ${issues}`);
59347
59407
  }
59348
59408
  throw err;
59349
59409
  }
59350
- return writeResults;
59410
+ const parentsWired = await this.wireUpParents(this.collectDistinctParents(items));
59411
+ return { children: writeResults, parentsWired };
59412
+ }
59413
+ collectDistinctParents(items) {
59414
+ const seen = new Set;
59415
+ const ordered = [];
59416
+ for (const item of items) {
59417
+ const wbs = item.parent_wbs;
59418
+ if (typeof wbs !== "string" || wbs === "")
59419
+ continue;
59420
+ if (seen.has(wbs))
59421
+ continue;
59422
+ seen.add(wbs);
59423
+ ordered.push(wbs);
59424
+ }
59425
+ return ordered;
59426
+ }
59427
+ async wireUpParents(parentWbsList) {
59428
+ const out = [];
59429
+ for (const wbs of parentWbsList) {
59430
+ const entry = { wbs, rostered: false, transitionedTo: null, errors: [] };
59431
+ try {
59432
+ const roster = await this.refreshRoster(wbs);
59433
+ entry.rostered = roster.written;
59434
+ } catch (err) {
59435
+ entry.errors.push(`refreshRoster: ${err instanceof Error ? err.message : String(err)}`);
59436
+ }
59437
+ try {
59438
+ const current = await this.show(wbs);
59439
+ if (current.status === "todo") {
59440
+ const filePath = await this.resolveTaskFile(wbs);
59441
+ const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
59442
+ const wr = await this.writeService.transition(ref, "wip", "system");
59443
+ entry.transitionedTo = wr.toStatus ?? "wip";
59444
+ } else {
59445
+ entry.transitionedTo = null;
59446
+ }
59447
+ } catch (err) {
59448
+ entry.errors.push(`transition: ${err instanceof Error ? err.message : String(err)}`);
59449
+ }
59450
+ out.push(entry);
59451
+ }
59452
+ return out;
59351
59453
  }
59352
59454
  async createBatchItem(item) {
59353
59455
  const folder = this.ctx.tasksDir;
@@ -59383,6 +59485,14 @@ ${issues}`);
59383
59485
  if (item.priority !== undefined && item.priority !== null) {
59384
59486
  content2 = patchFrontmatterField(content2, "priority", item.priority);
59385
59487
  }
59488
+ if (item.tags !== undefined && item.tags.length > 0) {
59489
+ content2 = patchFrontmatterField(content2, "tags", `[${item.tags.map((tag) => JSON.stringify(tag)).join(", ")}]`);
59490
+ }
59491
+ if ((item.requirements ?? "").trim() !== "") {
59492
+ const doc2 = MarkdownDocument.parse(content2, "task");
59493
+ doc2.replaceSection("Requirements", bulletizeRequirements(item.requirements ?? ""));
59494
+ content2 = doc2.serialize();
59495
+ }
59386
59496
  const ref2 = { kind: "task", id: wbs, filePath, folder };
59387
59497
  return { ref: ref2, content: content2 };
59388
59498
  }
@@ -59717,22 +59827,25 @@ var init_task_service = __esm(() => {
59717
59827
  // ../../packages/app/src/services/task-verdict.ts
59718
59828
  function deriveVerdict(answerText, taskCheckPassed) {
59719
59829
  const requirements = extractRequirements(answerText);
59720
- const checks4 = extractChecks(answerText, taskCheckPassed);
59830
+ const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(extractAcceptanceCriteria(answerText));
59831
+ const checks4 = extractChecks(answerText, taskCheckPassed, acceptanceCriteria);
59721
59832
  if (requirements.length === 0) {
59722
- return { verdict: "UNKNOWN", requirements, checks: checks4 };
59833
+ return { verdict: "UNKNOWN", requirements, acceptanceCriteria, checks: checks4 };
59723
59834
  }
59724
59835
  const hasUnmet = requirements.some((r) => r.status === "UNMET");
59725
- if (hasUnmet) {
59726
- return { verdict: "FAIL", requirements, checks: checks4 };
59836
+ const hasUnmetAc = acceptanceCriteria.some((ac) => ac.status === "UNMET");
59837
+ if (hasUnmet || hasUnmetAc) {
59838
+ return { verdict: "FAIL", requirements, acceptanceCriteria, checks: checks4 };
59727
59839
  }
59728
59840
  const hasPartial = requirements.some((r) => r.status === "PARTIAL");
59729
- if (hasPartial) {
59730
- return { verdict: "PARTIAL", requirements, checks: checks4 };
59841
+ const hasPartialAc = acceptanceCriteria.some((ac) => ac.status === "PARTIAL");
59842
+ if (hasPartial || hasPartialAc) {
59843
+ return { verdict: "PARTIAL", requirements, acceptanceCriteria, checks: checks4 };
59731
59844
  }
59732
59845
  if (!taskCheckPassed) {
59733
- return { verdict: "PARTIAL", requirements, checks: checks4 };
59846
+ return { verdict: "PARTIAL", requirements, acceptanceCriteria, checks: checks4 };
59734
59847
  }
59735
- return { verdict: "PASS", requirements, checks: checks4 };
59848
+ return { verdict: "PASS", requirements, acceptanceCriteria, checks: checks4 };
59736
59849
  }
59737
59850
  function extractRequirements(text4) {
59738
59851
  const reqs = [];
@@ -59755,6 +59868,12 @@ function extractRequirements(text4) {
59755
59868
  if (/^[-:]+$/.test(cells[0] ?? ""))
59756
59869
  continue;
59757
59870
  if (inTable && cells.length >= 2) {
59871
+ const first = (cells[0] ?? "").toLowerCase();
59872
+ const second = (cells[1] ?? "").toLowerCase();
59873
+ if ((first === "ac" || first.includes("acceptance") || first === "check" || first === "name") && (second.includes("status") || second === "verdict")) {
59874
+ inTable = false;
59875
+ continue;
59876
+ }
59758
59877
  const id = cells[0] ?? "";
59759
59878
  const statusRaw = (cells[1] ?? "").toUpperCase();
59760
59879
  const evidence = cells.length >= 3 ? cells[2] ?? "" : "";
@@ -59775,7 +59894,82 @@ function normalizeStatus(raw) {
59775
59894
  return "UNMET";
59776
59895
  return null;
59777
59896
  }
59778
- function extractChecks(_text, taskCheckPassed) {
59897
+ function extractAcceptanceCriteria(text4) {
59898
+ const rows = [];
59899
+ const lines = text4.split(`
59900
+ `);
59901
+ let inTable = false;
59902
+ for (const line of lines) {
59903
+ const trimmed = line.trim();
59904
+ if (!trimmed.startsWith("|"))
59905
+ continue;
59906
+ const cells = trimmed.split("|").map((c3) => c3.trim()).filter(Boolean);
59907
+ if (!inTable && cells.length >= 4) {
59908
+ const h0 = (cells[0] ?? "").toLowerCase();
59909
+ const h1 = (cells[1] ?? "").toLowerCase();
59910
+ const h2 = (cells[2] ?? "").toLowerCase();
59911
+ if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status") && h2.includes("evidence")) {
59912
+ inTable = true;
59913
+ continue;
59914
+ }
59915
+ }
59916
+ if (/^[-:]+$/.test(cells[0] ?? ""))
59917
+ continue;
59918
+ if (inTable && cells.length >= 4) {
59919
+ const id = cells[0] ?? "";
59920
+ const status = normalizeAcceptanceCriteriaStatus(cells[1] ?? "");
59921
+ const evidenceType = normalizeEvidenceType(cells[2] ?? "");
59922
+ const evidence = cells[3] ?? "";
59923
+ if (status !== null && evidenceType !== null && id.length > 0) {
59924
+ rows.push({ id, status, evidenceType, evidence });
59925
+ }
59926
+ }
59927
+ }
59928
+ return rows;
59929
+ }
59930
+ function normalizeAcceptanceCriteriaStatus(raw) {
59931
+ const upper = raw.toUpperCase();
59932
+ if (/\bMET\b/.test(upper))
59933
+ return "MET";
59934
+ if (/\bPARTIAL\b/.test(upper))
59935
+ return "PARTIAL";
59936
+ if (/\bUNMET\b/.test(upper))
59937
+ return "UNMET";
59938
+ if (/\bN\/A\b/.test(upper) || /\bNA\b/.test(upper))
59939
+ return "N/A";
59940
+ return null;
59941
+ }
59942
+ function normalizeEvidenceType(raw) {
59943
+ const normalized = raw.toLowerCase().trim();
59944
+ if (normalized === "test")
59945
+ return "test";
59946
+ if (normalized === "command")
59947
+ return "command";
59948
+ if (normalized === "static-ref" || normalized === "static")
59949
+ return "static-ref";
59950
+ if (normalized === "manual-review" || normalized === "manual")
59951
+ return "manual-review";
59952
+ if (normalized === "llm-judge" || normalized === "judge")
59953
+ return "llm-judge";
59954
+ if (normalized === "n/a" || normalized === "na")
59955
+ return "n/a";
59956
+ return null;
59957
+ }
59958
+ function applyAcceptanceCriteriaEvidenceRule(rows) {
59959
+ return rows.map((row) => {
59960
+ if (row.status === "MET" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command") {
59961
+ return { ...row, status: "PARTIAL" };
59962
+ }
59963
+ return row;
59964
+ });
59965
+ }
59966
+ function requiresExecutableEvidence(id) {
59967
+ const normalized = id.toLowerCase();
59968
+ const nonCore = normalized.includes("[advisory]") || normalized.includes("[non-core]");
59969
+ const nonBehavior = normalized.includes("[non-behavior]") || normalized.includes("[docs-only]") || normalized.includes("[doc-only]");
59970
+ return !nonCore && !nonBehavior;
59971
+ }
59972
+ function extractChecks(_text, taskCheckPassed, acceptanceCriteria) {
59779
59973
  const checks4 = [
59780
59974
  {
59781
59975
  name: "spur task check",
@@ -59807,6 +60001,14 @@ function extractChecks(_text, taskCheckPassed) {
59807
60001
  }
59808
60002
  }
59809
60003
  }
60004
+ if (acceptanceCriteria.length > 0) {
60005
+ const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command");
60006
+ checks4.push({
60007
+ name: downgraded.length > 0 ? "evidence-rule-failed" : "evidence-rule-pass",
60008
+ status: downgraded.length > 0 ? "fail" : "pass",
60009
+ evidence: downgraded.length > 0 ? `Executable evidence missing for: ${downgraded.map((row) => row.id).join(", ")}` : "All behavior-bearing AC rows have executable evidence or are explicitly non-behavioral."
60010
+ });
60011
+ }
59810
60012
  return checks4;
59811
60013
  }
59812
60014
 
@@ -59964,9 +60166,11 @@ var init_team_service = __esm(() => {
59964
60166
  });
59965
60167
 
59966
60168
  // ../../packages/app/src/workflow/actions/agent-run.ts
60169
+ import { execFile } from "child_process";
59967
60170
  import { existsSync as existsSync4 } from "fs";
59968
60171
  import { mkdir, writeFile } from "fs/promises";
59969
60172
  import { dirname as dirname9, isAbsolute as isAbsolute2, join as join10 } from "path";
60173
+ import { promisify as promisify3 } from "util";
59970
60174
 
59971
60175
  class AgentRunActionRunner {
59972
60176
  kind = KIND;
@@ -60015,7 +60219,8 @@ class AgentRunActionRunner {
60015
60219
  const capture = asOptionalBoolean(options.capture) || answerFile !== undefined;
60016
60220
  const agentLabel = agent ?? "<default>";
60017
60221
  if (capture) {
60018
- const { exitCode: exitCode2, answer } = await this.agentService.runCapture(input, flags);
60222
+ const captured = await this.agentService.runCapture(input, flags);
60223
+ const { exitCode: exitCode2, answer } = captured;
60019
60224
  const ok2 = exitCode2 === 0;
60020
60225
  if (answerFile !== undefined) {
60021
60226
  const target = isAbsolute2(answerFile) ? answerFile : join10(cwd, answerFile);
@@ -60032,6 +60237,9 @@ class AgentRunActionRunner {
60032
60237
  };
60033
60238
  }
60034
60239
  }
60240
+ if (!ok2) {
60241
+ await writePartialWorkArtifact(context3, agentLabel, captured, cwd);
60242
+ }
60035
60243
  return {
60036
60244
  ok: ok2,
60037
60245
  data: { exitCode: exitCode2, agent: agentLabel, answer },
@@ -60086,8 +60294,60 @@ function asOptionalNumber(value) {
60086
60294
  }
60087
60295
  return;
60088
60296
  }
60089
- var KIND = "agent.run";
60090
- var init_agent_run = () => {};
60297
+ async function writePartialWorkArtifact(context3, agentLabel, captured, cwd) {
60298
+ try {
60299
+ const exitReason = captured.signal !== undefined ? `killed by signal ${captured.signal} (likely timeout)` : `exited with code ${captured.exitCode}`;
60300
+ const diffStat = await gitDiffStat(cwd);
60301
+ const stdoutTail = tail(captured.answer, PARTIAL_ARTIFACT_TAIL_CHARS);
60302
+ const stderrTail = tail(captured.stderr ?? "", PARTIAL_ARTIFACT_TAIL_CHARS);
60303
+ const body = [
60304
+ `# Partial-work handoff \u2014 ${agentLabel}`,
60305
+ "",
60306
+ `- run: ${context3.runId}`,
60307
+ `- state: ${context3.stateOrNodeId}`,
60308
+ `- exit reason: ${exitReason}`,
60309
+ `- elapsed: ${captured.durationMs ?? "unknown"}ms`,
60310
+ "",
60311
+ "## git diff --stat",
60312
+ "```",
60313
+ diffStat || "(no diff)",
60314
+ "```",
60315
+ "",
60316
+ "## stdout tail",
60317
+ "```",
60318
+ stdoutTail || "(empty)",
60319
+ "```",
60320
+ "",
60321
+ "## stderr tail",
60322
+ "```",
60323
+ stderrTail || "(empty)",
60324
+ "```",
60325
+ ""
60326
+ ].join(`
60327
+ `);
60328
+ const target = join10(cwd, ".spur", "run", `${context3.runId}-${context3.stateOrNodeId}-partial.md`);
60329
+ await mkdir(dirname9(target), { recursive: true });
60330
+ await writeFile(target, body, "utf8");
60331
+ } catch {}
60332
+ }
60333
+ async function gitDiffStat(cwd) {
60334
+ try {
60335
+ const { stdout } = await execFileAsync("git", ["diff", "--stat"], { cwd, maxBuffer: 1024 * 1024 });
60336
+ return stdout.trim();
60337
+ } catch {
60338
+ return "";
60339
+ }
60340
+ }
60341
+ function tail(text4, maxChars) {
60342
+ if (text4.length <= maxChars)
60343
+ return text4;
60344
+ return `... (truncated) ...
60345
+ ${text4.slice(text4.length - maxChars)}`;
60346
+ }
60347
+ var execFileAsync, PARTIAL_ARTIFACT_TAIL_CHARS = 4000, KIND = "agent.run";
60348
+ var init_agent_run = __esm(() => {
60349
+ execFileAsync = promisify3(execFile);
60350
+ });
60091
60351
 
60092
60352
  // ../../packages/app/src/workflow/actions/file-exists.ts
60093
60353
  class FileExistsActionRunner {
@@ -60166,9 +60426,51 @@ var init_file_read = __esm(() => {
60166
60426
  init_dist3();
60167
60427
  });
60168
60428
 
60429
+ // ../../packages/app/src/workflow/actions/file-read-into-var.ts
60430
+ class FileReadIntoVarActionRunner {
60431
+ fileSystem;
60432
+ kind = KIND4;
60433
+ constructor(fileSystem) {
60434
+ this.fileSystem = fileSystem;
60435
+ }
60436
+ async execute(options, context3) {
60437
+ const rawPath = options.path;
60438
+ if (typeof rawPath !== "string" || rawPath === "") {
60439
+ return { ok: false, error: "file.read.into-var: path is required" };
60440
+ }
60441
+ const varName = options.var;
60442
+ if (typeof varName !== "string" || varName === "") {
60443
+ return { ok: false, error: "file.read.into-var: var is required" };
60444
+ }
60445
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
60446
+ return {
60447
+ ok: false,
60448
+ error: `file.read.into-var: var name must match /^[A-Za-z_][A-Za-z0-9_]*$/, got "${varName}"`
60449
+ };
60450
+ }
60451
+ const trim = options.trim === undefined ? true : options.trim !== false;
60452
+ const resolved = isAbsolutePath(rawPath) ? normalizeSeparators(rawPath) : joinPath(context3.workdir ?? ".", rawPath);
60453
+ const stat2 = await this.fileSystem.stat(resolved);
60454
+ if (stat2 === null) {
60455
+ return { ok: false, error: `file.read.into-var: file not found: ${resolved}` };
60456
+ }
60457
+ const raw = await this.fileSystem.readFile(resolved);
60458
+ const projected = trim ? raw.replace(/\s+$/u, "") : raw;
60459
+ return {
60460
+ ok: true,
60461
+ data: { content: projected, raw, var: varName, path: resolved, size: stat2.size },
60462
+ setVars: { [varName]: projected }
60463
+ };
60464
+ }
60465
+ }
60466
+ var KIND4 = "file.read.into-var";
60467
+ var init_file_read_into_var = __esm(() => {
60468
+ init_dist3();
60469
+ });
60470
+
60169
60471
  // ../../packages/app/src/workflow/actions/hitl-confirm.ts
60170
60472
  class HitlConfirmActionRunner {
60171
- kind = KIND4;
60473
+ kind = KIND5;
60172
60474
  responder;
60173
60475
  constructor(responder) {
60174
60476
  this.responder = responder;
@@ -60196,12 +60498,9 @@ class HitlConfirmActionRunner {
60196
60498
  node: context3.stateOrNodeId,
60197
60499
  ok: !(answer.cancelled || answer.value === "cancel")
60198
60500
  });
60199
- if (answer.cancelled || answer.value === "cancel") {
60200
- return { ok: false, error: "hitl.confirm cancelled" };
60201
- }
60202
60501
  return {
60203
60502
  ok: true,
60204
- data: { answer: answer.value },
60503
+ data: { answer: answer.value, cancelled: answer.cancelled === true || answer.value === "cancel" },
60205
60504
  setVars: { [varName]: answer.value }
60206
60505
  };
60207
60506
  }
@@ -60211,11 +60510,11 @@ function asString2(value) {
60211
60510
  return;
60212
60511
  return String(value);
60213
60512
  }
60214
- var KIND4 = "hitl.confirm";
60513
+ var KIND5 = "hitl.confirm";
60215
60514
 
60216
60515
  // ../../packages/app/src/workflow/actions/hitl-input.ts
60217
60516
  class HitlInputActionRunner {
60218
- kind = KIND5;
60517
+ kind = KIND6;
60219
60518
  responder;
60220
60519
  constructor(responder) {
60221
60520
  this.responder = responder;
@@ -60258,11 +60557,11 @@ function asString3(value) {
60258
60557
  return;
60259
60558
  return String(value);
60260
60559
  }
60261
- var KIND5 = "hitl.input";
60560
+ var KIND6 = "hitl.input";
60262
60561
 
60263
60562
  // ../../packages/app/src/workflow/actions/hitl-select.ts
60264
60563
  class HitlSelectActionRunner {
60265
- kind = KIND6;
60564
+ kind = KIND7;
60266
60565
  responder;
60267
60566
  constructor(responder) {
60268
60567
  this.responder = responder;
@@ -60315,7 +60614,7 @@ function asStringArray(value) {
60315
60614
  return;
60316
60615
  return value.map((v) => String(v));
60317
60616
  }
60318
- var KIND6 = "hitl.select";
60617
+ var KIND7 = "hitl.select";
60319
60618
 
60320
60619
  // ../../packages/app/src/workflow/actions/http-request.ts
60321
60620
  function resolveTemplate(value, vars) {
@@ -60332,7 +60631,7 @@ function redactHeaders(message) {
60332
60631
  }
60333
60632
 
60334
60633
  class HttpRequestActionRunner {
60335
- kind = KIND7;
60634
+ kind = KIND8;
60336
60635
  requester;
60337
60636
  allowlist;
60338
60637
  constructor(requester, allowlist) {
@@ -60487,7 +60786,7 @@ function asNumberArray(value) {
60487
60786
  return [];
60488
60787
  return value.filter((v) => typeof v === "number" && !Number.isNaN(v));
60489
60788
  }
60490
- var KIND7 = "http.request", ALLOWED_METHODS, DEFAULT_TIMEOUT_MS3 = 30000, MAX_TIMEOUT_MS = 120000, DEFAULT_MAX_RESPONSE_BYTES = 1048576, HEADER_NAME_RE, HEADER_VALUE_RE, PRIVATE_IP_PATTERNS;
60789
+ var KIND8 = "http.request", ALLOWED_METHODS, DEFAULT_TIMEOUT_MS3 = 30000, MAX_TIMEOUT_MS = 120000, DEFAULT_MAX_RESPONSE_BYTES = 1048576, HEADER_NAME_RE, HEADER_VALUE_RE, PRIVATE_IP_PATTERNS;
60491
60790
  var init_http_request = __esm(() => {
60492
60791
  ALLOWED_METHODS = {
60493
60792
  GET: true,
@@ -60516,7 +60815,7 @@ var init_http_request = __esm(() => {
60516
60815
 
60517
60816
  // ../../packages/app/src/workflow/actions/response-validate.ts
60518
60817
  class ResponseValidateActionRunner {
60519
- kind = KIND8;
60818
+ kind = KIND9;
60520
60819
  engine;
60521
60820
  constructor(engine2) {
60522
60821
  this.engine = engine2;
@@ -60545,7 +60844,7 @@ function asString5(value) {
60545
60844
  return;
60546
60845
  return String(value);
60547
60846
  }
60548
- var KIND8 = "response.validate";
60847
+ var KIND9 = "response.validate";
60549
60848
 
60550
60849
  // ../../packages/app/src/workflow/utils.ts
60551
60850
  var identity6 = (s) => s, NO_COLOR;
@@ -60562,7 +60861,7 @@ var init_utils3 = __esm(() => {
60562
60861
 
60563
60862
  // ../../packages/app/src/workflow/actions/rule-check.ts
60564
60863
  class RuleCheckActionRunner {
60565
- kind = KIND9;
60864
+ kind = KIND10;
60566
60865
  ruleService;
60567
60866
  constructor(ruleService) {
60568
60867
  this.ruleService = ruleService;
@@ -60597,7 +60896,7 @@ function asString6(value) {
60597
60896
  return;
60598
60897
  return String(value);
60599
60898
  }
60600
- var KIND9 = "rule.check";
60899
+ var KIND10 = "rule.check";
60601
60900
  var init_rule_check = __esm(() => {
60602
60901
  init_utils3();
60603
60902
  });
@@ -60609,6 +60908,7 @@ function registerSpurBuiltins(host, options) {
60609
60908
  host.registerAction(new RuleCheckActionRunner(options.ruleService), "builtin");
60610
60909
  host.registerAction(new FileExistsActionRunner(fileSystem), "builtin");
60611
60910
  host.registerAction(new FileReadActionRunner(fileSystem), "builtin");
60911
+ host.registerAction(new FileReadIntoVarActionRunner(fileSystem), "builtin");
60612
60912
  host.registerAction(new HitlConfirmActionRunner(options.hitlResponder), "builtin");
60613
60913
  host.registerAction(new HitlSelectActionRunner(options.hitlResponder), "builtin");
60614
60914
  host.registerAction(new HitlInputActionRunner(options.hitlResponder), "builtin");
@@ -60624,6 +60924,7 @@ var init_builtins2 = __esm(() => {
60624
60924
  init_agent_run();
60625
60925
  init_file_exists();
60626
60926
  init_file_read();
60927
+ init_file_read_into_var();
60627
60928
  init_http_request();
60628
60929
  init_rule_check();
60629
60930
  });
@@ -61239,6 +61540,7 @@ __export(exports_src2, {
61239
61540
  HitlInputActionRunner: () => HitlInputActionRunner,
61240
61541
  HitlConfirmActionRunner: () => HitlConfirmActionRunner,
61241
61542
  HistoryService: () => HistoryService,
61543
+ FileReadIntoVarActionRunner: () => FileReadIntoVarActionRunner,
61242
61544
  FileReadActionRunner: () => FileReadActionRunner,
61243
61545
  FileExistsActionRunner: () => FileExistsActionRunner,
61244
61546
  FeatureService: () => FeatureService,
@@ -61268,6 +61570,7 @@ var init_src3 = __esm(() => {
61268
61570
  init_agent_run();
61269
61571
  init_file_exists();
61270
61572
  init_file_read();
61573
+ init_file_read_into_var();
61271
61574
  init_rule_check();
61272
61575
  init_builtins2();
61273
61576
  init_lifecycle_adapter();
@@ -68788,23 +69091,115 @@ function registerFeatureCommand(program2, context3) {
68788
69091
  context3.setExitCode(1);
68789
69092
  }
68790
69093
  });
68791
- feature.command("update").summary("Update a feature status (lifecycle) or a scalar frontmatter field.").argument("<id>", "Feature ID").argument("[status]", "New lifecycle status (omit when using --field/--value)").option("--field <key>", "Frontmatter field to set (e.g. priority)").option("--value <value>", "New value for --field").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, status, options) => {
69094
+ feature.command("update").summary("Update a feature status, scalar frontmatter field, or section body.").argument("<id>", "Feature ID").argument("[status]", "New lifecycle status").option("--field <key>", "Frontmatter field to set (e.g. priority)").option("--value <value>", "New value for --field").option("--section <name>", "Section name to replace").option("--from-file <path>", "File to read section body from (requires --section)").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, status, options) => {
68792
69095
  const svc = await makeService(context3, options.folder);
68793
69096
  try {
69097
+ let result;
69098
+ if (options.section !== undefined) {
69099
+ if (options.fromFile === undefined) {
69100
+ context3.output.error("--from-file is required with --section");
69101
+ context3.setExitCode(2);
69102
+ return;
69103
+ }
69104
+ result = await svc.updateSection(id, options.section, options.fromFile);
69105
+ if (!options.json) {
69106
+ for (const warning of result.warnings ?? []) {
69107
+ context3.output.error(warning);
69108
+ }
69109
+ context3.output.write(`Updated section '${options.section}' in feature ${result.ref.id}`);
69110
+ }
69111
+ } else if (options.fromFile !== undefined) {
69112
+ context3.output.error("--section is required with --from-file");
69113
+ context3.setExitCode(2);
69114
+ return;
69115
+ }
68794
69116
  if (options.field !== undefined) {
68795
69117
  if (options.value === undefined) {
68796
69118
  context3.output.error("--value is required with --field");
68797
69119
  context3.setExitCode(2);
68798
69120
  return;
68799
69121
  }
68800
- const result = await svc.update(id, options.field, options.value);
68801
- write(context3, options.json, result, `Updated ${options.field} on feature ${result.ref.id}`);
68802
- } else if (status !== undefined) {
68803
- const result = await svc.transition(id, status);
68804
- write(context3, options.json, result, `${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
68805
- } else {
68806
- context3.output.error("Either <status> or --field/--value is required");
69122
+ result = await svc.update(id, options.field, options.value);
69123
+ if (!options.json) {
69124
+ context3.output.write(`Updated ${options.field} on feature ${result.ref.id}`);
69125
+ }
69126
+ } else if (options.value !== undefined) {
69127
+ context3.output.error("--field is required with --value");
69128
+ context3.setExitCode(2);
69129
+ return;
69130
+ }
69131
+ if (status !== undefined) {
69132
+ result = await svc.transition(id, status);
69133
+ if (!options.json) {
69134
+ context3.output.write(`${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
69135
+ }
69136
+ }
69137
+ if (result === undefined) {
69138
+ context3.output.error("Either <status>, --field/--value, or --section/--from-file is required");
68807
69139
  context3.setExitCode(2);
69140
+ return;
69141
+ }
69142
+ if (options.json) {
69143
+ context3.output.write(toJson2(result));
69144
+ }
69145
+ } catch (err) {
69146
+ context3.output.error(String(err));
69147
+ context3.setExitCode(1);
69148
+ }
69149
+ });
69150
+ feature.command("advance").summary("Walk a feature through the legal forward lifecycle path.").argument("<id>", "Feature ID to advance").option("--to <status>", "Target status (default: 'done')").option("--folder <path>", "Custom features folder").option("--json", "Output machine-readable JSON").action(async (id, options) => {
69151
+ const svc = await makeService(context3, options.folder);
69152
+ const target = options.to ?? "done";
69153
+ const forwardPath = {
69154
+ backlog: "active",
69155
+ active: "verifying",
69156
+ verifying: "done"
69157
+ };
69158
+ const history = [];
69159
+ try {
69160
+ const initial = await svc.show(id);
69161
+ if (initial === null) {
69162
+ context3.output.error(`Feature ${id} not found`);
69163
+ context3.setExitCode(1);
69164
+ return;
69165
+ }
69166
+ let current = initial.status;
69167
+ if (current === target) {
69168
+ if (options.json) {
69169
+ context3.output.write(toJson2({ id, status: current, hops: history }));
69170
+ } else {
69171
+ context3.output.write(`${id}: already at ${current}; no advance needed`);
69172
+ }
69173
+ return;
69174
+ }
69175
+ let next = forwardPath[current];
69176
+ while (next !== undefined) {
69177
+ if (current === "active") {
69178
+ await assertFeatureCheckPass(context3, id, options.folder, false);
69179
+ } else if (current === "verifying") {
69180
+ await assertFeatureCheckPass(context3, id, options.folder, true);
69181
+ }
69182
+ const result = await svc.transition(id, next);
69183
+ history.push({ from: result.fromStatus ?? current, to: result.toStatus ?? next });
69184
+ const observed = await svc.show(id);
69185
+ current = observed?.status ?? result.toStatus ?? next;
69186
+ if (current !== next) {
69187
+ throw new Error(`Feature ${id} expected status '${next}' after transition, observed '${current}'`);
69188
+ }
69189
+ if (current === target)
69190
+ break;
69191
+ next = forwardPath[current];
69192
+ }
69193
+ if (current !== target) {
69194
+ context3.output.error(`${id}: cannot reach '${target}' from '${current}' along the forward path`);
69195
+ context3.setExitCode(1);
69196
+ return;
69197
+ }
69198
+ if (options.json) {
69199
+ context3.output.write(toJson2({ id, status: current, hops: history }));
69200
+ } else {
69201
+ const trail = history.map((h2) => `${h2.from} \u2192 ${h2.to}`).join(", ");
69202
+ context3.output.write(`${id}: advanced to ${current} (${trail})`);
68808
69203
  }
68809
69204
  } catch (err) {
68810
69205
  context3.output.error(String(err));
@@ -68916,13 +69311,6 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
68916
69311
  }
68917
69312
  });
68918
69313
  }
68919
- function write(context3, json3, result, humanLine) {
68920
- if (json3) {
68921
- context3.output.write(toJson2(result));
68922
- } else {
68923
- context3.output.write(humanLine);
68924
- }
68925
- }
68926
69314
  async function makeService(context3, folderOverride) {
68927
69315
  const resolved = await resolvePlanningFolders(context3.fs);
68928
69316
  const featuresDir = folderOverride ?? context3.fs.resolve(resolved.featuresDir);
@@ -68934,6 +69322,25 @@ async function makeService(context3, folderOverride) {
68934
69322
  });
68935
69323
  return new FeatureService({ fs: context3.fs, writeService, featuresDir, tasksDir });
68936
69324
  }
69325
+ async function assertFeatureCheckPass(context3, id, folderOverride, strict) {
69326
+ const resolved = await resolvePlanningFolders(context3.fs);
69327
+ const featuresDir = folderOverride ?? context3.fs.resolve(resolved.featuresDir);
69328
+ const tasksDir = context3.fs.resolve(resolved.tasksDir);
69329
+ const entries = await context3.fs.readDir(featuresDir);
69330
+ const fileName = entries.find((name) => name.match(new RegExp(`^${id}_.+\\.md$`)));
69331
+ if (fileName === undefined) {
69332
+ throw new Error(`Feature ${id} not found`);
69333
+ }
69334
+ const result = await new FeatureCheckService(context3.fs).check(`${featuresDir}/${fileName}`, id, {
69335
+ strict,
69336
+ featuresDir,
69337
+ tasksDir
69338
+ });
69339
+ if (!result.pass) {
69340
+ const details = result.findings.map((f) => `${f.layer} ${f.section}: ${f.message}`).join("; ");
69341
+ throw new Error(`Feature ${id} check failed before advance${strict ? " (strict)" : ""}: ${details}`);
69342
+ }
69343
+ }
68937
69344
 
68938
69345
  // src/commands/history.ts
68939
69346
  init_src3();
@@ -68987,7 +69394,7 @@ import { join as join13, resolve as resolve4 } from "path";
68987
69394
  var CLI_CONFIG = {
68988
69395
  binaryName: "spur",
68989
69396
  binaryLabel: "spur",
68990
- binaryVersion: "0.2.12",
69397
+ binaryVersion: "0.3.0",
68991
69398
  configDir: ".spur",
68992
69399
  configFile: ".spur/config.yaml",
68993
69400
  databaseFile: ".spur/spur.db"
@@ -69003,6 +69410,8 @@ var SCAFFOLD_MANIFEST = [
69003
69410
  { source: "workflows/feature-dev.yaml", target: "workflows/feature-dev.yaml" },
69004
69411
  { source: "workflows/task-pipeline.yaml", target: "workflows/task-pipeline.yaml" },
69005
69412
  { source: "workflows/planning-pipeline.yaml", target: "workflows/planning-pipeline.yaml" },
69413
+ { source: "workflows/idea-pipeline.yaml", target: "workflows/idea-pipeline.yaml" },
69414
+ { source: "workflows/wrapup-pipeline.yaml", target: "workflows/wrapup-pipeline.yaml" },
69006
69415
  { source: "tasks/section-matrix.yaml", target: "tasks/section-matrix.yaml" },
69007
69416
  { source: "templates/task/standard.md", target: "tasks/templates/standard.md" },
69008
69417
  { source: "templates/task/feature-impl.md", target: "tasks/templates/feature-impl.md" },
@@ -78339,15 +78748,23 @@ ${result.content}`);
78339
78748
  task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption("--file <path>", "Path to the batch JSON file validated against task-batch.schema.json").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (options) => {
78340
78749
  const svc = await makeService2(context3, options.folder);
78341
78750
  try {
78342
- const results = await svc.batchCreate(options.file);
78751
+ const { children, parentsWired } = await svc.batchCreate(options.file);
78343
78752
  if (options.json) {
78344
- const ids = results.map((r2) => r2.ref.id);
78345
- context3.output.write(toJson2({ created: results.length, wbs: ids }));
78753
+ const ids = children.map((r2) => r2.ref.id);
78754
+ context3.output.write(toJson2({ created: children.length, wbs: ids, parentsWired }));
78346
78755
  } else {
78347
- context3.output.write(`Created ${results.length} task(s)`);
78348
- for (const r2 of results) {
78756
+ context3.output.write(`Created ${children.length} task(s)`);
78757
+ for (const r2 of children) {
78349
78758
  context3.output.write(` ${r2.ref.id} ${r2.ref.filePath}`);
78350
78759
  }
78760
+ if (parentsWired.length > 0) {
78761
+ context3.output.write(`Wired ${parentsWired.length} parent(s):`);
78762
+ for (const p2 of parentsWired) {
78763
+ const txLine = p2.transitionedTo ? ` \u2192 ${p2.transitionedTo}` : "";
78764
+ const errLine = p2.errors.length > 0 ? ` (errors: ${p2.errors.join("; ")})` : "";
78765
+ context3.output.write(` ${p2.wbs} rostered=${p2.rostered}${txLine}${errLine}`);
78766
+ }
78767
+ }
78351
78768
  }
78352
78769
  } catch (err) {
78353
78770
  context3.output.error(String(err));
@@ -78404,7 +78821,7 @@ ${result.content}`);
78404
78821
  } else {
78405
78822
  context3.output.write(`Verdict: ${result.verdict} (${result.requirements.length} requirements, ${result.checks.length} checks)`);
78406
78823
  }
78407
- if (result.verdict === "UNKNOWN") {
78824
+ if (result.verdict !== "PASS") {
78408
78825
  context3.setExitCode(1);
78409
78826
  }
78410
78827
  });