@gobing-ai/spur 0.2.11 → 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,28 +58683,26 @@ 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
- const rLines = reqBody.trim().split(`
58669
- `);
58696
+ const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
58670
58697
  let numbered = 0;
58671
- let allLines = 0;
58672
- for (const l of rLines) {
58673
- if (l.trim().length > 0) {
58674
- allLines++;
58675
- if (/^\s*[-*]?\s*(?:\[[ xX]\]\s*)?R\d+\.?\s/.test(l))
58676
- numbered++;
58698
+ for (const block of blocks) {
58699
+ const firstLine = block.trimStart().split(`
58700
+ `)[0] ?? "";
58701
+ if (/^\s*[-*]?\s*(?:\[[ xX]\]\s*)?R\d+\.?\s/.test(firstLine)) {
58702
+ numbered++;
58677
58703
  }
58678
58704
  }
58679
- if (numbered === 0 || numbered < allLines * 0.5) {
58705
+ if (numbered === 0 || numbered < blocks.length * 0.5) {
58680
58706
  findings.push({
58681
58707
  layer: "L3",
58682
58708
  severity: "warning",
@@ -58737,6 +58763,18 @@ var init_task_check = __esm(() => {
58737
58763
  });
58738
58764
  }
58739
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
+ }
58740
58778
  }
58741
58779
  async runL4(doc2, fm, findings, featuresDir, tasksDir) {
58742
58780
  const featureId2 = fm.feature_id ?? fm["feature-id"];
@@ -58939,8 +58977,9 @@ function parseVerdict(raw, fallbackWbs) {
58939
58977
  const wbs = typeof obj.wbs === "string" ? obj.wbs : fallbackWbs ?? "";
58940
58978
  const verdict = normalizeVerdict(obj.verdict);
58941
58979
  const requirements = normalizeRequirements(obj.requirements);
58980
+ const acceptanceCriteria = normalizeAcceptanceCriteria(obj.acceptanceCriteria);
58942
58981
  const checks4 = normalizeChecks(obj.checks);
58943
- return { wbs, verdict, requirements, checks: checks4 };
58982
+ return { wbs, verdict, requirements, acceptanceCriteria, checks: checks4 };
58944
58983
  }
58945
58984
  } catch {}
58946
58985
  return { wbs: fallbackWbs ?? "", verdict: "UNKNOWN", requirements: [], checks: [] };
@@ -58975,6 +59014,16 @@ function normalizeRequirements(raw) {
58975
59014
  evidence: typeof r.evidence === "string" ? r.evidence : ""
58976
59015
  }));
58977
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
+ }
58978
59027
  function normalizeChecks(raw) {
58979
59028
  if (!Array.isArray(raw))
58980
59029
  return [];
@@ -59002,6 +59051,15 @@ function renderTesting(v) {
59002
59051
  lines.push(`| ${req.id} | ${req.status} | ${evidence} |`);
59003
59052
  }
59004
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
+ }
59005
59063
  lines.push("- Coverage: N/A (verdict-based; verify pipeline does not measure code coverage)");
59006
59064
  return lines.join(`
59007
59065
  `);
@@ -59142,7 +59200,7 @@ function bulletizeRequirements(raw) {
59142
59200
  return parts.map((p) => `- ${p}`).join(`
59143
59201
  `);
59144
59202
  }
