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