@gobing-ai/spur 0.3.23 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/spur.js +258 -25
- package/web/_astro/{BoardApp.b8HvN_Uy.js → BoardApp.Cuna9Z5I.js} +60 -60
- package/web/_astro/BoardApp.DSSZTsSW.js +1 -0
- package/web/_astro/{TaskDetail.BRr8ebLd.js → TaskDetail.Bso6Ccoa.js} +1 -1
- package/web/_astro/{arc.D9I3ghf3.js → arc.BpfaYkuC.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B60VjZ82.js → architectureDiagram-3BPJPVTR.Bsc64OWC.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.B8xiXsGb.js → blockDiagram-GPEHLZMM.CgrqZDLK.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.D8Q9jRJZ.js → c4Diagram-AAUBKEIU.5ROHxpqD.js} +1 -1
- package/web/_astro/channel.DKzvLld2.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.BVcogAuP.js → chunk-2J33WTMH.BIuSLWP4.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.pDodeppW.js → chunk-4BX2VUAB.u-fSGnxt.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.5dYLJxJn.js → chunk-55IACEB6.pL62Yh8J.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.C6GTI_n6.js → chunk-727SXJPM.Cxy6KODj.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.kgDX41bQ.js → chunk-AQP2D5EJ.D2XUILVx.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.CSnAi74r.js → chunk-FMBD7UC4.DNnV20jb.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.qQ8ldUUT.js → chunk-ND2GUHAM.Cc-L-_6y.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.PzPFlVri.js → chunk-QZHKN3VN.Bw_kp1ef.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.C7RulRYW.js → classDiagram-4FO5ZUOK.CoE-GIzM.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.C7RulRYW.js → classDiagram-v2-Q7XG4LA2.CoE-GIzM.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.Dw4HZHnT.js → cose-bilkent-S5V4N54A.CDsPYTeJ.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.MmGbZWM_.js → dagre-BM42HDAG.BUw8fm5M.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DugWYvZC.js → diagram-2AECGRRQ.Bx3Ndc4x.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.BL7D0BMi.js → diagram-5GNKFQAL.DZOACNsA.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.B9GQMjwM.js → diagram-KO2AKTUF.BVmZqHXi.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CbHPvhbu.js → diagram-LMA3HP47.Dhw7Sl9e.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bx6GcKpw.js → diagram-OG6HWLK6.xOqh3zFz.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35._sHJgXD7.js → erDiagram-TEJ5UH35.VX6HV9QU.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.D305L8m4.js → flowDiagram-I6XJVG4X.D5tzv1nF.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.qjJ4kX00.js → ganttDiagram-6RSMTGT7.bRb0hclW.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.D-t4MzVO.js → gitGraphDiagram-PVQCEYII.BrXfUWT_.js} +1 -1
- package/web/_astro/index.BRj8-zmo.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.WwSNIljZ.js → infoDiagram-5YYISTIA.LIQUmEkh.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.EWsUYdpd.js → ishikawaDiagram-YF4QCWOH.BMMwawXt.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CqKE9hNP.js → journeyDiagram-JHISSGLW.F7Rw6EQ6.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BDCCufoa.js → kanban-definition-UN3LZRKU.BnZBa0gE.js} +1 -1
- package/web/_astro/{linear.CyHEakPy.js → linear.DamosLmv.js} +1 -1
- package/web/_astro/{mermaid.core.B-9P_KDH.js → mermaid.core.BjIs-rhV.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.bCxpjW2r.js → mindmap-definition-RKZ34NQL.CAJpo14X.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.x_CRiGMu.js → pieDiagram-4H26LBE5.AcbGV8z3.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.CnbQkwo7.js → quadrantDiagram-W4KKPZXB.DFHo7-vl.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CfeJKVEP.js → requirementDiagram-4Y6WPE33.ZcAUCpFg.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.COYpTQlG.js → sankeyDiagram-5OEKKPKP.B3jEuSva.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DVzoeimO.js → sequenceDiagram-3UESZ5HK.Cpz6c_VX.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BVL5te9q.js → stateDiagram-AJRCARHV.g_pKql00.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.C_x1GbV0.js → stateDiagram-v2-BHNVJYJU.DFINnXL2.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.DtZPbOJL.js → timeline-definition-PNZ67QCA.D0m2aZaf.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.pexoUOut.js → vennDiagram-CIIHVFJN.Cyk_24V8.js} +1 -1
- package/web/_astro/{wardley-L42UT6IY.DLG5pKRN.js → wardley-L42UT6IY.CW7JQlsX.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CnYrM4tH.js → wardleyDiagram-YWT4CUSO.CkaxqiW8.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.kBrlce_Y.js → xychartDiagram-2RQKCTM6.DJorKgMY.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.LygxZgOy.js +0 -1
- package/web/_astro/channel.BToTp7Lu.js +0 -1
- package/web/_astro/index.CmVh0AUD.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spur",
|
package/spur.js
CHANGED
|
@@ -37077,6 +37077,8 @@ var init_finding_codes = __esm(() => {
|
|
|
37077
37077
|
"L2.forbidden-section",
|
|
37078
37078
|
"L2.disallowed-section",
|
|
37079
37079
|
"L3.requirements-format",
|
|
37080
|
+
"L3.requirements-empty",
|
|
37081
|
+
"L3.ac-empty",
|
|
37080
37082
|
"L3.solution-file-line",
|
|
37081
37083
|
"L3.review-priority-table",
|
|
37082
37084
|
"L3.testing-coverage",
|
|
@@ -37108,7 +37110,8 @@ var init_finding_codes = __esm(() => {
|
|
|
37108
37110
|
"L4.uncovered-task-scenario",
|
|
37109
37111
|
"L4.uncovered-feature-scenario",
|
|
37110
37112
|
"L4.verifying-incomplete-tasks",
|
|
37111
|
-
"L4.dogfood-missing"
|
|
37113
|
+
"L4.dogfood-missing",
|
|
37114
|
+
"L4.scenario-unverified"
|
|
37112
37115
|
];
|
|
37113
37116
|
FINDING_CODES = {
|
|
37114
37117
|
L1_MARKDOWN_PARSE: "L1.markdown-parse",
|
|
@@ -37117,6 +37120,8 @@ var init_finding_codes = __esm(() => {
|
|
|
37117
37120
|
L2_FORBIDDEN_SECTION: "L2.forbidden-section",
|
|
37118
37121
|
L2_DISALLOWED_SECTION: "L2.disallowed-section",
|
|
37119
37122
|
L3_REQUIREMENTS_FORMAT: "L3.requirements-format",
|
|
37123
|
+
L3_REQUIREMENTS_EMPTY: "L3.requirements-empty",
|
|
37124
|
+
L3_AC_EMPTY: "L3.ac-empty",
|
|
37120
37125
|
L3_SOLUTION_FILE_LINE: "L3.solution-file-line",
|
|
37121
37126
|
L3_REVIEW_PRIORITY_TABLE: "L3.review-priority-table",
|
|
37122
37127
|
L3_TESTING_COVERAGE: "L3.testing-coverage",
|
|
@@ -37148,7 +37153,8 @@ var init_finding_codes = __esm(() => {
|
|
|
37148
37153
|
L4_UNCOVERED_TASK_SCENARIO: "L4.uncovered-task-scenario",
|
|
37149
37154
|
L4_UNCOVERED_FEATURE_SCENARIO: "L4.uncovered-feature-scenario",
|
|
37150
37155
|
L4_VERIFYING_INCOMPLETE_TASKS: "L4.verifying-incomplete-tasks",
|
|
37151
|
-
L4_DOGFOOD_MISSING: "L4.dogfood-missing"
|
|
37156
|
+
L4_DOGFOOD_MISSING: "L4.dogfood-missing",
|
|
37157
|
+
L4_SCENARIO_UNVERIFIED: "L4.scenario-unverified"
|
|
37152
37158
|
};
|
|
37153
37159
|
});
|
|
37154
37160
|
|
|
@@ -60050,7 +60056,7 @@ class AgentService {
|
|
|
60050
60056
|
const raw = stringFlag(flags, "agent", "auto");
|
|
60051
60057
|
if (raw === "auto")
|
|
60052
60058
|
return this.resolveAgentAuto(prompt, flags, doctorRunner);
|
|
60053
|
-
return this.
|
|
60059
|
+
return this.resolveExecutorSelector(raw, doctorRunner, "explicit");
|
|
60054
60060
|
}
|
|
60055
60061
|
async resolveAgentAuto(prompt, flags, doctorRunner) {
|
|
60056
60062
|
const config4 = this.ctx.agentConfig;
|
|
@@ -60175,16 +60181,6 @@ class AgentService {
|
|
|
60175
60181
|
return usable.result;
|
|
60176
60182
|
return { ok: true, agent: canonical, source };
|
|
60177
60183
|
}
|
|
60178
|
-
async resolveAgentExplicit(name, doctorRunner, source) {
|
|
60179
|
-
const canonical = resolveAgentName(name);
|
|
60180
|
-
if (canonical === undefined) {
|
|
60181
|
-
return { ok: false, exitCode: 2, message: `Unknown agent: ${name}` };
|
|
60182
|
-
}
|
|
60183
|
-
const usable = await this.checkUsable(canonical, doctorRunner);
|
|
60184
|
-
if (!usable.ok)
|
|
60185
|
-
return usable.result;
|
|
60186
|
-
return { ok: true, agent: canonical, source };
|
|
60187
|
-
}
|
|
60188
60184
|
async checkUsable(canonical, doctorRunner) {
|
|
60189
60185
|
const result = await doctorRunner.runOne(canonical);
|
|
60190
60186
|
if (!result.installed) {
|
|
@@ -61245,7 +61241,8 @@ var init_feature_check = __esm(() => {
|
|
|
61245
61241
|
await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
|
|
61246
61242
|
}
|
|
61247
61243
|
const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join7(dirname8(options.featuresDir), "dogfood") : undefined);
|
|
61248
|
-
|
|
61244
|
+
const runDir = options?.runDir ?? (options?.tasksDir ? join7(dirname8(options.tasksDir), ".spur", "run") : undefined);
|
|
61245
|
+
await this.runL4(doc2, featureId2, status, options?.tasksDir, dogfoodDir, runDir, findings);
|
|
61249
61246
|
return { id: featureId2, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
|
|
61250
61247
|
}
|
|
61251
61248
|
runL3(doc2, findings) {
|
|
@@ -61377,13 +61374,14 @@ var init_feature_check = __esm(() => {
|
|
|
61377
61374
|
});
|
|
61378
61375
|
}
|
|
61379
61376
|
}
|
|
61380
|
-
async runL4(doc2, featureId2, status, tasksDir, dogfoodDir, findings) {
|
|
61377
|
+
async runL4(doc2, featureId2, status, tasksDir, dogfoodDir, runDir, findings) {
|
|
61381
61378
|
if (tasksDir === undefined)
|
|
61382
61379
|
return;
|
|
61383
61380
|
let linkedTasks = 0;
|
|
61384
61381
|
const incompleteTasks = [];
|
|
61385
61382
|
const linkedTaskAc = [];
|
|
61386
61383
|
const linkedTaskSolutions = [];
|
|
61384
|
+
const linkedTaskRecords = [];
|
|
61387
61385
|
try {
|
|
61388
61386
|
const entries = await this.fs.readDir(tasksDir);
|
|
61389
61387
|
for (const entry of entries) {
|
|
@@ -61398,13 +61396,15 @@ var init_feature_check = __esm(() => {
|
|
|
61398
61396
|
continue;
|
|
61399
61397
|
linkedTasks += 1;
|
|
61400
61398
|
const tStatus = tfm.status ?? "backlog";
|
|
61399
|
+
const wbs = entry.match(/^(\d{4})_/)?.[1] ?? entry;
|
|
61401
61400
|
if (tStatus !== "done" && tStatus !== "cancelled") {
|
|
61402
|
-
incompleteTasks.push(
|
|
61401
|
+
incompleteTasks.push(wbs);
|
|
61403
61402
|
}
|
|
61404
61403
|
const tac = stripAcFence(taskDoc.getSection("Acceptance Criteria") ?? "");
|
|
61405
61404
|
if (tac.trim().length > 0)
|
|
61406
61405
|
linkedTaskAc.push(tac);
|
|
61407
61406
|
linkedTaskSolutions.push(taskDoc.getSection("Solution") ?? "");
|
|
61407
|
+
linkedTaskRecords.push({ wbs, status: tStatus, ac: tac });
|
|
61408
61408
|
} catch {
|
|
61409
61409
|
findings.push({
|
|
61410
61410
|
layer: "L4",
|
|
@@ -61446,6 +61446,7 @@ var init_feature_check = __esm(() => {
|
|
|
61446
61446
|
});
|
|
61447
61447
|
}
|
|
61448
61448
|
}
|
|
61449
|
+
await this.checkScenarioSatisfaction(acBody, linkedTaskRecords, runDir, findings);
|
|
61449
61450
|
if (status === "verifying" && incompleteTasks.length > 0) {
|
|
61450
61451
|
findings.push({
|
|
61451
61452
|
layer: "L4",
|
|
@@ -61481,6 +61482,77 @@ var init_feature_check = __esm(() => {
|
|
|
61481
61482
|
}
|
|
61482
61483
|
}
|
|
61483
61484
|
}
|
|
61485
|
+
async checkScenarioSatisfaction(featureAc, linkedTasks, runDir, findings) {
|
|
61486
|
+
if (linkedTasks.length === 0 || runDir === undefined)
|
|
61487
|
+
return;
|
|
61488
|
+
const parsed = parseFeature(featureAc);
|
|
61489
|
+
if (parsed === null)
|
|
61490
|
+
return;
|
|
61491
|
+
if (parsed.scenarios.length === 0)
|
|
61492
|
+
return;
|
|
61493
|
+
const scenarioAliases = parsed.scenarios.map((s, i2) => ({
|
|
61494
|
+
title: s.name,
|
|
61495
|
+
normalized: normalizeTitle(s.name),
|
|
61496
|
+
alias: `AC-${i2 + 1}`
|
|
61497
|
+
}));
|
|
61498
|
+
const covers = {};
|
|
61499
|
+
for (const sc of scenarioAliases) {
|
|
61500
|
+
const linked = [];
|
|
61501
|
+
for (const task of linkedTasks) {
|
|
61502
|
+
const taskCov = checkAcCoverage(`Feature: x
|
|
61503
|
+
Scenario: ${sc.title}
|
|
61504
|
+
Given x`, task.ac);
|
|
61505
|
+
if (!taskCov.orphans.includes(sc.title)) {
|
|
61506
|
+
linked.push({ wbs: task.wbs, status: task.status });
|
|
61507
|
+
}
|
|
61508
|
+
}
|
|
61509
|
+
covers[sc.title] = linked;
|
|
61510
|
+
}
|
|
61511
|
+
for (const sc of scenarioAliases) {
|
|
61512
|
+
const linked = covers[sc.title] ?? [];
|
|
61513
|
+
if (linked.length === 0)
|
|
61514
|
+
continue;
|
|
61515
|
+
const verified = await this.isScenarioVerified(sc, linked, runDir);
|
|
61516
|
+
if (!verified) {
|
|
61517
|
+
findings.push({
|
|
61518
|
+
layer: "L4",
|
|
61519
|
+
code: FINDING_CODES.L4_SCENARIO_UNVERIFIED,
|
|
61520
|
+
severity: "warning",
|
|
61521
|
+
section: "Acceptance Criteria",
|
|
61522
|
+
message: `Feature scenario "${sc.title}" is linked but unverified: covering task(s) ` + `${linked.map((l) => l.wbs).join(", ")} have no PASS verdict with MET requirement`
|
|
61523
|
+
});
|
|
61524
|
+
}
|
|
61525
|
+
}
|
|
61526
|
+
}
|
|
61527
|
+
async isScenarioVerified(sc, linked, runDir) {
|
|
61528
|
+
for (const task of linked) {
|
|
61529
|
+
if (task.status !== "done")
|
|
61530
|
+
continue;
|
|
61531
|
+
const artifact = await this.readVerdictArtifact(runDir, task.wbs);
|
|
61532
|
+
if (artifact === null)
|
|
61533
|
+
continue;
|
|
61534
|
+
if (artifact.verdict !== "PASS")
|
|
61535
|
+
continue;
|
|
61536
|
+
const matched = artifact.requirements.find((r) => normalizeTitle(r.id) === sc.normalized || normalizeTitle(r.alias ?? r.id) === sc.normalized || r.id === sc.alias);
|
|
61537
|
+
if (matched !== undefined && matched.status === "MET")
|
|
61538
|
+
return true;
|
|
61539
|
+
}
|
|
61540
|
+
return false;
|
|
61541
|
+
}
|
|
61542
|
+
async readVerdictArtifact(runDir, wbs) {
|
|
61543
|
+
try {
|
|
61544
|
+
const raw = await this.fs.readFile(join7(runDir, `${wbs}-verdict.json`));
|
|
61545
|
+
const parsed = JSON.parse(raw);
|
|
61546
|
+
if (typeof parsed.verdict !== "string")
|
|
61547
|
+
return null;
|
|
61548
|
+
if (!Array.isArray(parsed.requirements))
|
|
61549
|
+
return null;
|
|
61550
|
+
const requirements = parsed.requirements.filter((r) => typeof r?.id === "string" && typeof r?.status === "string").map((r) => ({ id: r.id, status: r.status }));
|
|
61551
|
+
return { verdict: parsed.verdict, requirements };
|
|
61552
|
+
} catch {
|
|
61553
|
+
return null;
|
|
61554
|
+
}
|
|
61555
|
+
}
|
|
61484
61556
|
};
|
|
61485
61557
|
});
|
|
61486
61558
|
|
|
@@ -63754,7 +63826,7 @@ var init_task_check = __esm(() => {
|
|
|
63754
63826
|
this.runL3(doc2, entry, status, findings);
|
|
63755
63827
|
const tasksDir = dirname9(filePath);
|
|
63756
63828
|
const featuresDir = join9(dirname9(tasksDir), "features");
|
|
63757
|
-
await this.runL4(doc2, fm, findings, featuresDir, tasksDir);
|
|
63829
|
+
await this.runL4(doc2, fm, status, findings, featuresDir, tasksDir);
|
|
63758
63830
|
await this.runL4Rollup(doc2, wbs, status, findings, tasksDir);
|
|
63759
63831
|
if (status !== "done" && status !== "cancelled") {
|
|
63760
63832
|
await this.runL4Readiness(doc2, fm, wbs, status, findings, tasksDir);
|
|
@@ -63762,6 +63834,26 @@ var init_task_check = __esm(() => {
|
|
|
63762
63834
|
return { wbs, ...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides) };
|
|
63763
63835
|
}
|
|
63764
63836
|
runL3(doc2, entry, status, findings) {
|
|
63837
|
+
const reqBodyRaw = doc2.getSection("Requirements");
|
|
63838
|
+
if (reqBodyRaw !== null && isPlaceholderBody(reqBodyRaw)) {
|
|
63839
|
+
findings.push({
|
|
63840
|
+
layer: "L3",
|
|
63841
|
+
code: FINDING_CODES.L3_REQUIREMENTS_EMPTY,
|
|
63842
|
+
severity: "error",
|
|
63843
|
+
section: "Requirements",
|
|
63844
|
+
message: "Requirements is placeholder-only \u2014 a task with no requirements is unverifiable"
|
|
63845
|
+
});
|
|
63846
|
+
}
|
|
63847
|
+
const acBodyRaw = doc2.getSection("Acceptance Criteria");
|
|
63848
|
+
if (acBodyRaw !== null && isPlaceholderBody(stripAcFence(acBodyRaw))) {
|
|
63849
|
+
findings.push({
|
|
63850
|
+
layer: "L3",
|
|
63851
|
+
code: FINDING_CODES.L3_AC_EMPTY,
|
|
63852
|
+
severity: "error",
|
|
63853
|
+
section: "Acceptance Criteria",
|
|
63854
|
+
message: "Acceptance Criteria is placeholder-only \u2014 populate with real scenarios"
|
|
63855
|
+
});
|
|
63856
|
+
}
|
|
63765
63857
|
const reqBody = doc2.getSection("Requirements");
|
|
63766
63858
|
if (reqBody !== null && !isPlaceholderBody(reqBody)) {
|
|
63767
63859
|
const blocks = reqBody.trim().split(/\n\s*\n/).filter((b) => b.trim().length > 0);
|
|
@@ -63853,7 +63945,7 @@ var init_task_check = __esm(() => {
|
|
|
63853
63945
|
}
|
|
63854
63946
|
}
|
|
63855
63947
|
}
|
|
63856
|
-
async runL4(doc2, fm, findings, featuresDir, tasksDir) {
|
|
63948
|
+
async runL4(doc2, fm, status, findings, featuresDir, tasksDir) {
|
|
63857
63949
|
const template = typeof fm.template === "string" ? fm.template : DEFAULT_TASK_VARIANT;
|
|
63858
63950
|
if (template === DEFAULT_TASK_VARIANT) {
|
|
63859
63951
|
const designBody = doc2.getSection("Design");
|
|
@@ -63882,7 +63974,9 @@ var init_task_check = __esm(() => {
|
|
|
63882
63974
|
});
|
|
63883
63975
|
} else {
|
|
63884
63976
|
const featureStatus = await this.readFeatureStatus(featurePath);
|
|
63885
|
-
|
|
63977
|
+
const featureTerminal = featureStatus === "done" || featureStatus === "cancelled";
|
|
63978
|
+
const taskTerminal = status === "done" || status === "cancelled";
|
|
63979
|
+
if (featureTerminal && !taskTerminal) {
|
|
63886
63980
|
findings.push({
|
|
63887
63981
|
layer: "L4",
|
|
63888
63982
|
code: FINDING_CODES.L4_FEATURE_TERMINAL,
|
|
@@ -64586,6 +64680,26 @@ class TaskService {
|
|
|
64586
64680
|
const templateBodies = this.ctx.resolveTemplateBodies?.(variant) ?? {};
|
|
64587
64681
|
return { ...templateBodies, ...taskBodies };
|
|
64588
64682
|
}
|
|
64683
|
+
async findDuplicateFollowUp(featureId2, title, withinSec) {
|
|
64684
|
+
const siblings = await this.list({ featureId: featureId2 });
|
|
64685
|
+
const now = Date.now();
|
|
64686
|
+
const lowerTitle = title.toLowerCase();
|
|
64687
|
+
for (const t of siblings) {
|
|
64688
|
+
if ((t.name ?? "").toLowerCase() !== lowerTitle)
|
|
64689
|
+
continue;
|
|
64690
|
+
const createdAt = t.frontmatter?.created_at;
|
|
64691
|
+
if (typeof createdAt !== "string")
|
|
64692
|
+
continue;
|
|
64693
|
+
const createdMs = Date.parse(createdAt);
|
|
64694
|
+
if (Number.isNaN(createdMs))
|
|
64695
|
+
continue;
|
|
64696
|
+
const ageSec = (now - createdMs) / 1000;
|
|
64697
|
+
if (ageSec >= 0 && ageSec <= withinSec) {
|
|
64698
|
+
return { wbs: t.wbs, name: t.name };
|
|
64699
|
+
}
|
|
64700
|
+
}
|
|
64701
|
+
return null;
|
|
64702
|
+
}
|
|
64589
64703
|
async create(params) {
|
|
64590
64704
|
const folder = this.ctx.tasksDir;
|
|
64591
64705
|
let background = "";
|
|
@@ -64595,6 +64709,12 @@ class TaskService {
|
|
|
64595
64709
|
const variant = params.template ?? (params.featureId !== undefined ? "feature-impl" : DEFAULT_TASK_VARIANT);
|
|
64596
64710
|
const status = params.status ?? (params.featureId !== undefined ? "todo" : "backlog");
|
|
64597
64711
|
return this.writeService.createAllocated(folder, async () => {
|
|
64712
|
+
if (params.dedupeWithinSec !== undefined && params.featureId !== undefined) {
|
|
64713
|
+
const collision = await this.findDuplicateFollowUp(params.featureId, params.title, params.dedupeWithinSec);
|
|
64714
|
+
if (collision !== null) {
|
|
64715
|
+
throw new DuplicateFollowUpError(collision.wbs, collision.name, params.title);
|
|
64716
|
+
}
|
|
64717
|
+
}
|
|
64598
64718
|
const wbs = await this.allocateWbs();
|
|
64599
64719
|
const slug = this.slugify(params.title);
|
|
64600
64720
|
const filePath = this.resolveTaskPath(wbs, slug);
|
|
@@ -65223,7 +65343,7 @@ ${block}` : block);
|
|
|
65223
65343
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
65224
65344
|
}
|
|
65225
65345
|
}
|
|
65226
|
-
var DependencyMutationError, SectionMutationError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
|
|
65346
|
+
var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
|
|
65227
65347
|
var init_task_service = __esm(() => {
|
|
65228
65348
|
init_src2();
|
|
65229
65349
|
init_task_locator();
|
|
@@ -65244,6 +65364,18 @@ var init_task_service = __esm(() => {
|
|
|
65244
65364
|
this.code = code;
|
|
65245
65365
|
}
|
|
65246
65366
|
};
|
|
65367
|
+
DuplicateFollowUpError = class DuplicateFollowUpError extends Error {
|
|
65368
|
+
existingWbs;
|
|
65369
|
+
existingName;
|
|
65370
|
+
attemptedName;
|
|
65371
|
+
constructor(existingWbs, existingName, attemptedName) {
|
|
65372
|
+
super(`duplicate-follow-up: task ${existingWbs} ("${existingName}") already exists under this feature ` + `with an identical name. Reuse ${existingWbs} or pass --allow-duplicate-name to override.`);
|
|
65373
|
+
this.name = "DuplicateFollowUpError";
|
|
65374
|
+
this.existingWbs = existingWbs;
|
|
65375
|
+
this.existingName = existingName;
|
|
65376
|
+
this.attemptedName = attemptedName;
|
|
65377
|
+
}
|
|
65378
|
+
};
|
|
65247
65379
|
ROSTER_REGION_RE = /<!--[ \t]*AUTO-GENERATED[^>]*-->[\s\S]*?<!--[ \t]*END[ \t]*AUTO-GENERATED[^>]*-->/g;
|
|
65248
65380
|
TASK_ACTION_COMMANDS = {
|
|
65249
65381
|
refine: (wbs) => `/sp:dev-refine ${wbs} --auto`,
|
|
@@ -65446,6 +65578,50 @@ function extractChecks(_text, taskCheckPassed, acceptanceCriteria) {
|
|
|
65446
65578
|
}
|
|
65447
65579
|
return checks4;
|
|
65448
65580
|
}
|
|
65581
|
+
function classifyTaskOutcome(taskStatus, verdict) {
|
|
65582
|
+
const status = String(taskStatus).toLowerCase();
|
|
65583
|
+
if (NOT_STARTED_STATUSES[status]) {
|
|
65584
|
+
return "NOT-STARTED";
|
|
65585
|
+
}
|
|
65586
|
+
const v = String(verdict ?? "UNKNOWN").toUpperCase();
|
|
65587
|
+
if (v === "PASS" || v === "PARTIAL" || v === "FAIL")
|
|
65588
|
+
return v;
|
|
65589
|
+
return "UNKNOWN";
|
|
65590
|
+
}
|
|
65591
|
+
function aggregateBatchVerdicts(results) {
|
|
65592
|
+
const notStarted = results.filter((r) => r.outcome === "NOT-STARTED");
|
|
65593
|
+
const rolledUp = results.filter((r) => r.outcome !== "NOT-STARTED");
|
|
65594
|
+
const counts = {
|
|
65595
|
+
PASS: rolledUp.filter((r) => r.outcome === "PASS").length,
|
|
65596
|
+
PARTIAL: rolledUp.filter((r) => r.outcome === "PARTIAL").length,
|
|
65597
|
+
FAIL: rolledUp.filter((r) => r.outcome === "FAIL").length,
|
|
65598
|
+
UNKNOWN: rolledUp.filter((r) => r.outcome === "UNKNOWN").length
|
|
65599
|
+
};
|
|
65600
|
+
let verdict;
|
|
65601
|
+
if (rolledUp.length === 0) {
|
|
65602
|
+
verdict = "UNKNOWN";
|
|
65603
|
+
} else if (counts.FAIL > 0) {
|
|
65604
|
+
verdict = "FAIL";
|
|
65605
|
+
} else if (counts.PARTIAL > 0 || counts.UNKNOWN > 0) {
|
|
65606
|
+
verdict = "PARTIAL";
|
|
65607
|
+
} else {
|
|
65608
|
+
verdict = "PASS";
|
|
65609
|
+
}
|
|
65610
|
+
const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
|
|
65611
|
+
return { verdict, rolledUp, notStarted, summary };
|
|
65612
|
+
}
|
|
65613
|
+
var NOT_STARTED_STATUSES;
|
|
65614
|
+
var init_task_verdict = __esm(() => {
|
|
65615
|
+
NOT_STARTED_STATUSES = {
|
|
65616
|
+
backlog: true,
|
|
65617
|
+
todo: true,
|
|
65618
|
+
wip: false,
|
|
65619
|
+
testing: false,
|
|
65620
|
+
blocked: true,
|
|
65621
|
+
done: false,
|
|
65622
|
+
cancelled: false
|
|
65623
|
+
};
|
|
65624
|
+
});
|
|
65449
65625
|
|
|
65450
65626
|
// ../../packages/app/src/services/team-service.ts
|
|
65451
65627
|
import { join as join10 } from "path";
|
|
@@ -67538,8 +67714,10 @@ __export(exports_src2, {
|
|
|
67538
67714
|
deriveVerdict: () => deriveVerdict,
|
|
67539
67715
|
createPsProcessInspector: () => createPsProcessInspector,
|
|
67540
67716
|
computeAggregate: () => computeAggregate,
|
|
67717
|
+
classifyTaskOutcome: () => classifyTaskOutcome,
|
|
67541
67718
|
clampToolUseLimit: () => clampToolUseLimit,
|
|
67542
67719
|
bridgeEventBus: () => bridgeEventBus,
|
|
67720
|
+
aggregateBatchVerdicts: () => aggregateBatchVerdicts,
|
|
67543
67721
|
WorkflowAppService: () => WorkflowAppService,
|
|
67544
67722
|
UnsupportedProcessPlatformError: () => UnsupportedProcessPlatformError,
|
|
67545
67723
|
TokenLedgerWatcher: () => TokenLedgerWatcher,
|
|
@@ -67588,6 +67766,7 @@ __export(exports_src2, {
|
|
|
67588
67766
|
FeatureCheckService: () => FeatureCheckService,
|
|
67589
67767
|
FINDING_CODES: () => FINDING_CODES,
|
|
67590
67768
|
FEATURE_LIFECYCLE_PROFILE: () => FEATURE_LIFECYCLE_PROFILE,
|
|
67769
|
+
DuplicateFollowUpError: () => DuplicateFollowUpError,
|
|
67591
67770
|
DependencyMutationError: () => DependencyMutationError,
|
|
67592
67771
|
DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX,
|
|
67593
67772
|
CorpusMigrator: () => CorpusMigrator,
|
|
@@ -67619,6 +67798,7 @@ var init_src3 = __esm(() => {
|
|
|
67619
67798
|
init_task_record();
|
|
67620
67799
|
init_task_scaffold();
|
|
67621
67800
|
init_task_service();
|
|
67801
|
+
init_task_verdict();
|
|
67622
67802
|
init_team_service();
|
|
67623
67803
|
init_token_ledger_service();
|
|
67624
67804
|
init_token_ledger_watcher();
|
|
@@ -75628,7 +75808,7 @@ import { join as join15, resolve as resolve4 } from "path";
|
|
|
75628
75808
|
var CLI_CONFIG = {
|
|
75629
75809
|
binaryName: "spur",
|
|
75630
75810
|
binaryLabel: "spur",
|
|
75631
|
-
binaryVersion: "0.3.
|
|
75811
|
+
binaryVersion: "0.3.24",
|
|
75632
75812
|
configDir: ".spur",
|
|
75633
75813
|
configFile: ".spur/config.yaml",
|
|
75634
75814
|
databaseFile: ".spur/spur.db"
|
|
@@ -86201,7 +86381,7 @@ function renderMigrationReport(report, dryRun, corpusDir) {
|
|
|
86201
86381
|
}
|
|
86202
86382
|
function registerTaskCommand(program2, context4) {
|
|
86203
86383
|
const task = program2.command("task").summary("manage tasks");
|
|
86204
|
-
task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
|
|
86384
|
+
task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option("--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation").option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option("--folder <path>", "Custom tasks folder").option("--dedupe-within <seconds>", "Refuse creation if an existing task under the same feature has an identical name created within the last N seconds (task 0341 R4)", (v) => Number.parseInt(v, 10)).option("--allow-duplicate-name", "Override the --dedupe-within guard (creates anyway)").option("--json", "Output machine-readable JSON").action(async (title2, options) => {
|
|
86205
86385
|
if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
|
|
86206
86386
|
context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
|
|
86207
86387
|
context4.setExitCode(2);
|
|
@@ -86213,7 +86393,8 @@ function registerTaskCommand(program2, context4) {
|
|
|
86213
86393
|
title: title2,
|
|
86214
86394
|
featureId: options.feature,
|
|
86215
86395
|
parentWbs: options.parent,
|
|
86216
|
-
template: options.template
|
|
86396
|
+
template: options.template,
|
|
86397
|
+
dedupeWithinSec: options.allowDuplicateName ? undefined : options.dedupeWithin
|
|
86217
86398
|
});
|
|
86218
86399
|
if (options.json) {
|
|
86219
86400
|
context4.output.write(toJson2(result));
|
|
@@ -86221,8 +86402,13 @@ function registerTaskCommand(program2, context4) {
|
|
|
86221
86402
|
context4.output.write(`Created task ${result.ref.id}: ${result.ref.filePath}`);
|
|
86222
86403
|
}
|
|
86223
86404
|
} catch (err) {
|
|
86224
|
-
|
|
86225
|
-
|
|
86405
|
+
if (err instanceof DuplicateFollowUpError) {
|
|
86406
|
+
context4.output.error(err.message);
|
|
86407
|
+
context4.setExitCode(3);
|
|
86408
|
+
} else {
|
|
86409
|
+
context4.output.error(String(err));
|
|
86410
|
+
context4.setExitCode(1);
|
|
86411
|
+
}
|
|
86226
86412
|
}
|
|
86227
86413
|
});
|
|
86228
86414
|
task.command("show").summary("Show a task by WBS.").argument("<wbs>", "Task WBS number").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
|
|
@@ -86605,6 +86791,53 @@ ${result.content}`);
|
|
|
86605
86791
|
context4.setExitCode(1);
|
|
86606
86792
|
}
|
|
86607
86793
|
});
|
|
86794
|
+
task.command("verifyall-aggregate").summary("Aggregate per-task verify outcomes into a deterministic batch verdict (verifyall).").description("Reads a JSON array of per-task outcomes ({wbs,outcome}) and emits the batch " + "verdict with NOT-STARTED tasks excluded from the rollup. Replaces agent-discretion " + "rollup prose (dev-operations.md \xA73a) with deterministic code (task 0341).").option("--from-file <path>", "Path to JSON array of {wbs,outcome[,reason]} rows").option("--json", "Output machine-readable JSON").action(async (options) => {
|
|
86795
|
+
const inputPath = options.fromFile ?? ".spur/run/verifyall-batch-input.json";
|
|
86796
|
+
let raw2;
|
|
86797
|
+
try {
|
|
86798
|
+
raw2 = readFileSync8(inputPath, "utf-8");
|
|
86799
|
+
} catch {
|
|
86800
|
+
context4.output.error(`Batch input file not found: ${inputPath}`);
|
|
86801
|
+
context4.setExitCode(1);
|
|
86802
|
+
return;
|
|
86803
|
+
}
|
|
86804
|
+
let rows;
|
|
86805
|
+
try {
|
|
86806
|
+
const parsed = JSON.parse(raw2);
|
|
86807
|
+
if (!Array.isArray(parsed)) {
|
|
86808
|
+
throw new Error("expected a JSON array");
|
|
86809
|
+
}
|
|
86810
|
+
rows = parsed;
|
|
86811
|
+
} catch (err) {
|
|
86812
|
+
context4.output.error(`Invalid batch input JSON: ${String(err)}`);
|
|
86813
|
+
context4.setExitCode(1);
|
|
86814
|
+
return;
|
|
86815
|
+
}
|
|
86816
|
+
const results = rows.map((r2) => {
|
|
86817
|
+
const outcome = r2.outcome.toUpperCase();
|
|
86818
|
+
if (!["PASS", "PARTIAL", "FAIL", "NOT-STARTED", "UNKNOWN"].includes(outcome)) {
|
|
86819
|
+
throw new Error(`Invalid outcome for ${r2.wbs}: ${r2.outcome}`);
|
|
86820
|
+
}
|
|
86821
|
+
return {
|
|
86822
|
+
wbs: r2.wbs,
|
|
86823
|
+
outcome,
|
|
86824
|
+
reason: r2.reason
|
|
86825
|
+
};
|
|
86826
|
+
});
|
|
86827
|
+
const aggregation = aggregateBatchVerdicts(results);
|
|
86828
|
+
if (options.json) {
|
|
86829
|
+
context4.output.write(JSON.stringify(aggregation, null, 2));
|
|
86830
|
+
} else {
|
|
86831
|
+
context4.output.write(`Batch verdict: ${aggregation.verdict}`);
|
|
86832
|
+
context4.output.write(aggregation.summary);
|
|
86833
|
+
if (aggregation.notStarted.length > 0) {
|
|
86834
|
+
context4.output.write(`NOT-STARTED (excluded from rollup): ${aggregation.notStarted.map((r2) => r2.wbs).join(", ")}`);
|
|
86835
|
+
}
|
|
86836
|
+
}
|
|
86837
|
+
if (aggregation.verdict === "FAIL") {
|
|
86838
|
+
context4.setExitCode(1);
|
|
86839
|
+
}
|
|
86840
|
+
});
|
|
86608
86841
|
task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option("--strict", "Elevate ALL warnings to failures").option("--strict-core", "Gate variant: fail only on hard-core errors (the testing\u2192done guard)").option("--folder <path>", "Custom tasks folder").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
|
|
86609
86842
|
const svc = await makeCheckService(context4);
|
|
86610
86843
|
const json3 = options.json === true;
|