59145
- function stripLeadingSectionHeader(body, sectionName) {
59203
+ function stripLeadingSectionHeader2(body, sectionName) {
59146
59204
  const match = body.match(/^(\s*)#{1,6}\s+(.+?)\s*(?:\n|$)/);
59147
59205
  if (match === null)
59148
59206
  return body;
@@ -59281,7 +59339,7 @@ class TaskService {
59281
59339
  async updateSection(wbs, sectionName, sourceFile) {
59282
59340
  const filePath = await this.resolveTaskFile(wbs);
59283
59341
  const raw = await this.ctx.fs.readFile(sourceFile);
59284
- const body = stripLeadingSectionHeader(raw, sectionName);
59342
+ const body = stripLeadingSectionHeader2(raw, sectionName);
59285
59343
  const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
59286
59344
  return this.writeService.updateSection(ref, sectionName, body);
59287
59345
  }
@@ -59349,7 +59407,49 @@ ${issues}`);
59349
59407
  }
59350
59408
  throw err;
59351
59409
  }
59352
- 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;
59353
59453
  }
59354
59454
  async createBatchItem(item) {
59355
59455
  const folder = this.ctx.tasksDir;
@@ -59385,6 +59485,14 @@ ${issues}`);
59385
59485
  if (item.priority !== undefined && item.priority !== null) {
59386
59486
  content2 = patchFrontmatterField(content2, "priority", item.priority);
59387
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
+ }
59388
59496
  const ref2 = { kind: "task", id: wbs, filePath, folder };
59389
59497
  return { ref: ref2, content: content2 };
59390
59498
  }
@@ -59719,22 +59827,25 @@ var init_task_service = __esm(() => {
59719
59827
  // ../../packages/app/src/services/task-verdict.ts
59720
59828
  function deriveVerdict(answerText, taskCheckPassed) {
59721
59829
  const requirements = extractRequirements(answerText);
59722
- const checks4 = extractChecks(answerText, taskCheckPassed);
59830
+ const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(extractAcceptanceCriteria(answerText));
59831
+ const checks4 = extractChecks(answerText, taskCheckPassed, acceptanceCriteria);
59723
59832
  if (requirements.length === 0) {
59724
- return { verdict: "UNKNOWN", requirements, checks: checks4 };
59833
+ return { verdict: "UNKNOWN", requirements, acceptanceCriteria, checks: checks4 };
59725
59834
  }
59726
59835
  const hasUnmet = requirements.some((r) => r.status === "UNMET");
59727
- if (hasUnmet) {
59728
- 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 };
59729
59839
  }
59730
59840
  const hasPartial = requirements.some((r) => r.status === "PARTIAL");
59731
- if (hasPartial) {
59732
- 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 };
59733
59844
  }
59734
59845
  if (!taskCheckPassed) {
59735
- return { verdict: "PARTIAL", requirements, checks: checks4 };
59846
+ return { verdict: "PARTIAL", requirements, acceptanceCriteria, checks: checks4 };
59736
59847
  }
59737
- return { verdict: "PASS", requirements, checks: checks4 };
59848
+ return { verdict: "PASS", requirements, acceptanceCriteria, checks: checks4 };
59738
59849
  }
59739
59850
  function extractRequirements(text4) {
59740
59851
  const reqs = [];
@@ -59757,6 +59868,12 @@ function extractRequirements(text4) {
59757
59868
  if (/^[-:]+$/.test(cells[0] ?? ""))
59758
59869
  continue;
59759
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
+ }
59760
59877
  const id = cells[0] ?? "";
59761
59878
  const statusRaw = (cells[1] ?? "").toUpperCase();
59762
59879
  const evidence = cells.length >= 3 ? cells[2] ?? "" : "";
@@ -59777,7 +59894,82 @@ function normalizeStatus(raw) {
59777
59894
  return "UNMET";
59778
59895
  return null;
59779
59896
  }
59780
- 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) {
59781
59973
  const checks4 = [
59782
59974
  {
59783
59975
  name: "spur task check",
@@ -59809,6 +60001,14 @@ function extractChecks(_text, taskCheckPassed) {
59809
60001
  }
59810
60002
  }
59811
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
+ }
59812
60012
  return checks4;
59813
60013
  }
59814
60014
 
@@ -59966,8 +60166,11 @@ var init_team_service = __esm(() => {
59966
60166
  });
59967
60167
 
59968
60168
  // ../../packages/app/src/workflow/actions/agent-run.ts
60169
+ import { execFile } from "child_process";
60170
+ import { existsSync as existsSync4 } from "fs";
59969
60171
  import { mkdir, writeFile } from "fs/promises";
59970
60172
  import { dirname as dirname9, isAbsolute as isAbsolute2, join as join10 } from "path";
60173
+ import { promisify as promisify3 } from "util";
59971
60174
 
59972
60175
  class AgentRunActionRunner {
59973
60176
  kind = KIND;
@@ -60012,16 +60215,31 @@ class AgentRunActionRunner {
60012
60215
  if (continueFlag !== undefined)
60013
60216
  flags.continue = continueFlag;
60014
60217
  const answerFile = asOptionalString(options.answerFile);
60218
+ const expectFile = asOptionalString(options.expectFile);
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);
60022
60227
  await mkdir(dirname9(target), { recursive: true });
60023
60228
  await writeFile(target, answer, "utf8");
60024
60229
  }
60230
+ if (ok2 && expectFile !== undefined) {
60231
+ const target = isAbsolute2(expectFile) ? expectFile : join10(cwd, expectFile);
60232
+ if (!existsSync4(target)) {
60233
+ return {
60234
+ ok: false,
60235
+ data: { exitCode: exitCode2, agent: agentLabel, answer },
60236
+ error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
60237
+ };
60238
+ }
60239
+ }
60240
+ if (!ok2) {
60241
+ await writePartialWorkArtifact(context3, agentLabel, captured, cwd);
60242
+ }
60025
60243
  return {
60026
60244
  ok: ok2,
60027
60245
  data: { exitCode: exitCode2, agent: agentLabel, answer },
@@ -60031,6 +60249,16 @@ class AgentRunActionRunner {
60031
60249
  }
60032
60250
  const exitCode = await this.agentService.run(input, flags);
60033
60251
  const ok = exitCode === 0;
60252
+ if (ok && expectFile !== undefined) {
60253
+ const target = isAbsolute2(expectFile) ? expectFile : join10(cwd, expectFile);
60254
+ if (!existsSync4(target)) {
60255
+ return {
60256
+ ok: false,
60257
+ data: { exitCode, agent: agentLabel },
60258
+ error: `agent.run (${agentLabel}) exited 0 but expected file is absent: ${expectFile}`
60259
+ };
60260
+ }
60261
+ }
60034
60262
  return {
60035
60263
  ok,
60036
60264
  data: { exitCode, agent: agentLabel },
@@ -60066,8 +60294,60 @@ function asOptionalNumber(value) {
60066
60294
  }
60067
60295
  return;
60068
60296
  }
60069
- var KIND = "agent.run";
60070
- 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
+ });
60071
60351
 
60072
60352
  // ../../packages/app/src/workflow/actions/file-exists.ts
60073
60353
  class FileExistsActionRunner {
@@ -60146,9 +60426,51 @@ var init_file_read = __esm(() => {
60146
60426
  init_dist3();
60147
60427
  });
60148
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
+
60149
60471
  // ../../packages/app/src/workflow/actions/hitl-confirm.ts
60150
60472
  class HitlConfirmActionRunner {
60151
- kind = KIND4;
60473
+ kind = KIND5;
60152
60474
  responder;
60153
60475
  constructor(responder) {
60154
60476
  this.responder = responder;
@@ -60176,12 +60498,9 @@ class HitlConfirmActionRunner {
60176
60498
  node: context3.stateOrNodeId,
60177
60499
  ok: !(answer.cancelled || answer.value === "cancel")
60178
60500
  });
60179
- if (answer.cancelled || answer.value === "cancel") {
60180
- return { ok: false, error: "hitl.confirm cancelled" };
60181
- }
60182
60501
  return {
60183
60502
  ok: true,
60184
- data: { answer: answer.value },
60503
+ data: { answer: answer.value, cancelled: answer.cancelled === true || answer.value === "cancel" },
60185
60504
  setVars: { [varName]: answer.value }
60186
60505
  };
60187
60506
  }
@@ -60191,11 +60510,11 @@ function asString2(value) {
60191
60510
  return;
60192
60511
  return String(value);
60193
60512
  }
60194
- var KIND4 = "hitl.confirm";
60513
+ var KIND5 = "hitl.confirm";
60195
60514
 
60196
60515
  // ../../packages/app/src/workflow/actions/hitl-input.ts
60197
60516
  class HitlInputActionRunner {
60198
- kind = KIND5;
60517
+ kind = KIND6;
60199
60518
  responder;
60200
60519
  constructor(responder) {
60201
60520
  this.responder = responder;
@@ -60238,11 +60557,11 @@ function asString3(value) {
60238
60557
  return;
60239
60558
  return String(value);
60240
60559
  }
60241
- var KIND5 = "hitl.input";
60560
+ var KIND6 = "hitl.input";
60242
60561
 
60243
60562
  // ../../packages/app/src/workflow/actions/hitl-select.ts
60244
60563
  class HitlSelectActionRunner {
60245
- kind = KIND6;
60564
+ kind = KIND7;
60246
60565
  responder;
60247
60566
  constructor(responder) {
60248
60567
  this.responder = responder;
@@ -60295,7 +60614,7 @@ function asStringArray(value) {
60295
60614
  return;
60296
60615
  return value.map((v) => String(v));
60297
60616
  }
60298
- var KIND6 = "hitl.select";
60617
+ var KIND7 = "hitl.select";
60299
60618
 
60300
60619
  // ../../packages/app/src/workflow/actions/http-request.ts
60301
60620
  function resolveTemplate(value, vars) {
@@ -60312,7 +60631,7 @@ function redactHeaders(message) {
60312
60631
  }
60313
60632
 
60314
60633
  class HttpRequestActionRunner {
60315
- kind = KIND7;
60634
+ kind = KIND8;
60316
60635
  requester;
60317
60636
  allowlist;
60318
60637
  constructor(requester, allowlist) {
@@ -60467,7 +60786,7 @@ function asNumberArray(value) {
60467
60786
  return [];
60468
60787
  return value.filter((v) => typeof v === "number" && !Number.isNaN(v));
60469
60788
  }
60470
- 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;
60471
60790
  var init_http_request = __esm(() => {
60472
60791
  ALLOWED_METHODS = {
60473
60792
  GET: true,
@@ -60496,7 +60815,7 @@ var init_http_request = __esm(() => {
60496
60815
 
60497
60816
  // ../../packages/app/src/workflow/actions/response-validate.ts
60498
60817
  class ResponseValidateActionRunner {
60499
- kind = KIND8;
60818
+ kind = KIND9;
60500
60819
  engine;
60501
60820
  constructor(engine2) {
60502
60821
  this.engine = engine2;
@@ -60525,7 +60844,7 @@ function asString5(value) {
60525
60844
  return;
60526
60845
  return String(value);
60527
60846
  }
60528
- var KIND8 = "response.validate";
60847
+ var KIND9 = "response.validate";
60529
60848
 
60530
60849
  // ../../packages/app/src/workflow/utils.ts
60531
60850
  var identity6 = (s) => s, NO_COLOR;
@@ -60542,7 +60861,7 @@ var init_utils3 = __esm(() => {
60542
60861
 
60543
60862
  // ../../packages/app/src/workflow/actions/rule-check.ts
60544
60863
  class RuleCheckActionRunner {
60545
- kind = KIND9;
60864
+ kind = KIND10;
60546
60865
  ruleService;
60547
60866
  constructor(ruleService) {
60548
60867
  this.ruleService = ruleService;
@@ -60577,7 +60896,7 @@ function asString6(value) {
60577
60896
  return;
60578
60897
  return String(value);
60579
60898
  }
60580
- var KIND9 = "rule.check";
60899
+ var KIND10 = "rule.check";
60581
60900
  var init_rule_check = __esm(() => {
60582
60901
  init_utils3();
60583
60902
  });
@@ -60589,6 +60908,7 @@ function registerSpurBuiltins(host, options) {
60589
60908
  host.registerAction(new RuleCheckActionRunner(options.ruleService), "builtin");
60590
60909
  host.registerAction(new FileExistsActionRunner(fileSystem), "builtin");
60591
60910
  host.registerAction(new FileReadActionRunner(fileSystem), "builtin");
60911
+ host.registerAction(new FileReadIntoVarActionRunner(fileSystem), "builtin");
60592
60912
  host.registerAction(new HitlConfirmActionRunner(options.hitlResponder), "builtin");
60593
60913
  host.registerAction(new HitlSelectActionRunner(options.hitlResponder), "builtin");
60594
60914
  host.registerAction(new HitlInputActionRunner(options.hitlResponder), "builtin");
@@ -60604,6 +60924,7 @@ var init_builtins2 = __esm(() => {
60604
60924
  init_agent_run();
60605
60925
  init_file_exists();
60606
60926
  init_file_read();
60927
+ init_file_read_into_var();
60607
60928
  init_http_request();
60608
60929
  init_rule_check();
60609
60930
  });
@@ -61219,6 +61540,7 @@ __export(exports_src2, {
61219
61540
  HitlInputActionRunner: () => HitlInputActionRunner,
61220
61541
  HitlConfirmActionRunner: () => HitlConfirmActionRunner,
61221
61542
  HistoryService: () => HistoryService,
61543
+ FileReadIntoVarActionRunner: () => FileReadIntoVarActionRunner,
61222
61544
  FileReadActionRunner: () => FileReadActionRunner,
61223
61545
  FileExistsActionRunner: () => FileExistsActionRunner,
61224
61546
  FeatureService: () => FeatureService,
@@ -61248,6 +61570,7 @@ var init_src3 = __esm(() => {
61248
61570
  init_agent_run();
61249
61571
  init_file_exists();
61250
61572
  init_file_read();
61573
+ init_file_read_into_var();
61251
61574
  init_rule_check();
61252
61575
  init_builtins2();
61253
61576
  init_lifecycle_adapter();
@@ -68685,7 +69008,7 @@ init_src3();
68685
69008
  init_src3();
68686
69009
  init_loader();
68687
69010
  init_src2();
68688
- import { existsSync as existsSync4 } from "fs";
69011
+ import { existsSync as existsSync5 } from "fs";
68689
69012
  import { join as join12 } from "path";
68690
69013
 
68691
69014
  // src/workflow/resolve-spur-bin.ts
@@ -68708,11 +69031,11 @@ function resolveWorkflowPath(context3, profile) {
68708
69031
  const bundledRoot = bundledConfigRoot();
68709
69032
  if (bundledRoot !== null) {
68710
69033
  const bundledPath = join12(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
68711
- if (existsSync4(bundledPath))
69034
+ if (existsSync5(bundledPath))
68712
69035
  return bundledPath;
68713
69036
  }
68714
69037
  const projectPath = join12(context3.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
68715
- if (existsSync4(projectPath))
69038
+ if (existsSync5(projectPath))
68716
69039
  return projectPath;
68717
69040
  return null;
68718
69041
  }
@@ -68768,23 +69091,115 @@ function registerFeatureCommand(program2, context3) {
68768
69091
  context3.setExitCode(1);
68769
69092
  }
68770
69093
  });
68771
- 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) => {
68772
69095
  const svc = await makeService(context3, options.folder);
68773
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
+ }
68774
69116
  if (options.field !== undefined) {
68775
69117
  if (options.value === undefined) {
68776
69118
  context3.output.error("--value is required with --field");
68777
69119
  context3.setExitCode(2);
68778
69120
  return;
68779
69121
  }
68780
- const result = await svc.update(id, options.field, options.value);
68781
- write(context3, options.json, result, `Updated ${options.field} on feature ${result.ref.id}`);
68782
- } else if (status !== undefined) {
68783
- const result = await svc.transition(id, status);
68784
- write(context3, options.json, result, `${result.ref.id}: ${result.fromStatus} \u2192 ${result.toStatus}`);
68785
- } else {
68786
- 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");
68787
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");
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})`);
68788
69203
  }
68789
69204
  } catch (err) {
68790
69205
  context3.output.error(String(err));
@@ -68896,13 +69311,6 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
68896
69311
  }
68897
69312
  });
68898
69313
  }
68899
- function write(context3, json3, result, humanLine) {
68900
- if (json3) {
68901
- context3.output.write(toJson2(result));
68902
- } else {
68903
- context3.output.write(humanLine);
68904
- }
68905
- }
68906
69314
  async function makeService(context3, folderOverride) {
68907
69315
  const resolved = await resolvePlanningFolders(context3.fs);
68908
69316
  const featuresDir = folderOverride ?? context3.fs.resolve(resolved.featuresDir);
@@ -68914,6 +69322,25 @@ async function makeService(context3, folderOverride) {
68914
69322
  });
68915
69323
  return new FeatureService({ fs: context3.fs, writeService, featuresDir, tasksDir });
68916
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
+ }
68917
69344
 
68918
69345
  // src/commands/history.ts
68919
69346
  init_src3();
@@ -68967,7 +69394,7 @@ import { join as join13, resolve as resolve4 } from "path";
68967
69394
  var CLI_CONFIG = {
68968
69395
  binaryName: "spur",
68969
69396
  binaryLabel: "spur",
68970
- binaryVersion: "0.2.11",
69397
+ binaryVersion: "0.3.0",
68971
69398
  configDir: ".spur",
68972
69399
  configFile: ".spur/config.yaml",
68973
69400
  databaseFile: ".spur/spur.db"
@@ -68983,6 +69410,8 @@ var SCAFFOLD_MANIFEST = [
68983
69410
  { source: "workflows/feature-dev.yaml", target: "workflows/feature-dev.yaml" },
68984
69411
  { source: "workflows/task-pipeline.yaml", target: "workflows/task-pipeline.yaml" },
68985
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" },
68986
69415
  { source: "tasks/section-matrix.yaml", target: "tasks/section-matrix.yaml" },
68987
69416
  { source: "templates/task/standard.md", target: "tasks/templates/standard.md" },
68988
69417
  { source: "templates/task/feature-impl.md", target: "tasks/templates/feature-impl.md" },
@@ -77672,7 +78101,7 @@ async function readTargetStatus(context3, targetPath) {
77672
78101
  init_src3();
77673
78102
  init_loader();
77674
78103
  init_src2();
77675
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
78104
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
77676
78105
  import { join as join18 } from "path";
77677
78106
  // schemas/section-matrix.schema.json
77678
78107
  var section_matrix_schema_default = {
@@ -78319,15 +78748,23 @@ ${result.content}`);
78319
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) => {
78320
78749
  const svc = await makeService2(context3, options.folder);
78321
78750
  try {
78322
- const results = await svc.batchCreate(options.file);
78751
+ const { children, parentsWired } = await svc.batchCreate(options.file);
78323
78752
  if (options.json) {
78324
- const ids = results.map((r2) => r2.ref.id);
78325
- 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 }));
78326
78755
  } else {
78327
- context3.output.write(`Created ${results.length} task(s)`);
78328
- for (const r2 of results) {
78756
+ context3.output.write(`Created ${children.length} task(s)`);
78757
+ for (const r2 of children) {
78329
78758
  context3.output.write(` ${r2.ref.id} ${r2.ref.filePath}`);
78330
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
+ }
78331
78768
  }
78332
78769
  } catch (err) {
78333
78770
  context3.output.error(String(err));
@@ -78384,7 +78821,7 @@ ${result.content}`);
78384
78821
  } else {
78385
78822
  context3.output.write(`Verdict: ${result.verdict} (${result.requirements.length} requirements, ${result.checks.length} checks)`);
78386
78823
  }
78387
- if (result.verdict === "UNKNOWN") {
78824
+ if (result.verdict !== "PASS") {
78388
78825
  context3.setExitCode(1);
78389
78826
  }
78390
78827
  });
@@ -78495,7 +78932,7 @@ function loadTemplateContent(projectRoot, variant) {
78495
78932
  if (templateMissSet.has(variant))
78496
78933
  return;
78497
78934
  const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
78498
- if (existsSync5(localPath)) {
78935
+ if (existsSync6(localPath)) {
78499
78936
  const content = readFileSync6(localPath, "utf8");
78500
78937
  templateContentCache.set(variant, content);
78501
78938
  return content;
@@ -78503,7 +78940,7 @@ function loadTemplateContent(projectRoot, variant) {
78503
78940
  const root = bundledConfigRoot();
78504
78941
  if (root !== null) {
78505
78942
  const templatePath = join18(root, "templates", "task", `${variant}.md`);
78506
- if (existsSync5(templatePath)) {
78943
+ if (existsSync6(templatePath)) {
78507
78944
  const content = readFileSync6(templatePath, "utf8");
78508
78945
  templateContentCache.set(variant, content);
78509
78946
  return content;
@@ -78519,13 +78956,13 @@ function loadTemplateBodies(projectRoot, variant) {
78519
78956
  return cached2;
78520
78957
  let bodies = {};
78521
78958
  const localPath = join18(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
78522
- if (existsSync5(localPath)) {
78959
+ if (existsSync6(localPath)) {
78523
78960
  bodies = extractTemplateBodies(readFileSync6(localPath, "utf8"));
78524
78961
  } else {
78525
78962
  const root = bundledConfigRoot();
78526
78963
  if (root !== null) {
78527
78964
  const templatePath = join18(root, "templates", "task", `${variant}.md`);
78528
- if (existsSync5(templatePath)) {
78965
+ if (existsSync6(templatePath)) {
78529
78966
  bodies = extractTemplateBodies(readFileSync6(templatePath, "utf8"));
78530
78967
  }
78531
78968
  }
@@ -78560,7 +78997,7 @@ async function loadSectionMatrix(projectRoot) {
78560
78997
  }
78561
78998
  async function loadSectionMatrixUncached(projectRoot) {
78562
78999
  const localPath = join18(projectRoot, ".spur", "tasks", "section-matrix.yaml");
78563
- if (existsSync5(localPath)) {
79000
+ if (existsSync6(localPath)) {
78564
79001
  const data = await loadStructuredSpurConfig(localPath, {
78565
79002
  validateJsonSchema: true,
78566
79003
  embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
@@ -78570,7 +79007,7 @@ async function loadSectionMatrixUncached(projectRoot) {
78570
79007
  const root = bundledConfigRoot();
78571
79008
  if (root !== null) {
78572
79009
  const matrixPath = join18(root, "tasks", "section-matrix.yaml");
78573
- if (existsSync5(matrixPath)) {
79010
+ if (existsSync6(matrixPath)) {
78574
79011
  const data = await loadStructuredSpurConfig(matrixPath, {
78575
79012
  validateJsonSchema: true,
78576
79013
  embeddedSchemas: EMBEDDED_SPUR_SCHEMAS