@gobing-ai/spur 0.3.97 → 0.3.98
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/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugins/sp/README.md +9 -9
- package/plugins/sp/lib/inline-run.generated.mjs +16 -9
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +3 -3
- package/plugins/sp/scripts/inline-run-setup.mjs +4 -2
- package/plugins/sp/scripts/inline-run-setup.ts +12 -12
- package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
- package/plugins/sp/skills/conflict-finding/SKILL.md +1 -1
- package/plugins/sp/skills/conflict-finding/references/authority-resolution.md +5 -3
- package/plugins/sp/skills/conflict-finding/references/remediation-routing.md +9 -8
- package/plugins/sp/skills/doc-evolve/SKILL.md +4 -3
- package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +7 -4
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +7 -4
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +16 -13
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +7 -8
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +3 -0
- package/spur.js +483 -254
- package/web/_astro/{BoardApp.vveLOdDq.js → BoardApp.CYbe-OTb.js} +5 -5
- package/web/_astro/{BoardApp.Bmv5WkJ9.js → BoardApp.Dflq5a8x.js} +1 -1
- package/web/_astro/{TaskDetail.IKc3uDYV.js → TaskDetail.CK7oXddP.js} +1 -1
- package/web/_astro/{arc.C63Ufg35.js → arc.BIqkDATc.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.CP8Hyldk.js → architectureDiagram-3BPJPVTR.D0B3SvWv.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Bo_48MRA.js → blockDiagram-GPEHLZMM.C2YyKQmL.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ch61sw-O.js → c4Diagram-AAUBKEIU.DmtTYrt_.js} +1 -1
- package/web/_astro/channel.JuKVyuhZ.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CHpzS7xf.js → chunk-2J33WTMH.HsRXbh7l.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DzMNNWIw.js → chunk-4BX2VUAB.obE_P-R3.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.BqPrDw_Q.js → chunk-55IACEB6.CjWTF5mz.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.ZzBHhNw-.js → chunk-727SXJPM.DMo799Ax.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.1_O_LLkx.js → chunk-AQP2D5EJ.-cE6JOKZ.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.GS4UHg7_.js → chunk-FMBD7UC4.D6Rw6HIk.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BnqotN4g.js → chunk-ND2GUHAM.d5rBD73Z.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.Ghuh8zoJ.js → chunk-QZHKN3VN.r3ox40Q3.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BW9vC3zg.js → classDiagram-4FO5ZUOK.CGGbvjjJ.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BW9vC3zg.js → classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.D6jNC0ti.js → cose-bilkent-S5V4N54A.CmKtPd4l.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BOGoCs4L.js → cynefin-OW5HDTMX.QNg5GXs8.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.8iU5VcWi.js → dagre-BM42HDAG.B1SabsjC.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CVt88HwH.js → diagram-2AECGRRQ.RtDbNj89.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.C9HxUUMl.js → diagram-5GNKFQAL.IzZpuM-Y.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.4ywkQN3t.js → diagram-KO2AKTUF.DPTF_fgv.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.C48A6gpz.js → diagram-LMA3HP47.CCyza39P.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DxFieQW4.js → diagram-OG6HWLK6.BXAi8zcZ.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.CtjtQqYs.js → erDiagram-TEJ5UH35.DUDX7_MR.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.C5zAiahU.js → flowDiagram-I6XJVG4X.C63QTh3N.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.D_L6zsny.js → ganttDiagram-6RSMTGT7.CnlZJSwG.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Ignnrk3V.js → gitGraphDiagram-PVQCEYII.CTqIrOPC.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DLv8-P5u.js → infoDiagram-5YYISTIA.BYMiDFh9.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.p0OTpwFO.js → ishikawaDiagram-YF4QCWOH.CRiQmI-z.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CIaBt-DD.js → journeyDiagram-JHISSGLW.YQgWBsEU.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.DGySIntq.js → kanban-definition-UN3LZRKU.1ECnrrK1.js} +1 -1
- package/web/_astro/{linear.BATV4RXu.js → linear.DzVA71yT.js} +1 -1
- package/web/_astro/{mermaid.core.DzkwM3VX.js → mermaid.core.BK7P2Sr_.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.3piBRRzA.js → mindmap-definition-RKZ34NQL.BvXyWZK7.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.DUNueLub.js → pieDiagram-4H26LBE5.BiXk1TW3.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.9bihVbrt.js → quadrantDiagram-W4KKPZXB.krvBH8lh.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CkjsC-YT.js → requirementDiagram-4Y6WPE33.ih1gL25c.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js → sankeyDiagram-5OEKKPKP.CpUxkv4u.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BivrH99P.js → sequenceDiagram-3UESZ5HK.clAfeN7y.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CqLQ1Vzz.js → stateDiagram-AJRCARHV.DpXWb8-m.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.DvvoLD3b.js → stateDiagram-v2-BHNVJYJU.ClZb9GIH.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.B-KR78gv.js → timeline-definition-PNZ67QCA.BLcX5dh7.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.Ck0Eieb5.js → vennDiagram-CIIHVFJN.oGgpKMpx.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CRfdd_do.js → wardleyDiagram-YWT4CUSO.BPHtMtt-.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.Ck_Jxk9C.js → xychartDiagram-2RQKCTM6.BNZOPYSk.js} +1 -1
- package/web/index.html +1 -1
- package/web/_astro/channel.BKCmJpWM.js +0 -1
package/spur.js
CHANGED
|
@@ -62293,6 +62293,14 @@ var init_run_dao = __esm(() => {
|
|
|
62293
62293
|
metadata_json = json_set(COALESCE(NULLIF(metadata_json, ''), '{}'), '$.staleReason', ?2)
|
|
62294
62294
|
WHERE id = ?3 AND status IN ('running', 'pending')`, nowIso, reason, runId);
|
|
62295
62295
|
}
|
|
62296
|
+
async cancelRun(runId) {
|
|
62297
|
+
const nowIso = new Date().toISOString();
|
|
62298
|
+
await this.adapter.run(`UPDATE runs
|
|
62299
|
+
SET status = 'failed', completed_at = ?1, updated_at = ?1,
|
|
62300
|
+
terminal_reason = 'cancelled',
|
|
62301
|
+
metadata_json = json_set(COALESCE(NULLIF(metadata_json, ''), '{}'), '$.staleReason', ?2)
|
|
62302
|
+
WHERE id = ?3 AND status IN ('running', 'pending', 'paused', 'interrupted')`, nowIso, "cancelled by operator (spur workflow cancel)", runId);
|
|
62303
|
+
}
|
|
62296
62304
|
async setPid(runId, pid) {
|
|
62297
62305
|
await this.adapter.run("UPDATE runs SET pid = ? WHERE id = ?", pid, runId);
|
|
62298
62306
|
}
|
|
@@ -79050,15 +79058,6 @@ function parseVerifyVerdict(raw, fallbackWbs) {
|
|
|
79050
79058
|
const verdict = { ...result.data, wbs: result.data.wbs !== "" ? result.data.wbs : wbs };
|
|
79051
79059
|
return { kind: "valid", wbs, verdict };
|
|
79052
79060
|
}
|
|
79053
|
-
async function readVerifyVerdict(fs3, path9, fallbackWbs) {
|
|
79054
|
-
let raw;
|
|
79055
|
-
try {
|
|
79056
|
-
raw = await fs3.readFile(path9);
|
|
79057
|
-
} catch {
|
|
79058
|
-
return { kind: "missing", wbs: fallbackWbs ?? "" };
|
|
79059
|
-
}
|
|
79060
|
-
return parseVerifyVerdict(raw, fallbackWbs);
|
|
79061
|
-
}
|
|
79062
79061
|
function checkRowName(row) {
|
|
79063
79062
|
for (const v of [row.name, row.check, row.id]) {
|
|
79064
79063
|
if (typeof v === "string" && v.trim() !== "")
|
|
@@ -82355,6 +82354,7 @@ function formatNoopMessage(wbs, status) {
|
|
|
82355
82354
|
}
|
|
82356
82355
|
function evaluateDoneTransition(input) {
|
|
82357
82356
|
const { wbs, taskFilePath, currentStatus, targetStatus, forced, reason, artifact, readError } = input;
|
|
82357
|
+
const verdictPath = input.verdictPath ?? `.spur/run/${wbs}-verdict.json`;
|
|
82358
82358
|
if (targetStatus === currentStatus) {
|
|
82359
82359
|
return { kind: "noop", fromStatus: currentStatus, message: formatNoopMessage(wbs, currentStatus) };
|
|
82360
82360
|
}
|
|
@@ -82362,7 +82362,6 @@ function evaluateDoneTransition(input) {
|
|
|
82362
82362
|
return { kind: "allow", reason: "forced" };
|
|
82363
82363
|
}
|
|
82364
82364
|
if (artifact === undefined) {
|
|
82365
|
-
const verdictPath2 = `.spur/run/${wbs}-verdict.json`;
|
|
82366
82365
|
if (readError) {
|
|
82367
82366
|
return {
|
|
82368
82367
|
kind: "deny",
|
|
@@ -82370,7 +82369,7 @@ function evaluateDoneTransition(input) {
|
|
|
82370
82369
|
message: [
|
|
82371
82370
|
`Cannot transition task ${wbs} to done: verdict artifact is unusable.`,
|
|
82372
82371
|
` task: ${taskFilePath}`,
|
|
82373
|
-
` verdict: ${
|
|
82372
|
+
` verdict: ${verdictPath} (present but rejected)`,
|
|
82374
82373
|
` reason: ${readError}`,
|
|
82375
82374
|
` remediation: fix the artifact (re-run \`/sp:dev-verify ${wbs}\` until PASS), ` + `or override with \`spur task update ${wbs} done --force-done --reason "<why>"\`.`
|
|
82376
82375
|
].join(`
|
|
@@ -82383,7 +82382,7 @@ function evaluateDoneTransition(input) {
|
|
|
82383
82382
|
message: [
|
|
82384
82383
|
`Cannot transition task ${wbs} to done: missing verify verdict artifact.`,
|
|
82385
82384
|
` task: ${taskFilePath}`,
|
|
82386
|
-
` verdict: ${
|
|
82385
|
+
` verdict: ${verdictPath} (not found)`,
|
|
82387
82386
|
` remediation: re-run \`/sp:dev-verify ${wbs}\` until PASS (writes the artifact), ` + `or override with \`spur task update ${wbs} done --force-done --reason "<why>"\`.`
|
|
82388
82387
|
].join(`
|
|
82389
82388
|
`)
|
|
@@ -82397,7 +82396,6 @@ function evaluateDoneTransition(input) {
|
|
|
82397
82396
|
if (effective === "PASS") {
|
|
82398
82397
|
return { kind: "allow", reason: "pass" };
|
|
82399
82398
|
}
|
|
82400
|
-
const verdictPath = `.spur/run/${wbs}-verdict.json`;
|
|
82401
82399
|
const inconsistency = artifact.verdict !== computed ? { stored: artifact.verdict, computed } : undefined;
|
|
82402
82400
|
return {
|
|
82403
82401
|
kind: "deny",
|
|
@@ -84930,6 +84928,7 @@ function hasSolutionFileLineCitation(body) {
|
|
|
84930
84928
|
var TESTING_VERDICT_STUB_RE, EXTERNAL_EVIDENCE_RE, GATE_LANGUAGE_SECTIONS, TaskCheckService;
|
|
84931
84929
|
var init_task_check = __esm(() => {
|
|
84932
84930
|
init_src2();
|
|
84931
|
+
init_done_transition_guard();
|
|
84933
84932
|
init_feature_check();
|
|
84934
84933
|
init_planning_check_base();
|
|
84935
84934
|
init_structural_repair();
|
|
@@ -85601,12 +85600,14 @@ var init_task_check = __esm(() => {
|
|
|
85601
85600
|
exists2 = false;
|
|
85602
85601
|
}
|
|
85603
85602
|
if (!exists2) {
|
|
85603
|
+
const scoped = /^(@[^/\s]+\/[^/\s]+)\/(.+)$/.exec(cite.path);
|
|
85604
|
+
const detail = scoped ? `external package path \u2014 rewrite in the frozen external-evidence form: Evidence: ${scoped[1]} \`${scoped[2]}\` line ${cite.startLine}${cite.endLine ? `-${cite.endLine}` : ""} (origin and line number outside the backticks; task 0584 / ADR-062)` : `file not found at ${cite.path} (from project root). Point the citation at the code that implements this row, or move it to the external-evidence form if the file is not in this repo.`;
|
|
85604
85605
|
findings.push({
|
|
85605
85606
|
layer: "L4",
|
|
85606
85607
|
code: FINDING_CODES.L4_ANCHOR_UNRESOLVED,
|
|
85607
85608
|
severity: resolutionSeverity,
|
|
85608
85609
|
section,
|
|
85609
|
-
message: `Unresolved line anchor \`${cite.raw}\` \u2014
|
|
85610
|
+
message: `Unresolved line anchor \`${cite.raw}\` \u2014 ${detail}`
|
|
85610
85611
|
});
|
|
85611
85612
|
reported++;
|
|
85612
85613
|
continue;
|
|
@@ -85773,8 +85774,21 @@ var init_task_check = __esm(() => {
|
|
|
85773
85774
|
async checkVerdictArtifact(wbs, tasksDir, findings) {
|
|
85774
85775
|
const projectRoot = resolveProjectRootFromTasksDir(tasksDir);
|
|
85775
85776
|
const runDir = join25(projectRoot, ".spur", "run");
|
|
85776
|
-
const
|
|
85777
|
-
const
|
|
85777
|
+
const loaded = await readVerdictArtifact(this.fs, runDir, wbs);
|
|
85778
|
+
const verdictPath = loaded.path;
|
|
85779
|
+
if (loaded.readError) {
|
|
85780
|
+
findings.push({
|
|
85781
|
+
layer: "L4",
|
|
85782
|
+
code: FINDING_CODES.L4_MALFORMED_VERDICT_ARTIFACT,
|
|
85783
|
+
severity: "error",
|
|
85784
|
+
section: "Testing",
|
|
85785
|
+
message: `Verdict artifact at ${verdictPath} is unusable: ${loaded.readError}`
|
|
85786
|
+
});
|
|
85787
|
+
return;
|
|
85788
|
+
}
|
|
85789
|
+
if (loaded.artifact === undefined)
|
|
85790
|
+
return;
|
|
85791
|
+
const outcome = parseVerifyVerdict(JSON.stringify(loaded.artifact), wbs);
|
|
85778
85792
|
if (outcome.kind === "missing")
|
|
85779
85793
|
return;
|
|
85780
85794
|
if (outcome.kind === "malformed") {
|
|
@@ -91575,7 +91589,6 @@ async function deriveVerifiedOutcome(deps, window2 = {}) {
|
|
|
91575
91589
|
}
|
|
91576
91590
|
async function deriveTaskInput(deps, wbs, linkedRuns) {
|
|
91577
91591
|
const runCompleted = (r) => r.status === "done" || r.status === "completed";
|
|
91578
|
-
const supersedingFailedRun = linkedRuns.some((r) => r.status === "failed" || r.status === "cancelled");
|
|
91579
91592
|
let done = false;
|
|
91580
91593
|
let forcedDone = false;
|
|
91581
91594
|
let sectionVerdictPresent = false;
|
|
@@ -91592,8 +91605,9 @@ async function deriveTaskInput(deps, wbs, linkedRuns) {
|
|
|
91592
91605
|
raw = null;
|
|
91593
91606
|
}
|
|
91594
91607
|
if (raw !== null) {
|
|
91595
|
-
|
|
91596
|
-
|
|
91608
|
+
const frontmatter = MarkdownDocument.parse(raw, "task").frontmatterData ?? {};
|
|
91609
|
+
forcedDone = frontmatter.done_forced === true;
|
|
91610
|
+
let historyStatus;
|
|
91597
91611
|
sectionVerdictPresent = parseVerdictLine(raw.split(`
|
|
91598
91612
|
`)) !== null;
|
|
91599
91613
|
for (const line of raw.split(`
|
|
@@ -91601,6 +91615,7 @@ async function deriveTaskInput(deps, wbs, linkedRuns) {
|
|
|
91601
91615
|
const entry = parseHistoryLine(line, "task", wbs);
|
|
91602
91616
|
if (!entry)
|
|
91603
91617
|
continue;
|
|
91618
|
+
historyStatus = entry.to;
|
|
91604
91619
|
if (entry.to === "wip" && firstWipAt === null)
|
|
91605
91620
|
firstWipAt = entry.timestamp;
|
|
91606
91621
|
if (entry.to === "done") {
|
|
@@ -91610,7 +91625,7 @@ async function deriveTaskInput(deps, wbs, linkedRuns) {
|
|
|
91610
91625
|
if (reachedDone && entry.from === "done")
|
|
91611
91626
|
reopened = true;
|
|
91612
91627
|
}
|
|
91613
|
-
done =
|
|
91628
|
+
done = String(frontmatter.status ?? historyStatus ?? "").toLowerCase() === "done";
|
|
91614
91629
|
}
|
|
91615
91630
|
}
|
|
91616
91631
|
let verdictPresent = false;
|
|
@@ -91623,7 +91638,8 @@ async function deriveTaskInput(deps, wbs, linkedRuns) {
|
|
|
91623
91638
|
const verdict = verdictRead.artifact;
|
|
91624
91639
|
if (verdict) {
|
|
91625
91640
|
verdictPresent = typeof verdict.verdict === "string";
|
|
91626
|
-
|
|
91641
|
+
const parsed = parseVerifyVerdict(JSON.stringify(verdict), wbs);
|
|
91642
|
+
passVerdict = parsed.kind === "valid" && parsed.verdict.verdict === "PASS" && parsed.verdict.requirements.length + parsed.verdict.acceptanceCriteria.length > 0 && verdictRead.artifact !== undefined && computeAggregate(verdictRead.artifact) === "PASS";
|
|
91627
91643
|
const digest = typeof verdict.proof?.digest === "string" ? verdict.proof.digest : verdict.proofDigest;
|
|
91628
91644
|
proofDigestPresent = typeof digest === "string" && digest.length > 0;
|
|
91629
91645
|
if (typeof verdict.proof?.runId === "string" && verdict.proof.runId.length > 0) {
|
|
@@ -91633,7 +91649,9 @@ async function deriveTaskInput(deps, wbs, linkedRuns) {
|
|
|
91633
91649
|
boundDefinitionDigest = verdict.proof.definitionDigest;
|
|
91634
91650
|
}
|
|
91635
91651
|
}
|
|
91636
|
-
const
|
|
91652
|
+
const certifyingRun = boundRunId !== null ? linkedRuns.find((r) => r.runId === boundRunId && runCompleted(r) && (boundDefinitionDigest === null || r.definitionDigest !== null && r.definitionDigest === boundDefinitionDigest)) : linkedRuns.find(runCompleted);
|
|
91653
|
+
const certifyingRunCompleted = certifyingRun !== undefined;
|
|
91654
|
+
const supersedingFailedRun = linkedRuns.some((r) => (r.status === "failed" || r.status === "cancelled") && (certifyingRun === undefined || Date.parse(r.startedAt ?? "") > Date.parse(certifyingRun.completedAt ?? "")));
|
|
91637
91655
|
if (passVerdict) {
|
|
91638
91656
|
let tokens = 0;
|
|
91639
91657
|
let any2 = false;
|
|
@@ -91673,6 +91691,7 @@ var init_verified_outcome = __esm(() => {
|
|
|
91673
91691
|
init_src2();
|
|
91674
91692
|
init_done_transition_guard();
|
|
91675
91693
|
init_task_record();
|
|
91694
|
+
init_verify_verdict();
|
|
91676
91695
|
});
|
|
91677
91696
|
|
|
91678
91697
|
// ../../packages/app/src/services/history-service.ts
|
|
@@ -92595,9 +92614,9 @@ class WorkflowActionTraceWriter {
|
|
|
92595
92614
|
}
|
|
92596
92615
|
await this.inner.finalizeRun(runId, status, completedAt ?? new Date().toISOString(), undefined, reason === undefined ? undefined : classifyTerminalReason({ status, engineReason: reason, errorText: reason }));
|
|
92597
92616
|
if (this.db === undefined)
|
|
92598
|
-
return { ok: true };
|
|
92617
|
+
return { ok: true, startedAt: existing.started_at };
|
|
92599
92618
|
const rows = await new ActionRunDao(this.db).actionRowsByRunId(runId);
|
|
92600
|
-
return { ok: true, actionRows: rows.length };
|
|
92619
|
+
return { ok: true, actionRows: rows.length, startedAt: existing.started_at };
|
|
92601
92620
|
}
|
|
92602
92621
|
async guard(operation, context4, emit) {
|
|
92603
92622
|
try {
|
|
@@ -95858,6 +95877,14 @@ class ProofFingerprintActionRunner {
|
|
|
95858
95877
|
this.observabilityBus = observabilityBus;
|
|
95859
95878
|
}
|
|
95860
95879
|
async execute(options, context4) {
|
|
95880
|
+
const accepted = ACCEPTED_OPTION_KEYS;
|
|
95881
|
+
const unexpected = Object.keys(options).filter((key2) => !accepted.includes(key2)).sort();
|
|
95882
|
+
if (unexpected.length > 0) {
|
|
95883
|
+
return {
|
|
95884
|
+
ok: false,
|
|
95885
|
+
error: `${KIND11}: unexpected option(s): ${unexpected.join(", ")} \u2014 accepted keys: ` + `${[...accepted].sort().join(", ")}`
|
|
95886
|
+
};
|
|
95887
|
+
}
|
|
95861
95888
|
const varName = options.var;
|
|
95862
95889
|
if (typeof varName !== "string" || varName === "") {
|
|
95863
95890
|
return { ok: false, error: `${KIND11}: var is required` };
|
|
@@ -95865,6 +95892,12 @@ class ProofFingerprintActionRunner {
|
|
|
95865
95892
|
if (!VAR_NAME_RE.test(varName)) {
|
|
95866
95893
|
return { ok: false, error: `${KIND11}: var name must match ${VAR_NAME_RE}, got "${varName}"` };
|
|
95867
95894
|
}
|
|
95895
|
+
if (options.expect !== undefined && typeof options.expect !== "string") {
|
|
95896
|
+
return {
|
|
95897
|
+
ok: false,
|
|
95898
|
+
error: `${KIND11}: expect must be a string when supplied (got ${typeof options.expect})`
|
|
95899
|
+
};
|
|
95900
|
+
}
|
|
95868
95901
|
const inputs = await readProofInputContents(this.fileSystem, context4.workdir ?? ".", options);
|
|
95869
95902
|
if (!inputs.ok) {
|
|
95870
95903
|
return { ok: false, error: `${KIND11}: ${inputs.error}` };
|
|
@@ -95925,11 +95958,12 @@ class ProofFingerprintActionRunner {
|
|
|
95925
95958
|
};
|
|
95926
95959
|
}
|
|
95927
95960
|
}
|
|
95928
|
-
var KIND11 = "proof.fingerprint", VAR_NAME_RE;
|
|
95961
|
+
var KIND11 = "proof.fingerprint", VAR_NAME_RE, ACCEPTED_OPTION_KEYS;
|
|
95929
95962
|
var init_proof_fingerprint = __esm(() => {
|
|
95930
95963
|
init_proof_input_fingerprint();
|
|
95931
95964
|
init_tripwire();
|
|
95932
95965
|
VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
95966
|
+
ACCEPTED_OPTION_KEYS = ["expect", "featureFile", "taskFile", "var"];
|
|
95933
95967
|
});
|
|
95934
95968
|
|
|
95935
95969
|
// ../../packages/app/src/workflow/actions/response-validate.ts
|
|
@@ -98000,12 +98034,12 @@ class WorkflowAppService {
|
|
|
98000
98034
|
if (!before) {
|
|
98001
98035
|
return { runId, finalized: false, status: "not_found", killed: false };
|
|
98002
98036
|
}
|
|
98003
|
-
const isNonTerminal = before.status === "running" || before.status === "pending";
|
|
98037
|
+
const isNonTerminal = before.status === "running" || before.status === "pending" || before.status === "paused" || before.status === "interrupted";
|
|
98004
98038
|
const pid = isNonTerminal ? await dao2.getPid(runId) : null;
|
|
98005
98039
|
const killed = pid != null ? signalSubprocess(pid) : false;
|
|
98006
|
-
await dao2.
|
|
98040
|
+
await dao2.cancelRun(runId);
|
|
98007
98041
|
const after = await dao2.traceRowById(runId);
|
|
98008
|
-
const finalized = after?.status === "failed" &&
|
|
98042
|
+
const finalized = after?.status === "failed" && isNonTerminal;
|
|
98009
98043
|
return { runId, finalized, status: after?.status ?? "not_found", killed };
|
|
98010
98044
|
}
|
|
98011
98045
|
async continuePaused(runId, opts) {
|
|
@@ -99021,6 +99055,7 @@ async function persistWorktreeRuns(input) {
|
|
|
99021
99055
|
evidenceCopies.push({ source: source2, target, bytes });
|
|
99022
99056
|
}
|
|
99023
99057
|
const citedNames = new Set;
|
|
99058
|
+
const citedDirSkips = new Set;
|
|
99024
99059
|
for (const taskFile of input.taskFiles ?? []) {
|
|
99025
99060
|
let content;
|
|
99026
99061
|
try {
|
|
@@ -99030,7 +99065,13 @@ async function persistWorktreeRuns(input) {
|
|
|
99030
99065
|
}
|
|
99031
99066
|
for (const match of content.matchAll(RUN_CITATION_RE)) {
|
|
99032
99067
|
const name = asLiteralRunFileName(match[1] ?? "");
|
|
99033
|
-
if (name === undefined
|
|
99068
|
+
if (name === undefined)
|
|
99069
|
+
continue;
|
|
99070
|
+
if (match[2] !== undefined) {
|
|
99071
|
+
citedDirSkips.add(name);
|
|
99072
|
+
continue;
|
|
99073
|
+
}
|
|
99074
|
+
if (citedNames.has(name))
|
|
99034
99075
|
continue;
|
|
99035
99076
|
if (citedNames.size >= MAX_CITED_RUN_FILES) {
|
|
99036
99077
|
throw new Error(`persist-out: merged task files cite more than ${MAX_CITED_RUN_FILES} distinct ` + ".spur/run/ files \u2014 over the fixed citation cap (0984 R3); split the batch or prune the citations");
|
|
@@ -99077,6 +99118,8 @@ async function persistWorktreeRuns(input) {
|
|
|
99077
99118
|
}
|
|
99078
99119
|
const citedCopies = [];
|
|
99079
99120
|
const citedSkips = [];
|
|
99121
|
+
for (const name of citedDirSkips)
|
|
99122
|
+
citedSkips.push({ id: name, reason: `cited-directory:${name}` });
|
|
99080
99123
|
for (const name of citedNames) {
|
|
99081
99124
|
const scratchPath = join36(fromRunDir, name);
|
|
99082
99125
|
const durablePath = join36(fromRecordsDir, name);
|
|
@@ -99467,6 +99510,47 @@ function writeInlineRunOutcome(runId, outcome) {
|
|
|
99467
99510
|
closeSync4(headerFd);
|
|
99468
99511
|
}
|
|
99469
99512
|
}
|
|
99513
|
+
function projectInlineRunClose(runId, status, context4) {
|
|
99514
|
+
try {
|
|
99515
|
+
const runDir = runStoragePaths(process.cwd()).recordsDir;
|
|
99516
|
+
if (!existsSync9(runDir))
|
|
99517
|
+
mkdirSync9(runDir, { recursive: true });
|
|
99518
|
+
const statePath = join36(runDir, `${runId}.state.json`);
|
|
99519
|
+
let prior = {};
|
|
99520
|
+
try {
|
|
99521
|
+
const parsed = JSON.parse(readFileSync13(statePath, "utf8"));
|
|
99522
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
99523
|
+
prior = parsed;
|
|
99524
|
+
}
|
|
99525
|
+
} catch {}
|
|
99526
|
+
const at2 = new Date().toISOString();
|
|
99527
|
+
const state = {
|
|
99528
|
+
schemaVersion: 1,
|
|
99529
|
+
...prior,
|
|
99530
|
+
runId,
|
|
99531
|
+
status,
|
|
99532
|
+
ok: true,
|
|
99533
|
+
startedAt: typeof prior.startedAt === "string" ? prior.startedAt : context4?.startedAt ?? at2,
|
|
99534
|
+
updatedAt: at2
|
|
99535
|
+
};
|
|
99536
|
+
delete state.error;
|
|
99537
|
+
const temp = `${statePath}.tmp`;
|
|
99538
|
+
try {
|
|
99539
|
+
writeFileSync8(temp, `${JSON.stringify(state, null, 4)}
|
|
99540
|
+
`);
|
|
99541
|
+
renameSync7(temp, statePath);
|
|
99542
|
+
} catch (error51) {
|
|
99543
|
+
try {
|
|
99544
|
+
unlinkSync5(temp);
|
|
99545
|
+
} catch {}
|
|
99546
|
+
throw error51;
|
|
99547
|
+
}
|
|
99548
|
+
return;
|
|
99549
|
+
} catch (error51) {
|
|
99550
|
+
const detail = error51 instanceof Error ? error51.message : String(error51);
|
|
99551
|
+
return `failed to project the committed ${status} status into the run-record state file for run ${runId}: ${detail} (the run row is committed; replay the same close to repair)`;
|
|
99552
|
+
}
|
|
99553
|
+
}
|
|
99470
99554
|
function isInlineRunCloseStatus(status) {
|
|
99471
99555
|
return CLOSE_STATUSES.has(status);
|
|
99472
99556
|
}
|
|
@@ -99493,6 +99577,20 @@ function appendInlineRunLogLine(runId, detail) {
|
|
|
99493
99577
|
}
|
|
99494
99578
|
async function runInlineRunTrace(input) {
|
|
99495
99579
|
const operation = input.close ? "run.close" : "action.finish";
|
|
99580
|
+
if (input.close) {
|
|
99581
|
+
const invalidReason = (error51) => {
|
|
99582
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51, code: "INVALID_CLOSE_REASON" })}
|
|
99583
|
+
`);
|
|
99584
|
+
return 1;
|
|
99585
|
+
};
|
|
99586
|
+
if (input.reason !== undefined && !isTerminalReason(input.reason)) {
|
|
99587
|
+
return invalidReason(`close reason "${input.reason}" is not a terminal-reason enum value; pass one of: ${TERMINAL_REASONS.join(", ")}`);
|
|
99588
|
+
}
|
|
99589
|
+
if (input.status === "failed" && input.reason === undefined) {
|
|
99590
|
+
return invalidReason(`close status failed requires an explicit reason from the terminal-reason enum (0937 R2); pass one of: ${TERMINAL_REASONS.join(", ")}`);
|
|
99591
|
+
}
|
|
99592
|
+
}
|
|
99593
|
+
const closeReason = input.close ? input.reason ?? (input.status === "paused" ? "paused-operator" : "done") : undefined;
|
|
99496
99594
|
const fail = (error51) => {
|
|
99497
99595
|
appendInlineRunLogLine(input.runId, `trace-emission-failed operation=${operation} run=${input.runId}${input.node === "" ? "" : ` node=${input.node}`}${input.kind === "" ? "" : ` kind=${input.kind}`}: ${error51}`);
|
|
99498
99596
|
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51 })}
|
|
@@ -99506,7 +99604,7 @@ async function runInlineRunTrace(input) {
|
|
|
99506
99604
|
const detail = failure;
|
|
99507
99605
|
appendInlineRunLogLine(input.runId, `trace-emission-failed operation=${detail.operation ?? operation} run=${input.runId}: ${detail.error ?? "unknown error"}`);
|
|
99508
99606
|
});
|
|
99509
|
-
const result = input.close ? await writer.closeRun(input.runId, input.status, undefined,
|
|
99607
|
+
const result = input.close ? await writer.closeRun(input.runId, input.status, undefined, closeReason) : await writer.recordAction({
|
|
99510
99608
|
runId: input.runId,
|
|
99511
99609
|
node: input.node,
|
|
99512
99610
|
kind: input.kind,
|
|
@@ -99518,14 +99616,24 @@ async function runInlineRunTrace(input) {
|
|
|
99518
99616
|
const failure = result.failure;
|
|
99519
99617
|
return fail(failure.error ?? "unknown trace emission failure");
|
|
99520
99618
|
}
|
|
99619
|
+
const stateError = input.close ? projectInlineRunClose(input.runId, input.status, {
|
|
99620
|
+
startedAt: typeof result.startedAt === "string" ? result.startedAt : undefined
|
|
99621
|
+
}) : undefined;
|
|
99521
99622
|
if (input.close && input.status === "done" && result.actionRows === 0) {
|
|
99522
|
-
const error51 = `run ${input.runId} closed done with zero action_runs rows; emit --action/--actions-file during the run (no backfill); see inline-pipeline-driver.md#structured-trace-emission-adr-117-task-0868`;
|
|
99623
|
+
const error51 = `run ${input.runId} closed done with zero action_runs rows; emit --action/--actions-file during the run (no backfill); see inline-pipeline-driver.md#structured-trace-emission-adr-117-task-0868${stateError !== undefined ? `; run-record state projection also failed: ${stateError}` : ""}`;
|
|
99523
99624
|
appendInlineRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${error51}`);
|
|
99524
99625
|
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: error51, code: "NO_ACTION_ROWS", actionRows: 0 })}
|
|
99525
99626
|
`);
|
|
99526
99627
|
return 1;
|
|
99527
99628
|
}
|
|
99528
|
-
|
|
99629
|
+
if (input.close && stateError !== undefined) {
|
|
99630
|
+
appendInlineRunLogLine(input.runId, `trace-close-failed run=${input.runId}: ${stateError}`);
|
|
99631
|
+
process.stdout.write(`${JSON.stringify({ ok: false, runId: input.runId, error: stateError, code: "RUN_RECORD_STATE_FAILED" })}
|
|
99632
|
+
`);
|
|
99633
|
+
return 1;
|
|
99634
|
+
}
|
|
99635
|
+
const { startedAt: _threaded, ...stdoutResult } = result;
|
|
99636
|
+
process.stdout.write(`${JSON.stringify({ ...stdoutResult, runId: input.runId })}
|
|
99529
99637
|
`);
|
|
99530
99638
|
return 0;
|
|
99531
99639
|
} catch (error51) {
|
|
@@ -99680,8 +99788,10 @@ async function runInlineRunPersistOut(input) {
|
|
|
99680
99788
|
toWorkdir: process.cwd(),
|
|
99681
99789
|
...input.taskFiles.length > 0 ? { taskFiles: input.taskFiles } : {}
|
|
99682
99790
|
});
|
|
99683
|
-
|
|
99684
|
-
|
|
99791
|
+
const keyConflicts = result.skipped.filter((skip) => skip.reason === "external-key-conflict");
|
|
99792
|
+
if (keyConflicts.length > 0 || result.skipped.some((skip) => skip.reason.startsWith("record-conflict:"))) {
|
|
99793
|
+
const error51 = keyConflicts.length > 0 ? `persist-out: external-key conflict for source runs ${keyConflicts.map((skip) => skip.id).join(", ")}; retain the source worktree and reconcile provenance before teardown` : "persist-out: unresolved retained record conflicts; retain the worktree and reconcile copies before teardown";
|
|
99794
|
+
process.stdout.write(`${JSON.stringify({ ...result, ok: false, error: error51 })}
|
|
99685
99795
|
`);
|
|
99686
99796
|
return 1;
|
|
99687
99797
|
}
|
|
@@ -99751,7 +99861,7 @@ var init_inline_run_setup = __esm(async () => {
|
|
|
99751
99861
|
} = await import("fs"));
|
|
99752
99862
|
({ lstat: lstat2, mkdir: mkdir3, readdir: readdir2, readFile: readFile4 } = await import("fs/promises"));
|
|
99753
99863
|
SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
99754
|
-
RUN_CITATION_RE = /(?<![\w~:-]|[\w~:-]\/)\.spur\/run\/([A-Za-z0-9][A-Za-z0-9._*?<>{}|,\u2026-]*)
|
|
99864
|
+
RUN_CITATION_RE = /(?<![\w~:-]|[\w~:-]\/)\.spur\/run\/([A-Za-z0-9][A-Za-z0-9._*?<>{}|,\u2026-]*)(\/[^\s`]*)?/g;
|
|
99755
99865
|
CLOSE_STATUSES = new Set(["done", "failed", "paused"]);
|
|
99756
99866
|
ACTION_STATUSES = new Set(["done", "failed"]);
|
|
99757
99867
|
});
|
|
@@ -100017,11 +100127,13 @@ class PlanningWriteService {
|
|
|
100017
100127
|
lifecycle;
|
|
100018
100128
|
emitter;
|
|
100019
100129
|
projectName;
|
|
100130
|
+
onTransitionCommitted;
|
|
100020
100131
|
constructor(opts) {
|
|
100021
100132
|
this.fs = opts.fs;
|
|
100022
100133
|
this.lifecycle = opts.lifecycle ?? new SchemaLifecyclePort;
|
|
100023
100134
|
this.emitter = opts.emitter ?? new NoopEventEmitter;
|
|
100024
100135
|
this.projectName = opts.projectName ?? "spur";
|
|
100136
|
+
this.onTransitionCommitted = opts.onTransitionCommitted;
|
|
100025
100137
|
}
|
|
100026
100138
|
async create(ref, content) {
|
|
100027
100139
|
return this.executePipeline(ref, { kind: "create", content });
|
|
@@ -100035,8 +100147,8 @@ class PlanningWriteService {
|
|
|
100035
100147
|
lock.release();
|
|
100036
100148
|
}
|
|
100037
100149
|
}
|
|
100038
|
-
async updateSection(ref, sectionName, body) {
|
|
100039
|
-
return this.executePipeline(ref, { kind: "updateSection", sectionName, sectionBody: body });
|
|
100150
|
+
async updateSection(ref, sectionName, body, append = false) {
|
|
100151
|
+
return this.executePipeline(ref, { kind: "updateSection", sectionName, sectionBody: body, append });
|
|
100040
100152
|
}
|
|
100041
100153
|
async updateBody(ref, body) {
|
|
100042
100154
|
return this.executePipeline(ref, { kind: "updateBody", body });
|
|
@@ -100119,6 +100231,15 @@ class PlanningWriteService {
|
|
|
100119
100231
|
} : {}
|
|
100120
100232
|
};
|
|
100121
100233
|
await this.emitter.emit(event);
|
|
100234
|
+
let bookkeepingError;
|
|
100235
|
+
const committedHook = this.onTransitionCommitted;
|
|
100236
|
+
if (statusChanged && committedHook !== undefined && fromStatus !== undefined && toStatus !== undefined) {
|
|
100237
|
+
try {
|
|
100238
|
+
await committedHook(ref, fromStatus, toStatus);
|
|
100239
|
+
} catch (err) {
|
|
100240
|
+
bookkeepingError = err instanceof Error ? err.message : String(err);
|
|
100241
|
+
}
|
|
100242
|
+
}
|
|
100122
100243
|
const warnings = doc2.demotedHeadings.map((line) => `Demoted same-level heading one level deeper in section body (would become a phantom section): "${line}". ` + "Use bullet lists, tables, or **bold** labels for sub-structure instead.");
|
|
100123
100244
|
for (const dup of doc2.duplicateSectionNames) {
|
|
100124
100245
|
warnings.push(`Dropped duplicate "${dup}" section (appeared more than once in the file \u2014 ` + "likely a copy-paste or double-write error). The first occurrence was kept.");
|
|
@@ -100128,7 +100249,8 @@ class PlanningWriteService {
|
|
|
100128
100249
|
eventName,
|
|
100129
100250
|
...fromStatus !== undefined ? { fromStatus } : {},
|
|
100130
100251
|
...toStatus !== undefined ? { toStatus } : {},
|
|
100131
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
100252
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
100253
|
+
...bookkeepingError !== undefined ? { bookkeepingError } : {}
|
|
100132
100254
|
};
|
|
100133
100255
|
}
|
|
100134
100256
|
}
|
|
@@ -100151,9 +100273,21 @@ function applyMutation(doc2, mutation) {
|
|
|
100151
100273
|
if (mutation.sectionName !== undefined && mutation.sectionBody !== undefined) {
|
|
100152
100274
|
const body = mutation.sectionName.toLowerCase() === "acceptance criteria" ? normalizeAcFence(mutation.sectionBody) : mutation.sectionBody;
|
|
100153
100275
|
if (mutation.sectionName.toLowerCase() === "q&a") {
|
|
100276
|
+
if (body.trimStart().startsWith(QA_REPLACE_MARKER)) {
|
|
100277
|
+
doc2.replaceSection("Q&A", body.trimStart().slice(QA_REPLACE_MARKER.length).trimStart());
|
|
100278
|
+
break;
|
|
100279
|
+
}
|
|
100280
|
+
if (mutation.append) {
|
|
100281
|
+
appendSectionBody(doc2, "Q&A", body);
|
|
100282
|
+
break;
|
|
100283
|
+
}
|
|
100154
100284
|
appendQaEntry(doc2, body);
|
|
100155
100285
|
break;
|
|
100156
100286
|
}
|
|
100287
|
+
if (mutation.append) {
|
|
100288
|
+
appendSectionBody(doc2, mutation.sectionName, body);
|
|
100289
|
+
break;
|
|
100290
|
+
}
|
|
100157
100291
|
doc2.replaceSection(mutation.sectionName, body);
|
|
100158
100292
|
}
|
|
100159
100293
|
break;
|
|
@@ -100185,9 +100319,8 @@ ${line}
|
|
|
100185
100319
|
doc2.replaceSection("History", updated);
|
|
100186
100320
|
}
|
|
100187
100321
|
function appendQaEntry(doc2, body) {
|
|
100188
|
-
|
|
100189
|
-
|
|
100190
|
-
doc2.replaceSection("Q&A", body.trimStart().slice(REPLACE_MARKER.length).trimStart());
|
|
100322
|
+
if (body.trimStart().startsWith(QA_REPLACE_MARKER)) {
|
|
100323
|
+
doc2.replaceSection("Q&A", body.trimStart().slice(QA_REPLACE_MARKER.length).trimStart());
|
|
100191
100324
|
return;
|
|
100192
100325
|
}
|
|
100193
100326
|
const existing = doc2.getSection("Q&A") ?? "";
|
|
@@ -100200,6 +100333,15 @@ ${body.trim()}
|
|
|
100200
100333
|
${entry}` : entry;
|
|
100201
100334
|
doc2.replaceSection("Q&A", updated);
|
|
100202
100335
|
}
|
|
100336
|
+
function appendSectionBody(doc2, sectionName, body) {
|
|
100337
|
+
const existing = doc2.getSection(sectionName) ?? "";
|
|
100338
|
+
const updated = existing.trim().length > 0 ? `${existing.trimEnd()}
|
|
100339
|
+
|
|
100340
|
+
${body.trim()}
|
|
100341
|
+
` : `${body.trim()}
|
|
100342
|
+
`;
|
|
100343
|
+
doc2.replaceSection(sectionName, updated);
|
|
100344
|
+
}
|
|
100203
100345
|
function resolveEventName(kind, domain2, statusChanged) {
|
|
100204
100346
|
if (kind === "create") {
|
|
100205
100347
|
return domain2 === "task" ? "task.created" : "feature.created";
|
|
@@ -100219,6 +100361,7 @@ function normalizeStatusForDomain(status, domain2) {
|
|
|
100219
100361
|
return status;
|
|
100220
100362
|
}
|
|
100221
100363
|
}
|
|
100364
|
+
var QA_REPLACE_MARKER = "<!-- qa:replace -->";
|
|
100222
100365
|
var init_planning_write_service = __esm(() => {
|
|
100223
100366
|
init_src2();
|
|
100224
100367
|
init_errors8();
|
|
@@ -103352,6 +103495,221 @@ var init_task_scaffold = __esm(() => {
|
|
|
103352
103495
|
init_task_locator();
|
|
103353
103496
|
});
|
|
103354
103497
|
|
|
103498
|
+
// ../../packages/app/src/workflow/lifecycle-adapter.ts
|
|
103499
|
+
async function reconcileExistingLifecycleRow(getDb, profile, entityId2, toStatus) {
|
|
103500
|
+
if (toStatus !== "done" && toStatus !== "cancelled")
|
|
103501
|
+
return false;
|
|
103502
|
+
const persistence2 = new DbWorkflowPersistenceAdapter(await getDb());
|
|
103503
|
+
const run = await persistence2.findRunByKey(profile.workflowName, `${profile.entityPrefix}:${entityId2}`);
|
|
103504
|
+
if (run === undefined)
|
|
103505
|
+
return false;
|
|
103506
|
+
const engineStatus = toStatus === "cancelled" ? "failed" : "done";
|
|
103507
|
+
if (run.status === engineStatus && run.terminal_reason === toStatus)
|
|
103508
|
+
return false;
|
|
103509
|
+
await persistence2.finalizeRun(run.id, engineStatus, new Date().toISOString(), undefined, toStatus);
|
|
103510
|
+
return true;
|
|
103511
|
+
}
|
|
103512
|
+
|
|
103513
|
+
class LifecycleAdapter {
|
|
103514
|
+
opts;
|
|
103515
|
+
workflowCache;
|
|
103516
|
+
constructor(opts) {
|
|
103517
|
+
this.opts = opts;
|
|
103518
|
+
}
|
|
103519
|
+
async requestTransition(ref, currentStatus, to) {
|
|
103520
|
+
const { profile } = this.opts;
|
|
103521
|
+
const db2 = await this.opts.getDb();
|
|
103522
|
+
const host = createDefaultWorkflowEngineHost();
|
|
103523
|
+
host.registerGuard(new EnvShellGuardRunner(new NodeProcessExecutor3), "builtin");
|
|
103524
|
+
const persistence2 = withActionTrace(new DbWorkflowPersistenceAdapter(db2), createRunLogTraceFailureRecorder(this.opts.cwd));
|
|
103525
|
+
const svc = new WorkflowService(host, persistence2);
|
|
103526
|
+
const workflow = this.bindGuardVar(await this.loadWorkflow(), ref.id);
|
|
103527
|
+
const externalKey = `${profile.entityPrefix}:${ref.id}`;
|
|
103528
|
+
const now2 = new Date().toISOString();
|
|
103529
|
+
if (to === "done" && profile.entityPrefix === "task") {
|
|
103530
|
+
const links = await this.opts.taskRunLinkDao(db2).listByWbs(ref.id, 20);
|
|
103531
|
+
const hasPipelineRun = links.some((l) => l.kind === "pipeline");
|
|
103532
|
+
const hasPriorBypass = links.some((l) => l.kind === "provenance_bypass");
|
|
103533
|
+
if (!hasPipelineRun && !hasPriorBypass) {
|
|
103534
|
+
if (this.opts.provenanceBypass === true) {
|
|
103535
|
+
await this.opts.taskRunLinkDao(db2).insert({
|
|
103536
|
+
id: createId("trl"),
|
|
103537
|
+
wbs: ref.id,
|
|
103538
|
+
run_id: "manual",
|
|
103539
|
+
kind: "provenance_bypass",
|
|
103540
|
+
created_at: now2
|
|
103541
|
+
});
|
|
103542
|
+
} else {
|
|
103543
|
+
return {
|
|
103544
|
+
allowed: false,
|
|
103545
|
+
from: currentStatus,
|
|
103546
|
+
to,
|
|
103547
|
+
report: `No pipeline run recorded for ${ref.id}. Run the pipeline first (` + `spur workflow run task-pipeline.yaml --vars '{"wbs":"${ref.id}"}'), ` + "or pass --provenance-bypass on `spur task update` to bypass (recorded)."
|
|
103548
|
+
};
|
|
103549
|
+
}
|
|
103550
|
+
}
|
|
103551
|
+
const reviewDenial = await this.checkReviewReadyForDone(ref);
|
|
103552
|
+
if (reviewDenial !== null) {
|
|
103553
|
+
return {
|
|
103554
|
+
allowed: false,
|
|
103555
|
+
from: currentStatus,
|
|
103556
|
+
to,
|
|
103557
|
+
report: reviewDenial
|
|
103558
|
+
};
|
|
103559
|
+
}
|
|
103560
|
+
}
|
|
103561
|
+
const existing = await svc.findRunByKey(profile.workflowName, externalKey);
|
|
103562
|
+
const runId = existing?.id ?? createId("run");
|
|
103563
|
+
await svc.createOrAttachRun({
|
|
103564
|
+
id: runId,
|
|
103565
|
+
workflow_name: profile.workflowName,
|
|
103566
|
+
mode: "state-machine",
|
|
103567
|
+
status: "running",
|
|
103568
|
+
started_at: existing?.started_at ?? now2,
|
|
103569
|
+
completed_at: null,
|
|
103570
|
+
metadata_json: "{}",
|
|
103571
|
+
external_key: externalKey
|
|
103572
|
+
});
|
|
103573
|
+
if (existing === undefined) {
|
|
103574
|
+
await this.opts.taskRunLinkDao(db2).insert({
|
|
103575
|
+
id: createId("trl"),
|
|
103576
|
+
wbs: ref.id,
|
|
103577
|
+
run_id: runId,
|
|
103578
|
+
kind: profile.linkKind,
|
|
103579
|
+
created_at: now2
|
|
103580
|
+
});
|
|
103581
|
+
}
|
|
103582
|
+
await svc.reseedRun(workflow, runId, currentStatus);
|
|
103583
|
+
const result = await svc.requestTransition(workflow, runId, to, {
|
|
103584
|
+
workdir: this.opts.cwd,
|
|
103585
|
+
vars: { [profile.varKey]: ref.id, spurBin: this.opts.spurBin }
|
|
103586
|
+
});
|
|
103587
|
+
if (result.allowed) {
|
|
103588
|
+
const enterFailure = await this.runTargetEnterActions(workflow, to, runId);
|
|
103589
|
+
if (enterFailure !== null) {
|
|
103590
|
+
return { allowed: false, from: currentStatus, to, report: enterFailure };
|
|
103591
|
+
}
|
|
103592
|
+
const terminalStatus = to === "cancelled" ? "failed" : to === "done" ? "done" : null;
|
|
103593
|
+
await persistence2.finalizeRun(runId, terminalStatus ?? "running", new Date().toISOString(), undefined, to === "cancelled" ? "cancelled" : to === "done" ? "done" : undefined);
|
|
103594
|
+
return { allowed: true, from: result.fromState, to: result.toState };
|
|
103595
|
+
}
|
|
103596
|
+
return {
|
|
103597
|
+
allowed: false,
|
|
103598
|
+
from: currentStatus,
|
|
103599
|
+
to,
|
|
103600
|
+
report: this.formatDenialWithLegalPaths(result, workflow, currentStatus, ref.id)
|
|
103601
|
+
};
|
|
103602
|
+
}
|
|
103603
|
+
async runTargetEnterActions(workflow, to, runId) {
|
|
103604
|
+
if (this.opts.runEnterActions === false)
|
|
103605
|
+
return null;
|
|
103606
|
+
const actions = workflow.states.find((state) => state.id === to)?.onEnter ?? [];
|
|
103607
|
+
if (actions.length === 0)
|
|
103608
|
+
return null;
|
|
103609
|
+
const runner = new StreamingShellActionRunner(new NodeProcessExecutor3);
|
|
103610
|
+
const vars = workflow.vars ?? {};
|
|
103611
|
+
for (const [index2, action] of actions.entries()) {
|
|
103612
|
+
if (action.kind !== "shell") {
|
|
103613
|
+
return `State "${to}" onEnter[${index2}] is kind "${action.kind}"; the lifecycle caller only runs shell onEnter actions.`;
|
|
103614
|
+
}
|
|
103615
|
+
const command = action.options?.command;
|
|
103616
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
103617
|
+
return `State "${to}" onEnter[${index2}] has no shell command.`;
|
|
103618
|
+
}
|
|
103619
|
+
const result = await runner.execute({ ...action.options }, {
|
|
103620
|
+
runId,
|
|
103621
|
+
workdir: this.opts.cwd,
|
|
103622
|
+
stateOrNodeId: to,
|
|
103623
|
+
vars,
|
|
103624
|
+
env: {}
|
|
103625
|
+
});
|
|
103626
|
+
if (!result.ok) {
|
|
103627
|
+
const stderr = typeof result.data?.stderr === "string" ? result.data.stderr.trim() : "";
|
|
103628
|
+
const tail2 = stderr.length > 2000 ? stderr.slice(-2000) : stderr;
|
|
103629
|
+
const exitCode = result.data?.exitCode ?? "unknown";
|
|
103630
|
+
return [`State "${to}" onEnter shell failed (exit ${String(exitCode)}).`, result.error ?? "", tail2].filter((part) => part !== "").join(" ");
|
|
103631
|
+
}
|
|
103632
|
+
}
|
|
103633
|
+
return null;
|
|
103634
|
+
}
|
|
103635
|
+
formatDenialWithLegalPaths(result, workflow, currentStatus, id) {
|
|
103636
|
+
const base2 = this.formatDenial(result.detail, result.guardReport);
|
|
103637
|
+
if (result.reason !== "no-such-transition")
|
|
103638
|
+
return base2;
|
|
103639
|
+
const legal = workflow.transitions.filter((t) => t.from === currentStatus).map((t) => t.to);
|
|
103640
|
+
if (legal.length === 0)
|
|
103641
|
+
return base2;
|
|
103642
|
+
const command = this.opts.profile.entityPrefix === "feature" ? `spur feature sync ${id} (derives the legal hop path)` : `spur task update ${id} <legal-status> or \`spur task record ${id} --transition <legal-status>\``;
|
|
103643
|
+
return `${base2}. Legal path(s) from ${currentStatus}: ${legal.join(" \u2192 ")}. Reach them via \`${command}\`.`;
|
|
103644
|
+
}
|
|
103645
|
+
async loadWorkflow() {
|
|
103646
|
+
if (this.workflowCache === undefined) {
|
|
103647
|
+
const def = await loadWorkflowDef(this.opts.workflowPath, { validateSchema: false });
|
|
103648
|
+
if (def.kind !== "state-machine") {
|
|
103649
|
+
throw new Error(`${this.opts.profile.workflowName} workflow must be a state-machine; got "${def.kind}"`);
|
|
103650
|
+
}
|
|
103651
|
+
this.workflowCache = def;
|
|
103652
|
+
}
|
|
103653
|
+
return this.workflowCache;
|
|
103654
|
+
}
|
|
103655
|
+
bindGuardVar(workflow, id) {
|
|
103656
|
+
return {
|
|
103657
|
+
...workflow,
|
|
103658
|
+
vars: { ...workflow.vars, [this.opts.profile.varKey]: id, spurBin: this.opts.spurBin }
|
|
103659
|
+
};
|
|
103660
|
+
}
|
|
103661
|
+
async checkReviewReadyForDone(ref) {
|
|
103662
|
+
const markdown = await this.loadTaskMarkdown(ref);
|
|
103663
|
+
if (markdown === null) {
|
|
103664
|
+
return null;
|
|
103665
|
+
}
|
|
103666
|
+
const reviewBody = extractReviewSectionBody(markdown);
|
|
103667
|
+
if (reviewBody === null || !hasPopulatedPriorityTable(reviewBody)) {
|
|
103668
|
+
return `Review L3 gate failed for task ${ref.id}: ### Review must contain a populated ` + `P1\u2013P4 priority findings table before testing\u2192done (strict-core). ` + `Author the table via /sp:dev-review, then re-run the done transition.`;
|
|
103669
|
+
}
|
|
103670
|
+
return null;
|
|
103671
|
+
}
|
|
103672
|
+
async loadTaskMarkdown(ref) {
|
|
103673
|
+
if (this.opts.readTaskMarkdown) {
|
|
103674
|
+
return this.opts.readTaskMarkdown(ref);
|
|
103675
|
+
}
|
|
103676
|
+
try {
|
|
103677
|
+
const fs3 = createNodeFileSystem3(this.opts.cwd);
|
|
103678
|
+
return await fs3.readFile(ref.filePath);
|
|
103679
|
+
} catch {
|
|
103680
|
+
return null;
|
|
103681
|
+
}
|
|
103682
|
+
}
|
|
103683
|
+
formatDenial(detail, guardReport) {
|
|
103684
|
+
if (guardReport === undefined || guardReport === null)
|
|
103685
|
+
return detail;
|
|
103686
|
+
const report = typeof guardReport === "string" ? guardReport : JSON.stringify(guardReport);
|
|
103687
|
+
return `${detail} \u2014 ${report}`;
|
|
103688
|
+
}
|
|
103689
|
+
}
|
|
103690
|
+
var TASK_LIFECYCLE_PROFILE, FEATURE_LIFECYCLE_PROFILE;
|
|
103691
|
+
var init_lifecycle_adapter = __esm(() => {
|
|
103692
|
+
init_src2();
|
|
103693
|
+
init_dist8();
|
|
103694
|
+
init_dist5();
|
|
103695
|
+
init_task_check();
|
|
103696
|
+
init_action_trace();
|
|
103697
|
+
init_shell();
|
|
103698
|
+
init_shell2();
|
|
103699
|
+
TASK_LIFECYCLE_PROFILE = {
|
|
103700
|
+
linkKind: "lifecycle",
|
|
103701
|
+
workflowName: "task-lifecycle",
|
|
103702
|
+
entityPrefix: "task",
|
|
103703
|
+
varKey: "wbs"
|
|
103704
|
+
};
|
|
103705
|
+
FEATURE_LIFECYCLE_PROFILE = {
|
|
103706
|
+
linkKind: "feature-lifecycle",
|
|
103707
|
+
workflowName: "feature-lifecycle",
|
|
103708
|
+
entityPrefix: "feature",
|
|
103709
|
+
varKey: "featureId"
|
|
103710
|
+
};
|
|
103711
|
+
});
|
|
103712
|
+
|
|
103355
103713
|
// ../../packages/app/src/services/task-transition.ts
|
|
103356
103714
|
async function runTransitionCheckGate(gate, wbs, filePath, target) {
|
|
103357
103715
|
const result = await gate.service.check(filePath, wbs, {
|
|
@@ -103411,6 +103769,7 @@ async function transitionTaskGuarded(deps, input) {
|
|
|
103411
103769
|
forced: input.forceDone === true,
|
|
103412
103770
|
reason: input.reason,
|
|
103413
103771
|
artifact: loaded.artifact,
|
|
103772
|
+
verdictPath: loaded.path,
|
|
103414
103773
|
readError: loaded.readError
|
|
103415
103774
|
});
|
|
103416
103775
|
if (guardOutcome.kind === "noop") {
|
|
@@ -103918,7 +104277,7 @@ ${templateBackground}`;
|
|
|
103918
104277
|
});
|
|
103919
104278
|
return { runId, action, status: "queued" };
|
|
103920
104279
|
}
|
|
103921
|
-
async updateSection(wbs, sectionName, sourceFile) {
|
|
104280
|
+
async updateSection(wbs, sectionName, sourceFile, append = false) {
|
|
103922
104281
|
const filePath = await this.resolveTaskFile(wbs);
|
|
103923
104282
|
const raw = await this.ctx.fs.readFile(sourceFile);
|
|
103924
104283
|
const body = stripLeadingSectionHeader2(raw, sectionName);
|
|
@@ -103926,7 +104285,7 @@ ${templateBackground}`;
|
|
|
103926
104285
|
throw new SectionMutationError("invalid-solution", "Solution must contain at least one `file:line` citation");
|
|
103927
104286
|
}
|
|
103928
104287
|
const ref = { kind: "task", id: wbs, filePath, folder: this.ctx.tasksDir };
|
|
103929
|
-
let result = await this.writeService.updateSection(ref, sectionName, body);
|
|
104288
|
+
let result = await this.writeService.updateSection(ref, sectionName, body, append);
|
|
103930
104289
|
if (sectionName === "Acceptance Criteria") {
|
|
103931
104290
|
const warnings = await this.checkAcSubsetWarning(filePath, body);
|
|
103932
104291
|
if (warnings.length > 0) {
|
|
@@ -104091,6 +104450,7 @@ ${templateBackground}`;
|
|
|
104091
104450
|
}
|
|
104092
104451
|
if (current === target) {
|
|
104093
104452
|
result.transitionedTo = current;
|
|
104453
|
+
result.bookkeepingError = await this.reconcileTerminalBookkeeping(wbs, target);
|
|
104094
104454
|
} else if (target === "done") {
|
|
104095
104455
|
if (verdict.verdict !== "PASS") {
|
|
104096
104456
|
throw new GuardDeniedError(`Cannot transition task ${wbs} to done: verify verdict is ${verdict.verdict}, not PASS. ` + `Re-run the pipeline to a PASS verdict, or use ` + `\`spur task update ${wbs} done --force-done --reason "<why>"\` to override.`);
|
|
@@ -104108,6 +104468,9 @@ ${templateBackground}`;
|
|
|
104108
104468
|
const hopResult = await this.writeService.transition(ref, hop);
|
|
104109
104469
|
reached = hopResult.toStatus ?? hop;
|
|
104110
104470
|
result.transitionedTo = reached;
|
|
104471
|
+
if (hopResult.bookkeepingError !== undefined) {
|
|
104472
|
+
result.bookkeepingError = hopResult.bookkeepingError;
|
|
104473
|
+
}
|
|
104111
104474
|
if (hop === "done") {
|
|
104112
104475
|
result.closeAuditError = await reconcileDoneCloseAudit(this, wbs, {
|
|
104113
104476
|
forced: false,
|
|
@@ -104123,6 +104486,9 @@ ${templateBackground}`;
|
|
|
104123
104486
|
await this.ensurePipelineRunLink(wbs);
|
|
104124
104487
|
const transitionResult = await this.writeService.transition(ref, target);
|
|
104125
104488
|
result.transitionedTo = transitionResult.toStatus;
|
|
104489
|
+
if (transitionResult.bookkeepingError !== undefined) {
|
|
104490
|
+
result.bookkeepingError = transitionResult.bookkeepingError;
|
|
104491
|
+
}
|
|
104126
104492
|
if (target === "done") {
|
|
104127
104493
|
result.closeAuditError = await reconcileDoneCloseAudit(this, wbs, {
|
|
104128
104494
|
forced: false,
|
|
@@ -104133,6 +104499,9 @@ ${templateBackground}`;
|
|
|
104133
104499
|
} else {
|
|
104134
104500
|
const transitionResult = await this.writeService.transition(ref, target);
|
|
104135
104501
|
result.transitionedTo = transitionResult.toStatus;
|
|
104502
|
+
if (transitionResult.bookkeepingError !== undefined) {
|
|
104503
|
+
result.bookkeepingError = transitionResult.bookkeepingError;
|
|
104504
|
+
}
|
|
104136
104505
|
}
|
|
104137
104506
|
}
|
|
104138
104507
|
return result;
|
|
@@ -104144,6 +104513,19 @@ ${templateBackground}`;
|
|
|
104144
104513
|
const db2 = await getDb();
|
|
104145
104514
|
await ensurePipelineRunLink(db2, wbs);
|
|
104146
104515
|
}
|
|
104516
|
+
async reconcileTerminalBookkeeping(wbs, target) {
|
|
104517
|
+
if (target !== "done" && target !== "cancelled")
|
|
104518
|
+
return;
|
|
104519
|
+
const getDb = this.ctx.getDb;
|
|
104520
|
+
if (getDb === undefined)
|
|
104521
|
+
return;
|
|
104522
|
+
try {
|
|
104523
|
+
await reconcileExistingLifecycleRow(getDb, TASK_LIFECYCLE_PROFILE, wbs, target);
|
|
104524
|
+
return;
|
|
104525
|
+
} catch (err) {
|
|
104526
|
+
return err instanceof Error ? err.message : String(err);
|
|
104527
|
+
}
|
|
104528
|
+
}
|
|
104147
104529
|
async batchCreate(jsonPath) {
|
|
104148
104530
|
const raw = await this.ctx.fs.readFile(jsonPath);
|
|
104149
104531
|
let parsed;
|
|
@@ -104459,6 +104841,7 @@ var init_task_service = __esm(() => {
|
|
|
104459
104841
|
init_dist();
|
|
104460
104842
|
init_errors8();
|
|
104461
104843
|
init_run_path();
|
|
104844
|
+
init_lifecycle_adapter();
|
|
104462
104845
|
init_feature_check();
|
|
104463
104846
|
init_pipeline_run_link();
|
|
104464
104847
|
init_planning_check_base();
|
|
@@ -105465,207 +105848,6 @@ var init_idea_handoff_cli = __esm(async () => {
|
|
|
105465
105848
|
if (false) {}
|
|
105466
105849
|
});
|
|
105467
105850
|
|
|
105468
|
-
// ../../packages/app/src/workflow/lifecycle-adapter.ts
|
|
105469
|
-
class LifecycleAdapter {
|
|
105470
|
-
opts;
|
|
105471
|
-
workflowCache;
|
|
105472
|
-
constructor(opts) {
|
|
105473
|
-
this.opts = opts;
|
|
105474
|
-
}
|
|
105475
|
-
async requestTransition(ref, currentStatus, to) {
|
|
105476
|
-
const { profile } = this.opts;
|
|
105477
|
-
const db2 = await this.opts.getDb();
|
|
105478
|
-
const host = createDefaultWorkflowEngineHost();
|
|
105479
|
-
host.registerGuard(new EnvShellGuardRunner(new NodeProcessExecutor3), "builtin");
|
|
105480
|
-
const persistence2 = withActionTrace(new DbWorkflowPersistenceAdapter(db2), createRunLogTraceFailureRecorder(this.opts.cwd));
|
|
105481
|
-
const svc = new WorkflowService(host, persistence2);
|
|
105482
|
-
const workflow = this.bindGuardVar(await this.loadWorkflow(), ref.id);
|
|
105483
|
-
const externalKey = `${profile.entityPrefix}:${ref.id}`;
|
|
105484
|
-
const now2 = new Date().toISOString();
|
|
105485
|
-
if (to === "done" && profile.entityPrefix === "task") {
|
|
105486
|
-
const links = await this.opts.taskRunLinkDao(db2).listByWbs(ref.id, 20);
|
|
105487
|
-
const hasPipelineRun = links.some((l) => l.kind === "pipeline");
|
|
105488
|
-
const hasPriorBypass = links.some((l) => l.kind === "provenance_bypass");
|
|
105489
|
-
if (!hasPipelineRun && !hasPriorBypass) {
|
|
105490
|
-
if (this.opts.provenanceBypass === true) {
|
|
105491
|
-
await this.opts.taskRunLinkDao(db2).insert({
|
|
105492
|
-
id: createId("trl"),
|
|
105493
|
-
wbs: ref.id,
|
|
105494
|
-
run_id: "manual",
|
|
105495
|
-
kind: "provenance_bypass",
|
|
105496
|
-
created_at: now2
|
|
105497
|
-
});
|
|
105498
|
-
} else {
|
|
105499
|
-
return {
|
|
105500
|
-
allowed: false,
|
|
105501
|
-
from: currentStatus,
|
|
105502
|
-
to,
|
|
105503
|
-
report: `No pipeline run recorded for ${ref.id}. Run the pipeline first (` + `spur workflow run task-pipeline.yaml --vars '{"wbs":"${ref.id}"}'), ` + "or pass --provenance-bypass on `spur task update` to bypass (recorded)."
|
|
105504
|
-
};
|
|
105505
|
-
}
|
|
105506
|
-
}
|
|
105507
|
-
const reviewDenial = await this.checkReviewReadyForDone(ref);
|
|
105508
|
-
if (reviewDenial !== null) {
|
|
105509
|
-
return {
|
|
105510
|
-
allowed: false,
|
|
105511
|
-
from: currentStatus,
|
|
105512
|
-
to,
|
|
105513
|
-
report: reviewDenial
|
|
105514
|
-
};
|
|
105515
|
-
}
|
|
105516
|
-
}
|
|
105517
|
-
const existing = await svc.findRunByKey(profile.workflowName, externalKey);
|
|
105518
|
-
const runId = existing?.id ?? createId("run");
|
|
105519
|
-
await svc.createOrAttachRun({
|
|
105520
|
-
id: runId,
|
|
105521
|
-
workflow_name: profile.workflowName,
|
|
105522
|
-
mode: "state-machine",
|
|
105523
|
-
status: "running",
|
|
105524
|
-
started_at: existing?.started_at ?? now2,
|
|
105525
|
-
completed_at: null,
|
|
105526
|
-
metadata_json: "{}",
|
|
105527
|
-
external_key: externalKey
|
|
105528
|
-
});
|
|
105529
|
-
if (existing === undefined) {
|
|
105530
|
-
await this.opts.taskRunLinkDao(db2).insert({
|
|
105531
|
-
id: createId("trl"),
|
|
105532
|
-
wbs: ref.id,
|
|
105533
|
-
run_id: runId,
|
|
105534
|
-
kind: profile.linkKind,
|
|
105535
|
-
created_at: now2
|
|
105536
|
-
});
|
|
105537
|
-
}
|
|
105538
|
-
await svc.reseedRun(workflow, runId, currentStatus);
|
|
105539
|
-
const result = await svc.requestTransition(workflow, runId, to, {
|
|
105540
|
-
workdir: this.opts.cwd,
|
|
105541
|
-
vars: { [profile.varKey]: ref.id, spurBin: this.opts.spurBin }
|
|
105542
|
-
});
|
|
105543
|
-
if (result.allowed) {
|
|
105544
|
-
const enterFailure = await this.runTargetEnterActions(workflow, to, runId);
|
|
105545
|
-
if (enterFailure !== null) {
|
|
105546
|
-
return { allowed: false, from: currentStatus, to, report: enterFailure };
|
|
105547
|
-
}
|
|
105548
|
-
const terminalStatus = to === "cancelled" ? "failed" : to === "done" ? "done" : null;
|
|
105549
|
-
await persistence2.finalizeRun(runId, terminalStatus ?? "running", new Date().toISOString(), undefined, to === "cancelled" ? "cancelled" : to === "done" ? "done" : undefined);
|
|
105550
|
-
return { allowed: true, from: result.fromState, to: result.toState };
|
|
105551
|
-
}
|
|
105552
|
-
return {
|
|
105553
|
-
allowed: false,
|
|
105554
|
-
from: currentStatus,
|
|
105555
|
-
to,
|
|
105556
|
-
report: this.formatDenialWithLegalPaths(result, workflow, currentStatus, ref.id)
|
|
105557
|
-
};
|
|
105558
|
-
}
|
|
105559
|
-
async runTargetEnterActions(workflow, to, runId) {
|
|
105560
|
-
if (this.opts.runEnterActions === false)
|
|
105561
|
-
return null;
|
|
105562
|
-
const actions = workflow.states.find((state) => state.id === to)?.onEnter ?? [];
|
|
105563
|
-
if (actions.length === 0)
|
|
105564
|
-
return null;
|
|
105565
|
-
const runner = new StreamingShellActionRunner(new NodeProcessExecutor3);
|
|
105566
|
-
const vars = workflow.vars ?? {};
|
|
105567
|
-
for (const [index2, action] of actions.entries()) {
|
|
105568
|
-
if (action.kind !== "shell") {
|
|
105569
|
-
return `State "${to}" onEnter[${index2}] is kind "${action.kind}"; the lifecycle caller only runs shell onEnter actions.`;
|
|
105570
|
-
}
|
|
105571
|
-
const command = action.options?.command;
|
|
105572
|
-
if (typeof command !== "string" || command.trim() === "") {
|
|
105573
|
-
return `State "${to}" onEnter[${index2}] has no shell command.`;
|
|
105574
|
-
}
|
|
105575
|
-
const result = await runner.execute({ ...action.options }, {
|
|
105576
|
-
runId,
|
|
105577
|
-
workdir: this.opts.cwd,
|
|
105578
|
-
stateOrNodeId: to,
|
|
105579
|
-
vars,
|
|
105580
|
-
env: {}
|
|
105581
|
-
});
|
|
105582
|
-
if (!result.ok) {
|
|
105583
|
-
const stderr = typeof result.data?.stderr === "string" ? result.data.stderr.trim() : "";
|
|
105584
|
-
const tail2 = stderr.length > 2000 ? stderr.slice(-2000) : stderr;
|
|
105585
|
-
const exitCode = result.data?.exitCode ?? "unknown";
|
|
105586
|
-
return [`State "${to}" onEnter shell failed (exit ${String(exitCode)}).`, result.error ?? "", tail2].filter((part) => part !== "").join(" ");
|
|
105587
|
-
}
|
|
105588
|
-
}
|
|
105589
|
-
return null;
|
|
105590
|
-
}
|
|
105591
|
-
formatDenialWithLegalPaths(result, workflow, currentStatus, id) {
|
|
105592
|
-
const base2 = this.formatDenial(result.detail, result.guardReport);
|
|
105593
|
-
if (result.reason !== "no-such-transition")
|
|
105594
|
-
return base2;
|
|
105595
|
-
const legal = workflow.transitions.filter((t) => t.from === currentStatus).map((t) => t.to);
|
|
105596
|
-
if (legal.length === 0)
|
|
105597
|
-
return base2;
|
|
105598
|
-
const command = this.opts.profile.entityPrefix === "feature" ? `spur feature sync ${id} (derives the legal hop path)` : `spur task update ${id} <legal-status> or \`spur task record ${id} --transition <legal-status>\``;
|
|
105599
|
-
return `${base2}. Legal path(s) from ${currentStatus}: ${legal.join(" \u2192 ")}. Reach them via \`${command}\`.`;
|
|
105600
|
-
}
|
|
105601
|
-
async loadWorkflow() {
|
|
105602
|
-
if (this.workflowCache === undefined) {
|
|
105603
|
-
const def = await loadWorkflowDef(this.opts.workflowPath, { validateSchema: false });
|
|
105604
|
-
if (def.kind !== "state-machine") {
|
|
105605
|
-
throw new Error(`${this.opts.profile.workflowName} workflow must be a state-machine; got "${def.kind}"`);
|
|
105606
|
-
}
|
|
105607
|
-
this.workflowCache = def;
|
|
105608
|
-
}
|
|
105609
|
-
return this.workflowCache;
|
|
105610
|
-
}
|
|
105611
|
-
bindGuardVar(workflow, id) {
|
|
105612
|
-
return {
|
|
105613
|
-
...workflow,
|
|
105614
|
-
vars: { ...workflow.vars, [this.opts.profile.varKey]: id, spurBin: this.opts.spurBin }
|
|
105615
|
-
};
|
|
105616
|
-
}
|
|
105617
|
-
async checkReviewReadyForDone(ref) {
|
|
105618
|
-
const markdown = await this.loadTaskMarkdown(ref);
|
|
105619
|
-
if (markdown === null) {
|
|
105620
|
-
return null;
|
|
105621
|
-
}
|
|
105622
|
-
const reviewBody = extractReviewSectionBody(markdown);
|
|
105623
|
-
if (reviewBody === null || !hasPopulatedPriorityTable(reviewBody)) {
|
|
105624
|
-
return `Review L3 gate failed for task ${ref.id}: ### Review must contain a populated ` + `P1\u2013P4 priority findings table before testing\u2192done (strict-core). ` + `Author the table via /sp:dev-review, then re-run the done transition.`;
|
|
105625
|
-
}
|
|
105626
|
-
return null;
|
|
105627
|
-
}
|
|
105628
|
-
async loadTaskMarkdown(ref) {
|
|
105629
|
-
if (this.opts.readTaskMarkdown) {
|
|
105630
|
-
return this.opts.readTaskMarkdown(ref);
|
|
105631
|
-
}
|
|
105632
|
-
try {
|
|
105633
|
-
const fs3 = createNodeFileSystem3(this.opts.cwd);
|
|
105634
|
-
return await fs3.readFile(ref.filePath);
|
|
105635
|
-
} catch {
|
|
105636
|
-
return null;
|
|
105637
|
-
}
|
|
105638
|
-
}
|
|
105639
|
-
formatDenial(detail, guardReport) {
|
|
105640
|
-
if (guardReport === undefined || guardReport === null)
|
|
105641
|
-
return detail;
|
|
105642
|
-
const report = typeof guardReport === "string" ? guardReport : JSON.stringify(guardReport);
|
|
105643
|
-
return `${detail} \u2014 ${report}`;
|
|
105644
|
-
}
|
|
105645
|
-
}
|
|
105646
|
-
var TASK_LIFECYCLE_PROFILE, FEATURE_LIFECYCLE_PROFILE;
|
|
105647
|
-
var init_lifecycle_adapter = __esm(() => {
|
|
105648
|
-
init_src2();
|
|
105649
|
-
init_dist8();
|
|
105650
|
-
init_dist5();
|
|
105651
|
-
init_task_check();
|
|
105652
|
-
init_action_trace();
|
|
105653
|
-
init_shell();
|
|
105654
|
-
init_shell2();
|
|
105655
|
-
TASK_LIFECYCLE_PROFILE = {
|
|
105656
|
-
linkKind: "lifecycle",
|
|
105657
|
-
workflowName: "task-lifecycle",
|
|
105658
|
-
entityPrefix: "task",
|
|
105659
|
-
varKey: "wbs"
|
|
105660
|
-
};
|
|
105661
|
-
FEATURE_LIFECYCLE_PROFILE = {
|
|
105662
|
-
linkKind: "feature-lifecycle",
|
|
105663
|
-
workflowName: "feature-lifecycle",
|
|
105664
|
-
entityPrefix: "feature",
|
|
105665
|
-
varKey: "featureId"
|
|
105666
|
-
};
|
|
105667
|
-
});
|
|
105668
|
-
|
|
105669
105851
|
// ../../packages/app/src/workflow/mermaid-render.ts
|
|
105670
105852
|
function escId(id) {
|
|
105671
105853
|
return id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -106117,6 +106299,7 @@ __export(exports_src2, {
|
|
|
106117
106299
|
refreshHistoryRollups: () => refreshHistoryRollups,
|
|
106118
106300
|
redactAndBound: () => redactAndBound,
|
|
106119
106301
|
recordAgentQuotaEvent: () => recordAgentQuotaEvent,
|
|
106302
|
+
reconcileExistingLifecycleRow: () => reconcileExistingLifecycleRow,
|
|
106120
106303
|
readyRefineCommand: () => readyRefineCommand,
|
|
106121
106304
|
readyRefineAllCommand: () => readyRefineAllCommand,
|
|
106122
106305
|
readWorkflowRunRecord: () => readWorkflowRunRecord,
|
|
@@ -115595,11 +115778,19 @@ ${result.content}`);
|
|
|
115595
115778
|
"scenario title>` bullets or `Scenario: AC1 \u2014 <title>` blocks (AC<n> is task-local, never R<n>);",
|
|
115596
115779
|
"`(req: R<n>)` binds a requirement; `ac_altitude: task-local` exempts task-only AC.",
|
|
115597
115780
|
'Sections replace, with one exception: `--section "Q&A"` APPENDS a timestamped',
|
|
115598
|
-
"`#### Q&A entry \u2014 <ISO>` block. Start the body with `<!-- qa:replace -->` to replace it wholesale."
|
|
115781
|
+
"`#### Q&A entry \u2014 <ISO>` block. Start the body with `<!-- qa:replace -->` to replace it wholesale.",
|
|
115782
|
+
"Extend instead of replace: `--append` with `--section` writes the new body after the existing",
|
|
115783
|
+
"section content (existing bytes preserved; an empty/missing section is created) \u2014 the bounded",
|
|
115784
|
+
"way to extend a populated Review/Testing section without destroying it (task 1057)."
|
|
115599
115785
|
].join(`
|
|
115600
|
-
`)).option(...SHARED_OPTIONS.section).option(...SHARED_OPTIONS.fromFile).option(...SHARED_OPTIONS.featureFrontmatter).option(...SHARED_OPTIONS.prioritySet).option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--ac-altitude <mode>", "Set the ac_altitude frontmatter field. Valid: `graduating` (default; feature-AC subset rule enforced) or `task-local` (skip the DD-09 subset rule \u2014 task scenarios are intentionally not feature ship criteria). Mirrors the L1 schema enum (packages/domain/src/planning/schema.ts:304).").option("--estimate-hours <n>", "Set the estimate_hours frontmatter field (positive number) \u2014 decomposition size estimate; the inline pipeline driver dispatches no subagent at/below its floor").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--assignee <spec-id>", "Set the assignee frontmatter field to an agent spec id").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--provenance-bypass", "Record an audited provenance-bypass link when no pipeline run exists (replaces the SPUR_PROVENANCE_OVERRIDE env var; pair with --force-done)", false).option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, status, options) => {
|
|
115786
|
+
`)).option(...SHARED_OPTIONS.section).option(...SHARED_OPTIONS.fromFile).option("--append", "With --section: write the new body after the existing section content (existing content + one blank line + new body; surrounding whitespace trimmed) instead of replacing it wholesale. Requires --section; an empty/missing section is created. Section names are case-sensitive (see `spur task sections <wbs> list`).", false).option(...SHARED_OPTIONS.featureFrontmatter).option(...SHARED_OPTIONS.prioritySet).option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--ac-altitude <mode>", "Set the ac_altitude frontmatter field. Valid: `graduating` (default; feature-AC subset rule enforced) or `task-local` (skip the DD-09 subset rule \u2014 task scenarios are intentionally not feature ship criteria). Mirrors the L1 schema enum (packages/domain/src/planning/schema.ts:304).").option("--estimate-hours <n>", "Set the estimate_hours frontmatter field (positive number) \u2014 decomposition size estimate; the inline pipeline driver dispatches no subagent at/below its floor").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--assignee <spec-id>", "Set the assignee frontmatter field to an agent spec id").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--provenance-bypass", "Record an audited provenance-bypass link when no pipeline run exists (replaces the SPUR_PROVENANCE_OVERRIDE env var; pair with --force-done)", false).option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, status, options) => {
|
|
115601
115787
|
const svc = await makeService(context4, options.folder, options.lifecycle === false, options.provenanceBypass === true);
|
|
115602
115788
|
try {
|
|
115789
|
+
if (options.append === true && options.section === undefined) {
|
|
115790
|
+
writeJsonError(context4.output, options, "--append requires --section", "VALIDATION_FAILED");
|
|
115791
|
+
context4.setExitCode(2);
|
|
115792
|
+
return;
|
|
115793
|
+
}
|
|
115603
115794
|
if (options.assignee !== undefined) {
|
|
115604
115795
|
if (options.section !== undefined) {
|
|
115605
115796
|
writeJsonError(context4.output, options, "--assignee cannot be combined with --section", "VALIDATION_FAILED");
|
|
@@ -115626,7 +115817,7 @@ ${result.content}`);
|
|
|
115626
115817
|
context4.setExitCode(2);
|
|
115627
115818
|
return;
|
|
115628
115819
|
}
|
|
115629
|
-
const result = await svc.updateSection(wbs, options.section, options.fromFile);
|
|
115820
|
+
const result = await svc.updateSection(wbs, options.section, options.fromFile, options.append === true);
|
|
115630
115821
|
if (options.json) {
|
|
115631
115822
|
context4.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
|
|
115632
115823
|
} else {
|
|
@@ -115710,6 +115901,9 @@ ${result.content}`);
|
|
|
115710
115901
|
} else if (outcome.closeAuditError !== undefined) {
|
|
115711
115902
|
context4.output.error(`warning: failed to record done close audit fields: ${outcome.closeAuditError}`);
|
|
115712
115903
|
}
|
|
115904
|
+
if (result.bookkeepingError !== undefined) {
|
|
115905
|
+
context4.output.error(`warning: failed to reconcile task-lifecycle bookkeeping row: ${result.bookkeepingError} ` + "(task file is committed; replay the same terminal transition to repair)");
|
|
115906
|
+
}
|
|
115713
115907
|
if (options.json) {
|
|
115714
115908
|
context4.output.write(toEnvelopeJson(result, { enveloped: options.jsonEnvelope }));
|
|
115715
115909
|
} else {
|
|
@@ -116088,6 +116282,9 @@ Recover with: ${err.recoveryCommand}` : err.message;
|
|
|
116088
116282
|
if (result.closeAuditError !== undefined) {
|
|
116089
116283
|
context4.output.error(`warning: failed to record done close audit fields: ${result.closeAuditError}`);
|
|
116090
116284
|
}
|
|
116285
|
+
if (result.bookkeepingError !== undefined) {
|
|
116286
|
+
context4.output.error(`warning: failed to reconcile task-lifecycle bookkeeping row: ${result.bookkeepingError} ` + "(task file is committed; replay the same terminal transition to repair)");
|
|
116287
|
+
}
|
|
116091
116288
|
if (result.verdictMessage !== undefined) {
|
|
116092
116289
|
context4.output.error(`\u24D8 verdict artifact ${result.verdictState}: ${result.verdictMessage}`);
|
|
116093
116290
|
}
|
|
@@ -116456,7 +116653,10 @@ async function makeService(context4, folderOverride, noLifecycle = false, proven
|
|
|
116456
116653
|
const writeService = new PlanningWriteService({
|
|
116457
116654
|
fs: context4.fs,
|
|
116458
116655
|
...lifecycle ? { lifecycle } : {},
|
|
116459
|
-
emitter: makePlanningEmitter(context4)
|
|
116656
|
+
emitter: makePlanningEmitter(context4),
|
|
116657
|
+
onTransitionCommitted: async (ref, _from, to) => {
|
|
116658
|
+
await reconcileExistingLifecycleRow(() => context4.getDb(), TASK_LIFECYCLE_PROFILE, ref.id, to);
|
|
116659
|
+
}
|
|
116460
116660
|
});
|
|
116461
116661
|
return new TaskService({
|
|
116462
116662
|
fs: context4.fs,
|
|
@@ -118331,7 +118531,7 @@ import { createRequire } from "module";
|
|
|
118331
118531
|
var CLI_CONFIG = {
|
|
118332
118532
|
binaryName: "spur",
|
|
118333
118533
|
binaryLabel: "spur",
|
|
118334
|
-
binaryVersion: "0.3.
|
|
118534
|
+
binaryVersion: "0.3.98",
|
|
118335
118535
|
configDir: ".spur",
|
|
118336
118536
|
configFile: ".spur/config.yaml",
|
|
118337
118537
|
databaseFile: ".spur/spur.db"
|
|
@@ -127157,6 +127357,10 @@ init_dist5();
|
|
|
127157
127357
|
await init_src3();
|
|
127158
127358
|
import { homedir as homedir12 } from "os";
|
|
127159
127359
|
import { basename as basename20, join as join51, relative as relative11, resolve as resolve23 } from "path";
|
|
127360
|
+
// ../server/package.json
|
|
127361
|
+
var version3 = "0.3.98";
|
|
127362
|
+
|
|
127363
|
+
// ../server/src/modules/health/index.ts
|
|
127160
127364
|
var startedAt = Date.now();
|
|
127161
127365
|
var OPERATOR_AGENT_ID = "board-operator";
|
|
127162
127366
|
var healthModule = {
|
|
@@ -127167,6 +127371,9 @@ var healthModule = {
|
|
|
127167
127371
|
const memory = process.memoryUsage();
|
|
127168
127372
|
return c3.json({
|
|
127169
127373
|
status: "ok",
|
|
127374
|
+
timestamp: new Date().toISOString(),
|
|
127375
|
+
service: "spur",
|
|
127376
|
+
version: version3,
|
|
127170
127377
|
uptime_seconds: Math.round(uptime),
|
|
127171
127378
|
memory_rss_mb: Math.round(memory.rss / 1048576 * 100) / 100,
|
|
127172
127379
|
memory_heap_mb: Math.round(memory.heapUsed / 1048576 * 100) / 100
|
|
@@ -127263,7 +127470,7 @@ var healthModule = {
|
|
|
127263
127470
|
const doc2 = doctorMap.get(ex.name);
|
|
127264
127471
|
const installed = doc2?.installed ?? !avail.disabled;
|
|
127265
127472
|
const usable = doc2?.usable ?? !avail.disabled;
|
|
127266
|
-
const
|
|
127473
|
+
const version4 = doc2?.version ?? null;
|
|
127267
127474
|
const error51 = doc2?.error ?? null;
|
|
127268
127475
|
const isProject = projectExecutors.has(ex.name);
|
|
127269
127476
|
const isGlobal = globalExecutors.has(ex.name);
|
|
@@ -127280,7 +127487,7 @@ var healthModule = {
|
|
|
127280
127487
|
disabledSince: avail.since,
|
|
127281
127488
|
installed,
|
|
127282
127489
|
usable,
|
|
127283
|
-
version:
|
|
127490
|
+
version: version4,
|
|
127284
127491
|
error: error51,
|
|
127285
127492
|
elected: [],
|
|
127286
127493
|
executionCapabilities: ex.executionCapabilities,
|
|
@@ -127516,6 +127723,9 @@ var healthModule = {
|
|
|
127516
127723
|
try {
|
|
127517
127724
|
body = await c3.req.json();
|
|
127518
127725
|
} catch {}
|
|
127726
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
127727
|
+
return c3.json({ error: "request body must be a JSON object" }, 400);
|
|
127728
|
+
}
|
|
127519
127729
|
const target = body.name ?? body.path;
|
|
127520
127730
|
if (!target) {
|
|
127521
127731
|
return c3.json({ error: "Missing name or path in request body" }, 400);
|
|
@@ -127547,6 +127757,9 @@ var healthModule = {
|
|
|
127547
127757
|
} catch {
|
|
127548
127758
|
return c3.json({ error: "Invalid JSON request body" }, 400);
|
|
127549
127759
|
}
|
|
127760
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
127761
|
+
return c3.json({ error: "request body must be a JSON object" }, 400);
|
|
127762
|
+
}
|
|
127550
127763
|
const executorName = body.name ?? body.executor;
|
|
127551
127764
|
if (!executorName || typeof executorName !== "string" || executorName.trim().length === 0) {
|
|
127552
127765
|
return c3.json({ error: "Missing or invalid executor name" }, 400);
|
|
@@ -128589,6 +128802,9 @@ var processesModule = {
|
|
|
128589
128802
|
} catch {
|
|
128590
128803
|
return c3.json({ error: "request body must be valid JSON" }, 400);
|
|
128591
128804
|
}
|
|
128805
|
+
if (json4 === null || typeof json4 !== "object" || Array.isArray(json4)) {
|
|
128806
|
+
return c3.json({ error: "request body must be a JSON object" }, 400);
|
|
128807
|
+
}
|
|
128592
128808
|
const body = json4;
|
|
128593
128809
|
if (typeof body.line !== "string" || body.line.length === 0) {
|
|
128594
128810
|
return c3.json({ error: 'field "line" is required' }, 400);
|
|
@@ -128806,7 +129022,13 @@ function createTaskHandlers(ctx) {
|
|
|
128806
129022
|
return { ok: true, data: createResponseShape2(r2) };
|
|
128807
129023
|
}),
|
|
128808
129024
|
transition: os4.task.transition.handler(async ({ input }) => {
|
|
128809
|
-
await ctx.transitionTask(input);
|
|
129025
|
+
const guarded = await ctx.transitionTask(input);
|
|
129026
|
+
if (guarded.kind === "transitioned" && guarded.result.bookkeepingError !== undefined) {
|
|
129027
|
+
ctx.logger.error(`task ${input.wbs}: failed to reconcile task-lifecycle bookkeeping row after transition to ${guarded.result.toStatus}: ${guarded.result.bookkeepingError} ` + `(task file is committed; replay the same terminal transition, e.g. \`spur task record ${input.wbs} --transition ${input.toStatus}\`, to repair)`, { wbs: input.wbs, toStatus: guarded.result.toStatus });
|
|
129028
|
+
}
|
|
129029
|
+
if (guarded.kind === "transitioned" && guarded.result.closeAuditError !== undefined) {
|
|
129030
|
+
ctx.logger.error(`task ${input.wbs}: failed to record done close audit fields after transition to ${input.toStatus}: ${guarded.result.closeAuditError} ` + `(task file is committed; replay the same terminal transition, e.g. \`spur task record ${input.wbs} --transition ${input.toStatus}\`, to repair)`, { wbs: input.wbs, toStatus: input.toStatus });
|
|
129031
|
+
}
|
|
128810
129032
|
return { ok: true, data: { wbs: input.wbs, status: input.toStatus } };
|
|
128811
129033
|
}),
|
|
128812
129034
|
body: os4.task.body.handler(async ({ input }) => {
|
|
@@ -130775,7 +130997,6 @@ async function generateOpenApiSpec(pluginPaths = {}) {
|
|
|
130775
130997
|
}
|
|
130776
130998
|
|
|
130777
130999
|
// ../server/src/router.ts
|
|
130778
|
-
var version3 = "0.0.0";
|
|
130779
131000
|
var os5 = implement(contract);
|
|
130780
131001
|
var EMPTY_BOARD_CATALOG = { catalogVersion: 1, host: null, modules: [] };
|
|
130781
131002
|
var stubCtx = new Proxy({}, {
|
|
@@ -130989,6 +131210,7 @@ function createServerContext(appRt, options) {
|
|
|
130989
131210
|
runDir: join53(cwd, ".spur", "run"),
|
|
130990
131211
|
webDistPath: options.webDistPath,
|
|
130991
131212
|
boardModules: options.boardModules,
|
|
131213
|
+
logger: appRt.logger,
|
|
130992
131214
|
async getDb() {
|
|
130993
131215
|
if (!dbPromise) {
|
|
130994
131216
|
const created = (async () => {
|
|
@@ -131014,7 +131236,14 @@ function createServerContext(appRt, options) {
|
|
|
131014
131236
|
const lazyEmitter = new LazyPlanningEventEmitter(this.getDb.bind(this), eventsBus);
|
|
131015
131237
|
taskSvc = new TaskService({
|
|
131016
131238
|
fs: fs3,
|
|
131017
|
-
writeService: new PlanningWriteService({
|
|
131239
|
+
writeService: new PlanningWriteService({
|
|
131240
|
+
fs: fs3,
|
|
131241
|
+
projectName: "spur",
|
|
131242
|
+
emitter: lazyEmitter,
|
|
131243
|
+
onTransitionCommitted: async (ref, _from, to) => {
|
|
131244
|
+
await reconcileExistingLifecycleRow(this.getDb.bind(this), TASK_LIFECYCLE_PROFILE, ref.id, to);
|
|
131245
|
+
}
|
|
131246
|
+
}),
|
|
131018
131247
|
tasksDir: folders.tasksDir,
|
|
131019
131248
|
foldersConfig: folders.foldersConfig,
|
|
131020
131249
|
projectName: "spur",
|