@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/package.json +1 -1
- package/spur-cli/config/workflows/feature-dev.yaml +5 -0
- package/spur-cli/config/workflows/idea-pipeline.yaml +426 -0
- package/spur-cli/config/workflows/planning-pipeline.yaml +50 -18
- package/spur-cli/config/workflows/task-pipeline.yaml +69 -5
- package/spur-cli/config/workflows/wrapup-pipeline.yaml +240 -0
- package/spur.js +515 -78
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
|
|
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 {
|
|
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
|
|
58669
|
-
`);
|
|
58696
|
+
const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
|
|
58670
58697
|
let numbered = 0;
|
|
58671
|
-
|
|
58672
|
-
|
|
58673
|
-
|
|
58674
|
-
|
|
58675
|
-
|
|
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 <
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
59728
|
-
|
|
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
|
-
|
|
59732
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
60070
|
-
|
|
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 =
|
|
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
|
|
60513
|
+
var KIND5 = "hitl.confirm";
|
|
60195
60514
|
|
|
60196
60515
|
// ../../packages/app/src/workflow/actions/hitl-input.ts
|
|
60197
60516
|
class HitlInputActionRunner {
|
|
60198
|
-
kind =
|
|
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
|
|
60560
|
+
var KIND6 = "hitl.input";
|
|
60242
60561
|
|
|
60243
60562
|
// ../../packages/app/src/workflow/actions/hitl-select.ts
|
|
60244
60563
|
class HitlSelectActionRunner {
|
|
60245
|
-
kind =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 (
|
|
69034
|
+
if (existsSync5(bundledPath))
|
|
68712
69035
|
return bundledPath;
|
|
68713
69036
|
}
|
|
68714
69037
|
const projectPath = join12(context3.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
|
|
68715
|
-
if (
|
|
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
|
|
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
|
-
|
|
68781
|
-
|
|
68782
|
-
|
|
68783
|
-
|
|
68784
|
-
|
|
68785
|
-
|
|
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.
|
|
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
|
|
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
|
|
78751
|
+
const { children, parentsWired } = await svc.batchCreate(options.file);
|
|
78323
78752
|
if (options.json) {
|
|
78324
|
-
const ids =
|
|
78325
|
-
context3.output.write(toJson2({ created:
|
|
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 ${
|
|
78328
|
-
for (const r2 of
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
79010
|
+
if (existsSync6(matrixPath)) {
|
|
78574
79011
|
const data = await loadStructuredSpurConfig(matrixPath, {
|
|
78575
79012
|
validateJsonSchema: true,
|
|
78576
79013
|
embeddedSchemas: EMBEDDED_SPUR_SCHEMAS
|