@gobing-ai/spur 0.3.74 → 0.3.75
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/config/workflows/docs-pipeline.yaml +26 -1
- package/config/workflows/feature-dev.yaml +143 -102
- package/config/workflows/idea-pipeline.yaml +58 -11
- package/config/workflows/task-pipeline.yaml +76 -40
- package/config/workflows/wrapup-pipeline.yaml +183 -111
- package/package.json +1 -1
- package/plugins/sp/agents/expert-spur.md +7 -4
- package/plugins/sp/commands/dev-idea.md +1 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/skills/spur-cli/references/tasks.md +3 -1
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +23 -8
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +4 -1
- package/spur.js +2010 -362
- package/web/_astro/{BoardApp.C60RJZRj.js → BoardApp.BOW1815F.js} +97 -97
- package/web/_astro/BoardApp.C4zimv1U.js +1 -0
- package/web/_astro/{TaskDetail.Guk8VuNu.js → TaskDetail.CX6C7dSJ.js} +1 -1
- package/web/_astro/{arc.CAZtlWJM.js → arc.CwSvH1ji.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.9-XbF_Tz.js → architectureDiagram-3BPJPVTR.Dxne1RvP.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.DC4WLx3L.js → blockDiagram-GPEHLZMM.D9Fb14rC.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.JgjUQMgG.js → c4Diagram-AAUBKEIU.DmvWSFvv.js} +1 -1
- package/web/_astro/channel.CQHdDVp9.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.D_JFiXa-.js → chunk-2J33WTMH.BCCktIrE.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Bk_RxoeT.js → chunk-4BX2VUAB.BzCk9q27.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.a_Rj2VxL.js → chunk-55IACEB6.CZXVgTk4.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.C2NoS1U6.js → chunk-727SXJPM.CJL8UIpX.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.D1Xn6CNa.js → chunk-AQP2D5EJ.Dz8MDEdw.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.BhJrSBsX.js → chunk-FMBD7UC4.CkQeYYUw.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.ByVGduYQ.js → chunk-ND2GUHAM.KiC1QgzH.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.B47Paunq.js → chunk-QZHKN3VN.AJ08mw2e.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.BERMSD5C.js → classDiagram-4FO5ZUOK.DRWRDKzK.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BERMSD5C.js → classDiagram-v2-Q7XG4LA2.DRWRDKzK.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.O_rHGlhU.js → cose-bilkent-S5V4N54A.CavydfLP.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.qCW2GdNp.js → cynefin-OW5HDTMX.D_7o_a0B.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.C5Y4lH_u.js → dagre-BM42HDAG.LWk2dKgg.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DxZmRXxF.js → diagram-2AECGRRQ.82lbq6aB.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.qSxZeYSK.js → diagram-5GNKFQAL.hyNL1pwY.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.-7vl3bXs.js → diagram-KO2AKTUF.BaLqVf-b.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BHVV-UH3.js → diagram-LMA3HP47.DQJxj0oz.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.DHAuW9sK.js → diagram-OG6HWLK6.DhlL8cG2.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DMRLQwPN.js → erDiagram-TEJ5UH35.HoTnXwkF.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.B3EpPp_8.js → flowDiagram-I6XJVG4X.DkFKlZIZ.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.BgUrExXM.js → ganttDiagram-6RSMTGT7.9qPfOTDb.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.CU-9yVN4.js → gitGraphDiagram-PVQCEYII.B4G18Dwc.js} +1 -1
- package/web/_astro/index.Dcr_8fiK.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CvVTCRLe.js → infoDiagram-5YYISTIA.e3KJkXAM.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BUzOfKPf.js → ishikawaDiagram-YF4QCWOH.CY3yddhD.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BBTb7ziz.js → journeyDiagram-JHISSGLW.DaAKIO1t.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CTQQr70i.js → kanban-definition-UN3LZRKU.DGkDddtc.js} +1 -1
- package/web/_astro/{linear.CLKlJPiS.js → linear.DnDPcpd1.js} +1 -1
- package/web/_astro/{mermaid.core.BA4wFhLP.js → mermaid.core.1uBmxa9t.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.t1vG2l5e.js → mindmap-definition-RKZ34NQL.XFJVayxw.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.C8zJhyTu.js → pieDiagram-4H26LBE5.BXm2OgHh.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DqYYvb7T.js → quadrantDiagram-W4KKPZXB.DgB2p5fc.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.lzfDZ9nI.js → requirementDiagram-4Y6WPE33.jBR39bnS.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.lm6N5ORM.js → sankeyDiagram-5OEKKPKP.N3u9OhE3.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.E6_IV4Dj.js → sequenceDiagram-3UESZ5HK.CZhqZXNL.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.Bnv6Ok2p.js → stateDiagram-AJRCARHV.DlNu1VEa.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.mJHH_Ng_.js → stateDiagram-v2-BHNVJYJU.BEqi8NQV.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BhRgqSLa.js → timeline-definition-PNZ67QCA.BPcexclc.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.pjb5DMC9.js → vennDiagram-CIIHVFJN.BZalxKGQ.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CW20KUng.js → wardleyDiagram-YWT4CUSO.BqOljod9.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.vX9_yuMl.js → xychartDiagram-2RQKCTM6.tkO4ppN5.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.ymfj1EHA.js +0 -1
- package/web/_astro/channel.Bxxe2Byc.js +0 -1
- package/web/_astro/index.BhvM3djt.css +0 -1
package/spur.js
CHANGED
|
@@ -58609,6 +58609,132 @@ class SystemEventDao {
|
|
|
58609
58609
|
throw error51;
|
|
58610
58610
|
}
|
|
58611
58611
|
}
|
|
58612
|
+
async eventSummary(spec) {
|
|
58613
|
+
const until = spec.until ?? new Date().toISOString();
|
|
58614
|
+
const since = spec.since;
|
|
58615
|
+
const sinceMs = Date.parse(since);
|
|
58616
|
+
const untilMs = Date.parse(until);
|
|
58617
|
+
if (Number.isNaN(sinceMs) || Number.isNaN(untilMs) || untilMs <= sinceMs) {
|
|
58618
|
+
return {
|
|
58619
|
+
window: { since, until },
|
|
58620
|
+
totalEvents: 0,
|
|
58621
|
+
errorEventCount: 0,
|
|
58622
|
+
warningEventCount: 0,
|
|
58623
|
+
eventVolumeBuckets: [],
|
|
58624
|
+
topEventTypes: [],
|
|
58625
|
+
recentErrors: []
|
|
58626
|
+
};
|
|
58627
|
+
}
|
|
58628
|
+
const width = untilMs - sinceMs;
|
|
58629
|
+
let bucketMs = spec.bucketMs && spec.bucketMs > 0 ? spec.bucketMs : width <= 3600000 ? 60000 : width <= 21600000 ? 300000 : width <= 86400000 ? 900000 : width <= 604800000 ? 3600000 : 86400000;
|
|
58630
|
+
while (Math.ceil((untilMs - sinceMs) / bucketMs) > 240) {
|
|
58631
|
+
bucketMs *= 2;
|
|
58632
|
+
}
|
|
58633
|
+
const bucketCount = Math.max(1, Math.ceil((untilMs - sinceMs) / bucketMs));
|
|
58634
|
+
const buckets = [];
|
|
58635
|
+
for (let i2 = 0;i2 < bucketCount; i2++) {
|
|
58636
|
+
buckets.push({
|
|
58637
|
+
timestamp: new Date(sinceMs + i2 * bucketMs).toISOString(),
|
|
58638
|
+
total: 0,
|
|
58639
|
+
byPrefix: {},
|
|
58640
|
+
bySeverity: { info: 0, warning: 0, error: 0, unknown: 0 }
|
|
58641
|
+
});
|
|
58642
|
+
}
|
|
58643
|
+
try {
|
|
58644
|
+
const totalsRow = await this.db.queryFirst(`SELECT
|
|
58645
|
+
COUNT(*) AS total,
|
|
58646
|
+
SUM(CASE WHEN sev = 'error' THEN 1 ELSE 0 END) AS error_count,
|
|
58647
|
+
SUM(CASE WHEN sev = 'warning' THEN 1 ELSE 0 END) AS warning_count
|
|
58648
|
+
FROM (
|
|
58649
|
+
SELECT COALESCE(json_extract(payload_json, '$.presentation.severity'), 'unknown') AS sev
|
|
58650
|
+
FROM system_events
|
|
58651
|
+
WHERE occurred_at >= ?1 AND occurred_at < ?2
|
|
58652
|
+
)`, since, until);
|
|
58653
|
+
const bucketRows = await this.db.queryAll(`SELECT
|
|
58654
|
+
CAST((strftime('%s', occurred_at) * 1000 - ?1) / ?2 AS INTEGER) AS bucket_idx,
|
|
58655
|
+
CASE WHEN instr(event_name, '.') > 0 THEN substr(event_name, 1, instr(event_name, '.') - 1) ELSE event_name END AS prefix,
|
|
58656
|
+
COALESCE(json_extract(payload_json, '$.presentation.severity'), 'unknown') AS sev,
|
|
58657
|
+
COUNT(*) AS cnt
|
|
58658
|
+
FROM system_events
|
|
58659
|
+
WHERE occurred_at >= ?3 AND occurred_at < ?4
|
|
58660
|
+
GROUP BY bucket_idx, prefix, sev`, sinceMs, bucketMs, since, until);
|
|
58661
|
+
for (const row of bucketRows) {
|
|
58662
|
+
const idx = Number(row.bucket_idx);
|
|
58663
|
+
if (idx >= 0 && idx < buckets.length) {
|
|
58664
|
+
const b = buckets[idx];
|
|
58665
|
+
if (b !== undefined) {
|
|
58666
|
+
const cnt = Number(row.cnt);
|
|
58667
|
+
b.total += cnt;
|
|
58668
|
+
b.byPrefix[row.prefix] = (b.byPrefix[row.prefix] ?? 0) + cnt;
|
|
58669
|
+
if (row.sev === "info" || row.sev === "warning" || row.sev === "error") {
|
|
58670
|
+
b.bySeverity[row.sev] += cnt;
|
|
58671
|
+
} else {
|
|
58672
|
+
b.bySeverity.unknown += cnt;
|
|
58673
|
+
}
|
|
58674
|
+
}
|
|
58675
|
+
}
|
|
58676
|
+
}
|
|
58677
|
+
const topRows = await this.db.queryAll(`SELECT
|
|
58678
|
+
event_name AS name,
|
|
58679
|
+
CASE WHEN instr(event_name, '.') > 0 THEN substr(event_name, 1, instr(event_name, '.') - 1) ELSE event_name END AS prefix,
|
|
58680
|
+
COUNT(*) AS count,
|
|
58681
|
+
MAX(occurred_at) AS latest_at
|
|
58682
|
+
FROM system_events
|
|
58683
|
+
WHERE occurred_at >= ?1 AND occurred_at < ?2
|
|
58684
|
+
GROUP BY event_name
|
|
58685
|
+
ORDER BY count DESC, event_name ASC
|
|
58686
|
+
LIMIT 10`, since, until);
|
|
58687
|
+
const recentRows = await this.db.queryAll(`SELECT
|
|
58688
|
+
id,
|
|
58689
|
+
event_name AS name,
|
|
58690
|
+
occurred_at,
|
|
58691
|
+
COALESCE(
|
|
58692
|
+
json_extract(payload_json, '$.presentation.summary'),
|
|
58693
|
+
json_extract(payload_json, '$.presentation.title'),
|
|
58694
|
+
json_extract(payload_json, '$.error'),
|
|
58695
|
+
event_name
|
|
58696
|
+
) AS message,
|
|
58697
|
+
run_id AS ref_id
|
|
58698
|
+
FROM system_events
|
|
58699
|
+
WHERE occurred_at >= ?1 AND occurred_at < ?2
|
|
58700
|
+
AND COALESCE(json_extract(payload_json, '$.presentation.severity'), '') = 'error'
|
|
58701
|
+
ORDER BY occurred_at DESC, id DESC
|
|
58702
|
+
LIMIT 10`, since, until);
|
|
58703
|
+
return {
|
|
58704
|
+
window: { since, until },
|
|
58705
|
+
totalEvents: Number(totalsRow?.total ?? 0),
|
|
58706
|
+
errorEventCount: Number(totalsRow?.error_count ?? 0),
|
|
58707
|
+
warningEventCount: Number(totalsRow?.warning_count ?? 0),
|
|
58708
|
+
eventVolumeBuckets: buckets,
|
|
58709
|
+
topEventTypes: topRows.map((r) => ({
|
|
58710
|
+
name: r.name,
|
|
58711
|
+
prefix: r.prefix,
|
|
58712
|
+
count: Number(r.count),
|
|
58713
|
+
latestAt: r.latest_at
|
|
58714
|
+
})),
|
|
58715
|
+
recentErrors: recentRows.map((r) => ({
|
|
58716
|
+
id: r.id,
|
|
58717
|
+
name: r.name,
|
|
58718
|
+
occurredAt: r.occurred_at,
|
|
58719
|
+
message: String(r.message ?? r.name),
|
|
58720
|
+
...r.ref_id ? { refId: r.ref_id } : {}
|
|
58721
|
+
}))
|
|
58722
|
+
};
|
|
58723
|
+
} catch (error51) {
|
|
58724
|
+
if (error51 instanceof Error && error51.message.includes("no such table: system_events")) {
|
|
58725
|
+
return {
|
|
58726
|
+
window: { since, until },
|
|
58727
|
+
totalEvents: 0,
|
|
58728
|
+
errorEventCount: 0,
|
|
58729
|
+
warningEventCount: 0,
|
|
58730
|
+
eventVolumeBuckets: buckets,
|
|
58731
|
+
topEventTypes: [],
|
|
58732
|
+
recentErrors: []
|
|
58733
|
+
};
|
|
58734
|
+
}
|
|
58735
|
+
throw error51;
|
|
58736
|
+
}
|
|
58737
|
+
}
|
|
58612
58738
|
async deleteAll() {
|
|
58613
58739
|
await this.db.run("DELETE FROM system_events");
|
|
58614
58740
|
}
|
|
@@ -60660,14 +60786,19 @@ var init_run_dao = __esm(() => {
|
|
|
60660
60786
|
stampMetadata(runId, metadata) {
|
|
60661
60787
|
return this.mergeMetadata(runId, metadata);
|
|
60662
60788
|
}
|
|
60663
|
-
stampRunIdentity(runId, definitionDigest, workflowVersion) {
|
|
60789
|
+
stampRunIdentity(runId, definitionDigest, workflowVersion, source) {
|
|
60790
|
+
const sourceSet = source === undefined ? "" : `,
|
|
60791
|
+
'$.definitionSource.path', ?4,
|
|
60792
|
+
'$.definitionSource.layer', ?5,
|
|
60793
|
+
'$.definitionSource.workdir', ?6`;
|
|
60664
60794
|
return this.adapter.run(`UPDATE runs
|
|
60665
60795
|
SET metadata_json = json_set(
|
|
60666
60796
|
COALESCE(NULLIF(metadata_json, ''), '{}'),
|
|
60667
60797
|
'$.definitionDigest', ?1,
|
|
60668
|
-
'$.workflowVersion', ?2
|
|
60798
|
+
'$.workflowVersion', ?2${sourceSet}
|
|
60669
60799
|
)
|
|
60670
|
-
WHERE id = ?3
|
|
60800
|
+
WHERE id = ?3
|
|
60801
|
+
AND json_type(COALESCE(NULLIF(metadata_json, ''), '{}'), '$.definitionDigest') IS NULL`, definitionDigest, workflowVersion, runId, ...source !== undefined ? [source.path, source.layer, source.workdir] : []);
|
|
60671
60802
|
}
|
|
60672
60803
|
stampFailureReason(runId, reason) {
|
|
60673
60804
|
return this.adapter.run(`UPDATE runs
|
|
@@ -68066,6 +68197,208 @@ async function enqueueCoalesced(db2, spec) {
|
|
|
68066
68197
|
}
|
|
68067
68198
|
return { status: "enqueued", jobId: fallback.id, payload: spec.payload };
|
|
68068
68199
|
}
|
|
68200
|
+
async function queryQueueJobs(db2, spec = {}) {
|
|
68201
|
+
const limit = Math.min(500, Math.max(1, spec.limit ?? 100));
|
|
68202
|
+
const offset = Math.max(0, spec.offset ?? 0);
|
|
68203
|
+
let sinceMs;
|
|
68204
|
+
if (spec.since !== undefined) {
|
|
68205
|
+
sinceMs = typeof spec.since === "number" ? spec.since : Date.parse(spec.since);
|
|
68206
|
+
if (Number.isNaN(sinceMs))
|
|
68207
|
+
sinceMs = undefined;
|
|
68208
|
+
}
|
|
68209
|
+
try {
|
|
68210
|
+
const countConditions = ["1=1"];
|
|
68211
|
+
const countParams = [];
|
|
68212
|
+
if (sinceMs !== undefined) {
|
|
68213
|
+
countConditions.push("created_at >= ?");
|
|
68214
|
+
countParams.push(sinceMs);
|
|
68215
|
+
}
|
|
68216
|
+
const countRows = await db2.queryAll(`SELECT status, COUNT(*) AS cnt
|
|
68217
|
+
FROM queue_jobs
|
|
68218
|
+
WHERE ${countConditions.join(" AND ")}
|
|
68219
|
+
GROUP BY status`, ...countParams);
|
|
68220
|
+
const countsByStatus = {
|
|
68221
|
+
all: 0,
|
|
68222
|
+
pending: 0,
|
|
68223
|
+
processing: 0,
|
|
68224
|
+
completed: 0,
|
|
68225
|
+
failed: 0
|
|
68226
|
+
};
|
|
68227
|
+
for (const row of countRows) {
|
|
68228
|
+
const count3 = Number(row.cnt);
|
|
68229
|
+
countsByStatus.all += count3;
|
|
68230
|
+
if (row.status === "pending")
|
|
68231
|
+
countsByStatus.pending = count3;
|
|
68232
|
+
else if (row.status === "processing")
|
|
68233
|
+
countsByStatus.processing = count3;
|
|
68234
|
+
else if (row.status === "completed")
|
|
68235
|
+
countsByStatus.completed = count3;
|
|
68236
|
+
else if (row.status === "failed")
|
|
68237
|
+
countsByStatus.failed = count3;
|
|
68238
|
+
}
|
|
68239
|
+
const queryConditions = ["1=1"];
|
|
68240
|
+
const queryParams = [];
|
|
68241
|
+
if (spec.status !== undefined) {
|
|
68242
|
+
queryConditions.push("status = ?");
|
|
68243
|
+
queryParams.push(spec.status);
|
|
68244
|
+
}
|
|
68245
|
+
if (sinceMs !== undefined) {
|
|
68246
|
+
queryConditions.push("created_at >= ?");
|
|
68247
|
+
queryParams.push(sinceMs);
|
|
68248
|
+
}
|
|
68249
|
+
queryParams.push(limit + 1);
|
|
68250
|
+
queryParams.push(offset);
|
|
68251
|
+
const rows = await db2.queryAll(`SELECT id, type, payload, status, attempts, max_retries,
|
|
68252
|
+
created_at, updated_at, next_retry_at, last_error,
|
|
68253
|
+
processing_at, expires_at
|
|
68254
|
+
FROM queue_jobs
|
|
68255
|
+
WHERE ${queryConditions.join(" AND ")}
|
|
68256
|
+
ORDER BY created_at DESC, id DESC
|
|
68257
|
+
LIMIT ? OFFSET ?`, ...queryParams);
|
|
68258
|
+
const hasMore = rows.length > limit;
|
|
68259
|
+
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
68260
|
+
const jobs = pageRows.map((r) => {
|
|
68261
|
+
const queuedAt = new Date(r.created_at).toISOString();
|
|
68262
|
+
const startedAt = r.processing_at ? new Date(r.processing_at).toISOString() : null;
|
|
68263
|
+
const endedAt = r.status === "completed" || r.status === "failed" ? new Date(r.updated_at).toISOString() : null;
|
|
68264
|
+
const durationMs = startedAt && endedAt && r.processing_at != null ? r.updated_at - r.processing_at : null;
|
|
68265
|
+
let parsedPayload = null;
|
|
68266
|
+
if (r.payload) {
|
|
68267
|
+
try {
|
|
68268
|
+
const parsed = JSON.parse(r.payload);
|
|
68269
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
68270
|
+
parsedPayload = parsed;
|
|
68271
|
+
}
|
|
68272
|
+
} catch {
|
|
68273
|
+
parsedPayload = null;
|
|
68274
|
+
}
|
|
68275
|
+
}
|
|
68276
|
+
return {
|
|
68277
|
+
id: r.id,
|
|
68278
|
+
type: r.type,
|
|
68279
|
+
status: r.status,
|
|
68280
|
+
attempts: r.attempts,
|
|
68281
|
+
maxRetries: r.max_retries,
|
|
68282
|
+
queuedAt,
|
|
68283
|
+
startedAt,
|
|
68284
|
+
endedAt,
|
|
68285
|
+
durationMs,
|
|
68286
|
+
lastError: r.last_error ?? null,
|
|
68287
|
+
payload: parsedPayload
|
|
68288
|
+
};
|
|
68289
|
+
});
|
|
68290
|
+
const total = spec.status !== undefined ? countsByStatus[spec.status] ?? 0 : countsByStatus.all;
|
|
68291
|
+
return {
|
|
68292
|
+
jobs,
|
|
68293
|
+
total,
|
|
68294
|
+
hasMore,
|
|
68295
|
+
countsByStatus
|
|
68296
|
+
};
|
|
68297
|
+
} catch (error51) {
|
|
68298
|
+
if (error51 instanceof Error && error51.message.includes("no such table: queue_jobs")) {
|
|
68299
|
+
return {
|
|
68300
|
+
jobs: [],
|
|
68301
|
+
total: 0,
|
|
68302
|
+
hasMore: false,
|
|
68303
|
+
countsByStatus: { all: 0, pending: 0, processing: 0, completed: 0, failed: 0 }
|
|
68304
|
+
};
|
|
68305
|
+
}
|
|
68306
|
+
throw error51;
|
|
68307
|
+
}
|
|
68308
|
+
}
|
|
68309
|
+
async function queueJobKpis(db2, sinceMs, untilMs) {
|
|
68310
|
+
try {
|
|
68311
|
+
const countsRows = await db2.queryAll(`SELECT status, COUNT(*) AS cnt
|
|
68312
|
+
FROM queue_jobs
|
|
68313
|
+
WHERE created_at >= ?1 AND created_at < ?2
|
|
68314
|
+
GROUP BY status`, sinceMs, untilMs);
|
|
68315
|
+
let pending = 0;
|
|
68316
|
+
let processing = 0;
|
|
68317
|
+
let completed = 0;
|
|
68318
|
+
let failed = 0;
|
|
68319
|
+
for (const row of countsRows) {
|
|
68320
|
+
const count3 = Number(row.cnt);
|
|
68321
|
+
if (row.status === "pending")
|
|
68322
|
+
pending = count3;
|
|
68323
|
+
else if (row.status === "processing")
|
|
68324
|
+
processing = count3;
|
|
68325
|
+
else if (row.status === "completed")
|
|
68326
|
+
completed = count3;
|
|
68327
|
+
else if (row.status === "failed")
|
|
68328
|
+
failed = count3;
|
|
68329
|
+
}
|
|
68330
|
+
const activeJobs = pending + processing;
|
|
68331
|
+
const totalTerminal = completed + failed;
|
|
68332
|
+
const successRatePct = totalTerminal > 0 ? Math.round(completed / totalTerminal * 100) : 0;
|
|
68333
|
+
const errorRows = await db2.queryAll(`SELECT id, type, updated_at, last_error
|
|
68334
|
+
FROM queue_jobs
|
|
68335
|
+
WHERE status = 'failed' AND created_at >= ?1 AND created_at < ?2
|
|
68336
|
+
ORDER BY updated_at DESC, id DESC
|
|
68337
|
+
LIMIT 10`, sinceMs, untilMs);
|
|
68338
|
+
const recentJobErrors = errorRows.map((r) => ({
|
|
68339
|
+
id: r.id,
|
|
68340
|
+
name: r.type,
|
|
68341
|
+
occurredAt: new Date(r.updated_at).toISOString(),
|
|
68342
|
+
message: r.last_error ?? "Job failed with no error message"
|
|
68343
|
+
}));
|
|
68344
|
+
return {
|
|
68345
|
+
activeJobs,
|
|
68346
|
+
completedJobs: completed,
|
|
68347
|
+
failedJobs: failed,
|
|
68348
|
+
successRatePct,
|
|
68349
|
+
recentJobErrors
|
|
68350
|
+
};
|
|
68351
|
+
} catch (error51) {
|
|
68352
|
+
if (error51 instanceof Error && error51.message.includes("no such table: queue_jobs")) {
|
|
68353
|
+
return {
|
|
68354
|
+
activeJobs: 0,
|
|
68355
|
+
completedJobs: 0,
|
|
68356
|
+
failedJobs: 0,
|
|
68357
|
+
successRatePct: 0,
|
|
68358
|
+
recentJobErrors: []
|
|
68359
|
+
};
|
|
68360
|
+
}
|
|
68361
|
+
throw error51;
|
|
68362
|
+
}
|
|
68363
|
+
}
|
|
68364
|
+
async function queryScheduleLastExecution(db2, source, name) {
|
|
68365
|
+
try {
|
|
68366
|
+
if (source === "builtin") {
|
|
68367
|
+
const row2 = await db2.queryFirst(`SELECT status, updated_at FROM queue_jobs WHERE type = ? ORDER BY created_at DESC, id DESC LIMIT 1`, name);
|
|
68368
|
+
return row2 ?? null;
|
|
68369
|
+
}
|
|
68370
|
+
const row = await db2.queryFirst(`SELECT status, updated_at FROM queue_jobs
|
|
68371
|
+
WHERE type = 'scheduler.custom'
|
|
68372
|
+
AND json_extract(payload, '$.name') = ?
|
|
68373
|
+
ORDER BY created_at DESC, id DESC LIMIT 1`, name);
|
|
68374
|
+
return row ?? null;
|
|
68375
|
+
} catch (error51) {
|
|
68376
|
+
if (error51 instanceof Error && error51.message.includes("no such table: queue_jobs")) {
|
|
68377
|
+
return null;
|
|
68378
|
+
}
|
|
68379
|
+
throw error51;
|
|
68380
|
+
}
|
|
68381
|
+
}
|
|
68382
|
+
async function findActiveSchedulerCustomJob(db2, name) {
|
|
68383
|
+
const row = await db2.queryFirst(`SELECT id FROM queue_jobs
|
|
68384
|
+
WHERE type = 'scheduler.custom' AND status IN ('pending', 'processing')
|
|
68385
|
+
AND json_extract(payload, '$.name') = ?
|
|
68386
|
+
LIMIT 1`, name);
|
|
68387
|
+
return row ?? undefined;
|
|
68388
|
+
}
|
|
68389
|
+
async function failOrphanedProcessingJobs(db2, now) {
|
|
68390
|
+
const orphan = await db2.queryFirst(`SELECT count(*) AS cnt FROM queue_jobs WHERE status = 'processing'`);
|
|
68391
|
+
const count3 = orphan?.cnt ?? 0;
|
|
68392
|
+
if (count3 > 0) {
|
|
68393
|
+
await db2.run(`UPDATE queue_jobs
|
|
68394
|
+
SET status = 'failed',
|
|
68395
|
+
last_error = 'Process terminated: server restarted while job was in flight',
|
|
68396
|
+
processing_at = NULL,
|
|
68397
|
+
updated_at = ?
|
|
68398
|
+
WHERE status = 'processing'`, now);
|
|
68399
|
+
}
|
|
68400
|
+
return count3;
|
|
68401
|
+
}
|
|
68069
68402
|
var SQLITE_BUSY_TIMEOUT_MS = 30000;
|
|
68070
68403
|
var init_db2 = __esm(() => {
|
|
68071
68404
|
init_migrations();
|
|
@@ -70009,6 +70342,9 @@ function buildTaskSkeleton(options) {
|
|
|
70009
70342
|
`).trimEnd()}
|
|
70010
70343
|
`;
|
|
70011
70344
|
}
|
|
70345
|
+
function serializeTaskFrontmatter(data) {
|
|
70346
|
+
return $stringify(data, { lineWidth: 0 });
|
|
70347
|
+
}
|
|
70012
70348
|
function extractTemplateBodies(templateMarkdown) {
|
|
70013
70349
|
const doc2 = MarkdownDocument.parse(templateMarkdown, "task");
|
|
70014
70350
|
const bodies = {};
|
|
@@ -70037,6 +70373,7 @@ function renderTaskTemplate(templateContent, vars) {
|
|
|
70037
70373
|
}
|
|
70038
70374
|
var sectionNameSchema, matrixEntrySchema, matrixStatusSchema, sectionMatrixSchema, SECTION_GUIDANCE, CANONICAL_INDEX;
|
|
70039
70375
|
var init_task_skeleton = __esm(() => {
|
|
70376
|
+
init_dist2();
|
|
70040
70377
|
init_zod();
|
|
70041
70378
|
init_markdown_document();
|
|
70042
70379
|
sectionNameSchema = exports_external.enum(TASK_CANONICAL_SECTIONS);
|
|
@@ -70351,6 +70688,7 @@ __export(exports_src, {
|
|
|
70351
70688
|
sessionToolDurations: () => sessionToolDurations,
|
|
70352
70689
|
sessionTimeline: () => sessionTimeline,
|
|
70353
70690
|
sessionSpans: () => sessionSpans,
|
|
70691
|
+
serializeTaskFrontmatter: () => serializeTaskFrontmatter,
|
|
70354
70692
|
serializeStablePrefix: () => serializeStablePrefix,
|
|
70355
70693
|
serializeLayer: () => serializeLayer,
|
|
70356
70694
|
serializeEnvelope: () => serializeEnvelope,
|
|
@@ -70378,6 +70716,9 @@ __export(exports_src, {
|
|
|
70378
70716
|
readRollupWatermarks: () => readRollupWatermarks,
|
|
70379
70717
|
readRecordedImporterSchemaVersion: () => readRecordedImporterSchemaVersion,
|
|
70380
70718
|
readLockMetadata: () => readLockMetadata,
|
|
70719
|
+
queueJobKpis: () => queueJobKpis,
|
|
70720
|
+
queryScheduleLastExecution: () => queryScheduleLastExecution,
|
|
70721
|
+
queryQueueJobs: () => queryQueueJobs,
|
|
70381
70722
|
projectSnapshotSchema: () => projectSnapshotSchema,
|
|
70382
70723
|
passAllResolver: () => passAllResolver,
|
|
70383
70724
|
parseTodoItems: () => parseTodoItems,
|
|
@@ -70448,9 +70789,11 @@ __export(exports_src, {
|
|
|
70448
70789
|
formatRatio: () => formatRatio,
|
|
70449
70790
|
foldTotals: () => foldTotals,
|
|
70450
70791
|
findPendingQueueJob: () => findPendingQueueJob,
|
|
70792
|
+
findActiveSchedulerCustomJob: () => findActiveSchedulerCustomJob,
|
|
70451
70793
|
featureStatusIcon: () => featureStatusIcon,
|
|
70452
70794
|
featureSnapshotSchema: () => featureSnapshotSchema,
|
|
70453
70795
|
featureFrontmatterSchema: () => featureFrontmatterSchema,
|
|
70796
|
+
failOrphanedProcessingJobs: () => failOrphanedProcessingJobs,
|
|
70454
70797
|
extractTemplateBodies: () => extractTemplateBodies,
|
|
70455
70798
|
extractStablePrefix: () => extractStablePrefix,
|
|
70456
70799
|
extractPhases: () => extractPhases,
|
|
@@ -74601,7 +74944,7 @@ class PlanningCheckService {
|
|
|
74601
74944
|
}
|
|
74602
74945
|
findings.push(...structuralFindings(raw, this.docKind));
|
|
74603
74946
|
}
|
|
74604
|
-
summarizeWithStatus(status, findings, strict, overrides, accepted, id) {
|
|
74947
|
+
summarizeWithStatus(status, findings, strict, overrides, accepted, id, requiredList) {
|
|
74605
74948
|
const effectiveFindings = [];
|
|
74606
74949
|
const notes = [];
|
|
74607
74950
|
for (const f of findings) {
|
|
@@ -74643,12 +74986,24 @@ class PlanningCheckService {
|
|
|
74643
74986
|
break;
|
|
74644
74987
|
}
|
|
74645
74988
|
}
|
|
74646
|
-
const requiredSections = [];
|
|
74647
74989
|
const missingSections = [];
|
|
74648
|
-
|
|
74649
|
-
|
|
74650
|
-
|
|
74651
|
-
|
|
74990
|
+
let requiredSections;
|
|
74991
|
+
if (requiredList !== undefined) {
|
|
74992
|
+
requiredSections = [...requiredList];
|
|
74993
|
+
const seen = new Set;
|
|
74994
|
+
for (const f of effectiveFindings) {
|
|
74995
|
+
if (f.layer === "L2" && f.section && f.message.startsWith("Missing required") && !seen.has(f.section)) {
|
|
74996
|
+
seen.add(f.section);
|
|
74997
|
+
missingSections.push(f.section);
|
|
74998
|
+
}
|
|
74999
|
+
}
|
|
75000
|
+
} else {
|
|
75001
|
+
requiredSections = [];
|
|
75002
|
+
for (const f of effectiveFindings) {
|
|
75003
|
+
if (f.layer === "L2" && f.section && f.message.startsWith("Missing required")) {
|
|
75004
|
+
requiredSections.push(f.section);
|
|
75005
|
+
missingSections.push(f.section);
|
|
75006
|
+
}
|
|
74652
75007
|
}
|
|
74653
75008
|
}
|
|
74654
75009
|
return { status, findings: effectiveFindings, requiredSections, missingSections, pass: !hasError, notes };
|
|
@@ -76069,13 +76424,29 @@ var init_task_check = __esm(() => {
|
|
|
76069
76424
|
}
|
|
76070
76425
|
return {
|
|
76071
76426
|
wbs,
|
|
76072
|
-
...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs),
|
|
76427
|
+
...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs, entry?.required),
|
|
76073
76428
|
repairs
|
|
76074
76429
|
};
|
|
76075
76430
|
}
|
|
76431
|
+
checkContentPolicy(raw, wbs, options) {
|
|
76432
|
+
const findings = [];
|
|
76433
|
+
const doc2 = this.runL1(raw, wbs, findings);
|
|
76434
|
+
if (doc2 === null) {
|
|
76435
|
+
return { doc: null, findings };
|
|
76436
|
+
}
|
|
76437
|
+
const fm = doc2.frontmatterData ?? {};
|
|
76438
|
+
const effectiveStatus = options?.asStatus ?? fm.status ?? "backlog";
|
|
76439
|
+
const variant = fm.template ?? DEFAULT_TASK_VARIANT;
|
|
76440
|
+
const entry = this.resolveMatrixEntry(variant, effectiveStatus);
|
|
76441
|
+
this.runL2(doc2, entry, findings, raw);
|
|
76442
|
+
this.runL3(doc2, entry, effectiveStatus, findings);
|
|
76443
|
+
const { findings: effectiveFindings } = this.summarizeWithStatus(effectiveStatus, findings, options?.strict, options?.severityOverrides, undefined, wbs, entry?.required);
|
|
76444
|
+
return { doc: doc2, findings: effectiveFindings, entry };
|
|
76445
|
+
}
|
|
76076
76446
|
runL3(doc2, entry, status, findings) {
|
|
76447
|
+
const requiredAtStatus = new Set(entry?.required ?? []);
|
|
76077
76448
|
const reqBodyRaw = doc2.getSection("Requirements");
|
|
76078
|
-
if (reqBodyRaw !== null && isPlaceholderBody(reqBodyRaw)) {
|
|
76449
|
+
if (requiredAtStatus.has("Requirements") && reqBodyRaw !== null && isPlaceholderBody(reqBodyRaw)) {
|
|
76079
76450
|
findings.push({
|
|
76080
76451
|
layer: "L3",
|
|
76081
76452
|
code: FINDING_CODES.L3_REQUIREMENTS_EMPTY,
|
|
@@ -76085,7 +76456,7 @@ var init_task_check = __esm(() => {
|
|
|
76085
76456
|
});
|
|
76086
76457
|
}
|
|
76087
76458
|
const acBodyRaw = doc2.getSection("Acceptance Criteria");
|
|
76088
|
-
if (acBodyRaw !== null && isPlaceholderBody(stripAcFence(acBodyRaw))) {
|
|
76459
|
+
if (requiredAtStatus.has("Acceptance Criteria") && acBodyRaw !== null && isPlaceholderBody(stripAcFence(acBodyRaw))) {
|
|
76089
76460
|
findings.push({
|
|
76090
76461
|
layer: "L3",
|
|
76091
76462
|
code: FINDING_CODES.L3_AC_EMPTY,
|
|
@@ -76201,9 +76572,8 @@ var init_task_check = __esm(() => {
|
|
|
76201
76572
|
});
|
|
76202
76573
|
}
|
|
76203
76574
|
}
|
|
76204
|
-
const requiredSections = new Set(entry?.required ?? []);
|
|
76205
76575
|
for (const sectionName of ["Testing", "Solution"]) {
|
|
76206
|
-
if (!
|
|
76576
|
+
if (!requiredAtStatus.has(sectionName))
|
|
76207
76577
|
continue;
|
|
76208
76578
|
const body = doc2.getSection(sectionName);
|
|
76209
76579
|
if (body === null || !isPlaceholderBody(body))
|
|
@@ -83503,7 +83873,7 @@ class HistoryService {
|
|
|
83503
83873
|
...opts.file !== undefined && opts.file.length > 0 ? { files: [opts.file] } : opts.root !== undefined && opts.root.length > 0 ? { roots: [opts.root] } : { roots: discovery?.roots ?? [] },
|
|
83504
83874
|
dryRun
|
|
83505
83875
|
});
|
|
83506
|
-
if (!dryRun) {
|
|
83876
|
+
if (!dryRun && result.importedRecords > 0) {
|
|
83507
83877
|
for (const runRoot of discovery?.runRoots ?? []) {
|
|
83508
83878
|
const sourceFilePrefix = runRoot.root.endsWith(sep) ? runRoot.root : `${runRoot.root}${sep}`;
|
|
83509
83879
|
await dao2.observeImportedSessions({
|
|
@@ -86232,25 +86602,36 @@ function validateSchedulerCustomJobPayload(raw) {
|
|
|
86232
86602
|
}
|
|
86233
86603
|
async function handleSchedulerCustomJob(deps, job) {
|
|
86234
86604
|
const payload = validateSchedulerCustomJobPayload(job.payload);
|
|
86235
|
-
|
|
86236
|
-
|
|
86237
|
-
args: ["-c", payload.command],
|
|
86238
|
-
cwd: deps.cwd,
|
|
86239
|
-
timeout: deps.timeoutMs ?? SCHEDULER_CUSTOM_TIMEOUT_MS,
|
|
86240
|
-
maxOutput: SCHEDULER_CUSTOM_MAX_OUTPUT,
|
|
86241
|
-
forceBuffered: true,
|
|
86242
|
-
rejectOnError: false
|
|
86243
|
-
});
|
|
86244
|
-
const detail = outputTail2(result.stderr) || outputTail2(result.stdout);
|
|
86245
|
-
if (result.exitCode === null) {
|
|
86246
|
-
const signalDetail = result.signal === undefined ? "" : ` (${result.signal})`;
|
|
86247
|
-
throw new Error(`scheduler job "${payload.name}" terminated before a normal exit${signalDetail}${detail}`);
|
|
86605
|
+
if (activeJobs.has(payload.name)) {
|
|
86606
|
+
throw new Error(`scheduler job "${payload.name}" is already running in this process; skipping duplicate execution`);
|
|
86248
86607
|
}
|
|
86249
|
-
|
|
86250
|
-
|
|
86608
|
+
activeJobs.add(payload.name);
|
|
86609
|
+
try {
|
|
86610
|
+
const result = await deps.executor.run({
|
|
86611
|
+
command: "/bin/sh",
|
|
86612
|
+
args: ["-c", payload.command],
|
|
86613
|
+
cwd: deps.cwd,
|
|
86614
|
+
timeout: deps.timeoutMs ?? SCHEDULER_CUSTOM_TIMEOUT_MS,
|
|
86615
|
+
maxOutput: SCHEDULER_CUSTOM_MAX_OUTPUT,
|
|
86616
|
+
forceBuffered: true,
|
|
86617
|
+
rejectOnError: false
|
|
86618
|
+
});
|
|
86619
|
+
const detail = outputTail2(result.stderr) || outputTail2(result.stdout);
|
|
86620
|
+
if (result.exitCode === null) {
|
|
86621
|
+
const signalDetail = result.signal === undefined ? "" : ` (${result.signal})`;
|
|
86622
|
+
throw new Error(`scheduler job "${payload.name}" terminated before a normal exit${signalDetail}${detail}`);
|
|
86623
|
+
}
|
|
86624
|
+
if (result.exitCode !== 0) {
|
|
86625
|
+
throw new Error(`scheduler job "${payload.name}" exited ${result.exitCode}${detail}`);
|
|
86626
|
+
}
|
|
86627
|
+
} finally {
|
|
86628
|
+
activeJobs.delete(payload.name);
|
|
86251
86629
|
}
|
|
86252
86630
|
}
|
|
86253
|
-
var SCHEDULER_CUSTOM_JOB = "scheduler.custom", SCHEDULER_CUSTOM_TIMEOUT_MS = 3600000, SCHEDULER_CUSTOM_MAX_OUTPUT = 1e6;
|
|
86631
|
+
var SCHEDULER_CUSTOM_JOB = "scheduler.custom", SCHEDULER_CUSTOM_TIMEOUT_MS = 3600000, SCHEDULER_CUSTOM_MAX_OUTPUT = 1e6, activeJobs;
|
|
86632
|
+
var init_scheduler_custom_job_service = __esm(() => {
|
|
86633
|
+
activeJobs = new Set;
|
|
86634
|
+
});
|
|
86254
86635
|
|
|
86255
86636
|
// ../../packages/app/src/services/supervisor-service.ts
|
|
86256
86637
|
function defaultWrapperArgv(agentId) {
|
|
@@ -86716,6 +87097,248 @@ var init_system_event_follow = __esm(() => {
|
|
|
86716
87097
|
init_src2();
|
|
86717
87098
|
});
|
|
86718
87099
|
|
|
87100
|
+
// ../../packages/app/src/services/task-readiness.ts
|
|
87101
|
+
import { createHash as createHash8 } from "crypto";
|
|
87102
|
+
function readyRefineCommand(wbs) {
|
|
87103
|
+
return `/sp:dev-refine ${wbs} --auto --depth ready`;
|
|
87104
|
+
}
|
|
87105
|
+
function readyRefineAllCommand(featureId2) {
|
|
87106
|
+
return `/sp:dev-refineall --feature ${featureId2} --auto --depth ready`;
|
|
87107
|
+
}
|
|
87108
|
+
async function prepareCreatedTaskReady(opts) {
|
|
87109
|
+
const { wbs, tasks, agents, checkTask } = opts;
|
|
87110
|
+
const recoveryCommand = readyRefineCommand(wbs);
|
|
87111
|
+
const before = await tasks.show(wbs).catch((err) => {
|
|
87112
|
+
throw new TaskPreparationError({
|
|
87113
|
+
stage: "post-check",
|
|
87114
|
+
message: `saved task ${wbs} is unreadable before preparation: ${err instanceof Error ? err.message : String(err)}`,
|
|
87115
|
+
wbs,
|
|
87116
|
+
recoveryCommand
|
|
87117
|
+
});
|
|
87118
|
+
});
|
|
87119
|
+
const flags = {
|
|
87120
|
+
mode: "text",
|
|
87121
|
+
timeout: String(opts.timeoutMs ?? DEFAULT_READY_PREPARE_TIMEOUT_MS)
|
|
87122
|
+
};
|
|
87123
|
+
if (opts.agentSelector !== undefined && opts.agentSelector !== "")
|
|
87124
|
+
flags.agent = opts.agentSelector;
|
|
87125
|
+
if (opts.cwd !== undefined && opts.cwd !== "")
|
|
87126
|
+
flags.cwd = opts.cwd;
|
|
87127
|
+
const traced = await agents.runTraced(readyRefineCommand(wbs), flags);
|
|
87128
|
+
if (traced.exitCode === 2) {
|
|
87129
|
+
throw new TaskPreparationError({
|
|
87130
|
+
stage: "agent-dispatch",
|
|
87131
|
+
message: `ready preparation could not dispatch an executor for ${wbs}: ${traced.message ?? "agent resolution failed"}`,
|
|
87132
|
+
wbs,
|
|
87133
|
+
filePath: before.filePath,
|
|
87134
|
+
recoveryCommand
|
|
87135
|
+
});
|
|
87136
|
+
}
|
|
87137
|
+
if (traced.exitCode !== 0) {
|
|
87138
|
+
const why = traced.signal !== undefined ? `terminated by signal ${traced.signal}` : `executor exited ${traced.exitCode}`;
|
|
87139
|
+
throw new TaskPreparationError({
|
|
87140
|
+
stage: "agent-run",
|
|
87141
|
+
message: `ready preparation failed for ${wbs}: ${why}. Partial section edits are preserved on the task.`,
|
|
87142
|
+
wbs,
|
|
87143
|
+
filePath: before.filePath,
|
|
87144
|
+
recoveryCommand
|
|
87145
|
+
});
|
|
87146
|
+
}
|
|
87147
|
+
const after = await tasks.show(wbs).catch((err) => {
|
|
87148
|
+
throw new TaskPreparationError({
|
|
87149
|
+
stage: "post-check",
|
|
87150
|
+
message: `saved task ${wbs} is unreadable after preparation: ${err instanceof Error ? err.message : String(err)}`,
|
|
87151
|
+
wbs,
|
|
87152
|
+
filePath: before.filePath,
|
|
87153
|
+
recoveryCommand
|
|
87154
|
+
});
|
|
87155
|
+
});
|
|
87156
|
+
const check2 = await checkTask(after.filePath, wbs).catch((err) => {
|
|
87157
|
+
throw new TaskPreparationError({
|
|
87158
|
+
stage: "post-check",
|
|
87159
|
+
message: `post-check for ${wbs} could not run: ${err instanceof Error ? err.message : String(err)}`,
|
|
87160
|
+
wbs,
|
|
87161
|
+
filePath: after.filePath,
|
|
87162
|
+
recoveryCommand
|
|
87163
|
+
});
|
|
87164
|
+
});
|
|
87165
|
+
if (!check2.pass) {
|
|
87166
|
+
const summary = check2.findings.filter((f) => f.severity === "error").map((f) => `${f.layer}:${f.code} ${f.section !== undefined ? `(${f.section}) ` : ""}${f.message}`).slice(0, 5).join("; ");
|
|
87167
|
+
throw new TaskPreparationError({
|
|
87168
|
+
stage: "post-check",
|
|
87169
|
+
message: `ready post-check failed for ${wbs}: ${summary || "check did not pass"}`,
|
|
87170
|
+
wbs,
|
|
87171
|
+
filePath: after.filePath,
|
|
87172
|
+
findings: check2.findings,
|
|
87173
|
+
recoveryCommand
|
|
87174
|
+
});
|
|
87175
|
+
}
|
|
87176
|
+
if (after.status !== "todo") {
|
|
87177
|
+
try {
|
|
87178
|
+
await tasks.updateStatus(wbs, "todo", opts.actor ?? "ready-preparation");
|
|
87179
|
+
} catch (err) {
|
|
87180
|
+
throw new TaskPreparationError({
|
|
87181
|
+
stage: "promotion",
|
|
87182
|
+
message: `ready preparation could not promote ${wbs} to todo: ${err instanceof Error ? err.message : String(err)}`,
|
|
87183
|
+
wbs,
|
|
87184
|
+
filePath: after.filePath,
|
|
87185
|
+
recoveryCommand
|
|
87186
|
+
});
|
|
87187
|
+
}
|
|
87188
|
+
}
|
|
87189
|
+
return READY_DONE;
|
|
87190
|
+
}
|
|
87191
|
+
function extractBatchArray(answer) {
|
|
87192
|
+
const attempt = (text4) => {
|
|
87193
|
+
try {
|
|
87194
|
+
return JSON.parse(text4);
|
|
87195
|
+
} catch {
|
|
87196
|
+
return;
|
|
87197
|
+
}
|
|
87198
|
+
};
|
|
87199
|
+
const direct = attempt(answer);
|
|
87200
|
+
if (direct !== undefined)
|
|
87201
|
+
return direct;
|
|
87202
|
+
const start = answer.indexOf("[");
|
|
87203
|
+
const end = answer.lastIndexOf("]");
|
|
87204
|
+
if (start === -1 || end <= start) {
|
|
87205
|
+
throw new TaskPreparationError({
|
|
87206
|
+
stage: "invalid-output",
|
|
87207
|
+
message: "prepared batch output carried no JSON array"
|
|
87208
|
+
});
|
|
87209
|
+
}
|
|
87210
|
+
const sliced = attempt(answer.slice(start, end + 1));
|
|
87211
|
+
if (sliced === undefined) {
|
|
87212
|
+
throw new TaskPreparationError({
|
|
87213
|
+
stage: "invalid-output",
|
|
87214
|
+
message: "prepared batch output is not valid JSON"
|
|
87215
|
+
});
|
|
87216
|
+
}
|
|
87217
|
+
return sliced;
|
|
87218
|
+
}
|
|
87219
|
+
async function prepareBatchTaskReady(opts) {
|
|
87220
|
+
const { agents } = opts;
|
|
87221
|
+
const flags = {
|
|
87222
|
+
mode: "text",
|
|
87223
|
+
timeout: String(opts.timeoutMs ?? DEFAULT_READY_PREPARE_TIMEOUT_MS)
|
|
87224
|
+
};
|
|
87225
|
+
if (opts.agentSelector !== undefined && opts.agentSelector !== "")
|
|
87226
|
+
flags.agent = opts.agentSelector;
|
|
87227
|
+
if (opts.cwd !== undefined && opts.cwd !== "")
|
|
87228
|
+
flags.cwd = opts.cwd;
|
|
87229
|
+
let recoveryCommand;
|
|
87230
|
+
try {
|
|
87231
|
+
const supplied = JSON.parse(opts.batchSource);
|
|
87232
|
+
if (Array.isArray(supplied)) {
|
|
87233
|
+
for (const item of supplied) {
|
|
87234
|
+
if (typeof item === "object" && item !== null && "feature_id" in item) {
|
|
87235
|
+
const featureId2 = item.feature_id;
|
|
87236
|
+
if (typeof featureId2 === "string" && featureId2 !== "") {
|
|
87237
|
+
recoveryCommand = readyRefineAllCommand(featureId2);
|
|
87238
|
+
break;
|
|
87239
|
+
}
|
|
87240
|
+
}
|
|
87241
|
+
}
|
|
87242
|
+
}
|
|
87243
|
+
} catch {}
|
|
87244
|
+
const prompt = "Prepare this task batch to the ready checklist (requirements, design, plan, ac, decisions, dependencies, " + "premises). Read the batch file, assess and synthesize every candidate item in input order, preserving " + "names, feature_id, parent_wbs and authored constraints, and fill Requirements, Design, Plan and Acceptance " + "Criteria substantively (never placeholders). Never author Solution, Testing or Review sections. " + `Print ONLY the prepared JSON array \u2014 no prose, no code fence.
|
|
87245
|
+
` + `Batch file: ${opts.batchPath}
|
|
87246
|
+
` + `Batch contents:
|
|
87247
|
+
${opts.batchSource}`;
|
|
87248
|
+
const captured = await agents.runCapture(prompt, flags);
|
|
87249
|
+
if (captured.exitCode === 2) {
|
|
87250
|
+
throw new TaskPreparationError({
|
|
87251
|
+
stage: "agent-dispatch",
|
|
87252
|
+
message: `batch preparation could not dispatch an executor: ${captured.stderr || "agent resolution failed"}`,
|
|
87253
|
+
recoveryCommand
|
|
87254
|
+
});
|
|
87255
|
+
}
|
|
87256
|
+
if (captured.exitCode !== 0) {
|
|
87257
|
+
const why = captured.signal !== undefined ? `terminated by signal ${captured.signal}` : `executor exited ${captured.exitCode}`;
|
|
87258
|
+
throw new TaskPreparationError({
|
|
87259
|
+
stage: "agent-run",
|
|
87260
|
+
message: `batch preparation failed: ${why}`,
|
|
87261
|
+
recoveryCommand
|
|
87262
|
+
});
|
|
87263
|
+
}
|
|
87264
|
+
const parsed = extractBatchArray(captured.answer);
|
|
87265
|
+
const validated = taskBatchSchema.safeParse(parsed);
|
|
87266
|
+
if (!validated.success) {
|
|
87267
|
+
const issues = validated.error.issues.map((i2) => ` [${i2.path.join(".")}] ${i2.message}`).join(`
|
|
87268
|
+
`);
|
|
87269
|
+
throw new TaskPreparationError({
|
|
87270
|
+
stage: "validation",
|
|
87271
|
+
message: `prepared batch failed schema validation:
|
|
87272
|
+
${issues}`,
|
|
87273
|
+
recoveryCommand
|
|
87274
|
+
});
|
|
87275
|
+
}
|
|
87276
|
+
return { items: validated.data };
|
|
87277
|
+
}
|
|
87278
|
+
function computePlanningDigest(raw) {
|
|
87279
|
+
const doc2 = MarkdownDocument.parse(raw, "task");
|
|
87280
|
+
const fm = doc2.frontmatterData ?? {};
|
|
87281
|
+
const payload = {
|
|
87282
|
+
sections: PLANNING_DIGEST_SECTIONS.map((name) => {
|
|
87283
|
+
const body = doc2.getSection(name);
|
|
87284
|
+
return { name, body: body === null ? null : body.trim() };
|
|
87285
|
+
}),
|
|
87286
|
+
featureId: typeof fm.feature_id === "string" ? fm.feature_id : null,
|
|
87287
|
+
template: typeof fm.template === "string" ? fm.template : null,
|
|
87288
|
+
dependencies: Array.isArray(fm.dependencies) ? [...fm.dependencies].map(String).sort() : null
|
|
87289
|
+
};
|
|
87290
|
+
return createHash8("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
87291
|
+
}
|
|
87292
|
+
function verifyReadyChecks(checks4) {
|
|
87293
|
+
if (checks4 === undefined || checks4.length === 0) {
|
|
87294
|
+
return { ok: false, reason: "no ready-checklist evidence" };
|
|
87295
|
+
}
|
|
87296
|
+
for (const id of READY_CHECKLIST_IDS) {
|
|
87297
|
+
const row = checks4.find((c3) => c3.id === id);
|
|
87298
|
+
if (row === undefined)
|
|
87299
|
+
return { ok: false, reason: `checklist row "${id}" missing` };
|
|
87300
|
+
if (row.pass !== true)
|
|
87301
|
+
return { ok: false, reason: `checklist row "${id}" not passing` };
|
|
87302
|
+
if (typeof row.evidence !== "string" || row.evidence.trim() === "") {
|
|
87303
|
+
return { ok: false, reason: `checklist row "${id}" has empty evidence` };
|
|
87304
|
+
}
|
|
87305
|
+
}
|
|
87306
|
+
return { ok: true };
|
|
87307
|
+
}
|
|
87308
|
+
var READY_CHECKLIST_IDS, DEFAULT_READY_PREPARE_TIMEOUT_MS = 900000, TaskPreparationError, READY_SKIPPED, READY_DONE, EXECUTION_OWNED_SECTIONS, PLANNING_DIGEST_SECTIONS;
|
|
87309
|
+
var init_task_readiness = __esm(() => {
|
|
87310
|
+
init_src2();
|
|
87311
|
+
READY_CHECKLIST_IDS = [
|
|
87312
|
+
"requirements",
|
|
87313
|
+
"design",
|
|
87314
|
+
"plan",
|
|
87315
|
+
"ac",
|
|
87316
|
+
"decisions",
|
|
87317
|
+
"dependencies",
|
|
87318
|
+
"premises"
|
|
87319
|
+
];
|
|
87320
|
+
TaskPreparationError = class TaskPreparationError extends Error {
|
|
87321
|
+
stage;
|
|
87322
|
+
wbs;
|
|
87323
|
+
filePath;
|
|
87324
|
+
findings;
|
|
87325
|
+
recoveryCommand;
|
|
87326
|
+
constructor(init) {
|
|
87327
|
+
super(init.message);
|
|
87328
|
+
this.name = "TaskPreparationError";
|
|
87329
|
+
this.stage = init.stage;
|
|
87330
|
+
this.wbs = init.wbs;
|
|
87331
|
+
this.filePath = init.filePath;
|
|
87332
|
+
this.findings = init.findings;
|
|
87333
|
+
this.recoveryCommand = init.recoveryCommand;
|
|
87334
|
+
}
|
|
87335
|
+
};
|
|
87336
|
+
READY_SKIPPED = { readiness: { status: "skipped", depth: "ready" } };
|
|
87337
|
+
READY_DONE = { readiness: { status: "ready", depth: "ready" } };
|
|
87338
|
+
EXECUTION_OWNED_SECTIONS = { Solution: true, Testing: true, Review: true, History: true };
|
|
87339
|
+
PLANNING_DIGEST_SECTIONS = TASK_CANONICAL_SECTIONS.filter((s3) => !(s3 in EXECUTION_OWNED_SECTIONS));
|
|
87340
|
+
});
|
|
87341
|
+
|
|
86719
87342
|
// ../../packages/app/src/services/task-scaffold.ts
|
|
86720
87343
|
import { dirname as dirname13 } from "path";
|
|
86721
87344
|
|
|
@@ -86934,14 +87557,99 @@ class TaskService {
|
|
|
86934
87557
|
}
|
|
86935
87558
|
return null;
|
|
86936
87559
|
}
|
|
87560
|
+
async resolveCandidateShape(input) {
|
|
87561
|
+
const variant = input.template ?? (input.featureId !== undefined ? "feature-impl" : DEFAULT_TASK_VARIANT);
|
|
87562
|
+
let background = input.background ?? "";
|
|
87563
|
+
if (background === "" && input.featureId !== undefined) {
|
|
87564
|
+
background = await this.deriveBackground(input.featureId);
|
|
87565
|
+
}
|
|
87566
|
+
const taskBodies = {};
|
|
87567
|
+
if (background !== "")
|
|
87568
|
+
taskBodies.Background = background;
|
|
87569
|
+
if ((input.requirements ?? "").trim() !== "") {
|
|
87570
|
+
taskBodies.Requirements = bulletizeRequirements(input.requirements ?? "");
|
|
87571
|
+
}
|
|
87572
|
+
if ((input.design ?? "").trim() !== "") {
|
|
87573
|
+
taskBodies.Design = (input.design ?? "").trim();
|
|
87574
|
+
}
|
|
87575
|
+
if ((input.plan ?? "").trim() !== "") {
|
|
87576
|
+
taskBodies.Plan = (input.plan ?? "").trim();
|
|
87577
|
+
}
|
|
87578
|
+
if ((input.acceptanceCriteria ?? "").trim() !== "") {
|
|
87579
|
+
taskBodies["Acceptance Criteria"] = normalizeAcFence((input.acceptanceCriteria ?? "").trim());
|
|
87580
|
+
}
|
|
87581
|
+
if (taskBodies.Background === undefined) {
|
|
87582
|
+
taskBodies.Background = `Captured from the creation title: "${input.title}".`;
|
|
87583
|
+
}
|
|
87584
|
+
const templateBackground = this.ctx.resolveTemplateBodies?.(variant).Background;
|
|
87585
|
+
if (input.background === undefined && input.featureId === undefined && templateBackground !== undefined && templateBackground.trim() !== "") {
|
|
87586
|
+
taskBodies.Background = `${taskBodies.Background}
|
|
87587
|
+
|
|
87588
|
+
${templateBackground}`;
|
|
87589
|
+
}
|
|
87590
|
+
const now2 = new Date().toISOString();
|
|
87591
|
+
const fmFor = (status2) => ({
|
|
87592
|
+
schema_version: 1,
|
|
87593
|
+
name: input.title,
|
|
87594
|
+
status: status2,
|
|
87595
|
+
template: variant,
|
|
87596
|
+
created_at: now2,
|
|
87597
|
+
updated_at: now2,
|
|
87598
|
+
...input.featureId !== undefined ? { feature_id: input.featureId } : {},
|
|
87599
|
+
...input.parentWbs !== undefined ? { parent_wbs: input.parentWbs } : {},
|
|
87600
|
+
...input.priority !== undefined ? { priority: input.priority } : {},
|
|
87601
|
+
...input.tags !== undefined && input.tags.length > 0 ? { tags: input.tags } : {}
|
|
87602
|
+
});
|
|
87603
|
+
let status;
|
|
87604
|
+
if (input.explicitStatus !== undefined) {
|
|
87605
|
+
status = input.explicitStatus;
|
|
87606
|
+
} else {
|
|
87607
|
+
const probe = buildTaskSkeleton({
|
|
87608
|
+
wbs: CANDIDATE_PROBE_WBS,
|
|
87609
|
+
title: input.title,
|
|
87610
|
+
frontmatter: serializeTaskFrontmatter(fmFor("todo")),
|
|
87611
|
+
sections: this.sectionsForStatus(variant, "todo"),
|
|
87612
|
+
bodies: this.bodiesFor(variant, taskBodies)
|
|
87613
|
+
});
|
|
87614
|
+
const probeFindings = this.checkCandidateContent(probe, CANDIDATE_PROBE_WBS, "todo");
|
|
87615
|
+
status = probeFindings.some((f) => f.severity === "error") ? "backlog" : "todo";
|
|
87616
|
+
}
|
|
87617
|
+
return { variant, status, fm: fmFor(status), taskBodies };
|
|
87618
|
+
}
|
|
87619
|
+
checkCandidateContent(raw, wbs, asStatus) {
|
|
87620
|
+
const matrix = this.ctx.sectionMatrix;
|
|
87621
|
+
if (matrix === undefined) {
|
|
87622
|
+
throw new Error(`no section-matrix available for candidate validation (status=${asStatus}); ` + "a canonical section-matrix.yaml is required for task creation (F92 R1)");
|
|
87623
|
+
}
|
|
87624
|
+
const checker = new TaskCheckService(this.ctx.fs, matrix);
|
|
87625
|
+
return checker.checkContentPolicy(raw, wbs, { asStatus }).findings;
|
|
87626
|
+
}
|
|
87627
|
+
renderCandidate(title, wbs, shape) {
|
|
87628
|
+
return buildTaskSkeleton({
|
|
87629
|
+
wbs,
|
|
87630
|
+
title,
|
|
87631
|
+
frontmatter: serializeTaskFrontmatter(shape.fm),
|
|
87632
|
+
sections: this.sectionsForStatus(shape.variant, shape.status),
|
|
87633
|
+
bodies: this.bodiesFor(shape.variant, shape.taskBodies)
|
|
87634
|
+
});
|
|
87635
|
+
}
|
|
87636
|
+
validateCandidateOrThrow(title, shape) {
|
|
87637
|
+
const probe = this.renderCandidate(title, CANDIDATE_PROBE_WBS, shape);
|
|
87638
|
+
const findings = this.checkCandidateContent(probe, CANDIDATE_PROBE_WBS, shape.status);
|
|
87639
|
+
if (findings.some((f) => f.severity === "error")) {
|
|
87640
|
+
throw new TaskCandidateInvalidError(title, findings);
|
|
87641
|
+
}
|
|
87642
|
+
}
|
|
86937
87643
|
async create(params) {
|
|
86938
87644
|
const folder = this.ctx.tasksDir;
|
|
86939
|
-
|
|
86940
|
-
|
|
86941
|
-
|
|
86942
|
-
|
|
86943
|
-
|
|
86944
|
-
|
|
87645
|
+
const shape = await this.resolveCandidateShape({
|
|
87646
|
+
title: params.title,
|
|
87647
|
+
featureId: params.featureId,
|
|
87648
|
+
parentWbs: params.parentWbs,
|
|
87649
|
+
template: params.template,
|
|
87650
|
+
explicitStatus: params.status
|
|
87651
|
+
});
|
|
87652
|
+
this.validateCandidateOrThrow(params.title, shape);
|
|
86945
87653
|
return this.writeService.createAllocated(folder, async () => {
|
|
86946
87654
|
const dedupeWithinSec = params.dedupeWithinSec === null ? undefined : params.dedupeWithinSec ?? 300;
|
|
86947
87655
|
if (dedupeWithinSec !== undefined) {
|
|
@@ -86952,25 +87660,7 @@ class TaskService {
|
|
|
86952
87660
|
}
|
|
86953
87661
|
const slug = this.slugify(params.title);
|
|
86954
87662
|
const { wbs, filePath } = await this.allocateWbsChecked(slug);
|
|
86955
|
-
const
|
|
86956
|
-
const frontmatter = [
|
|
86957
|
-
"schema_version: 1",
|
|
86958
|
-
`name: "${params.title}"`,
|
|
86959
|
-
`status: ${status}`,
|
|
86960
|
-
`template: ${variant}`,
|
|
86961
|
-
`created_at: ${now2}`,
|
|
86962
|
-
`updated_at: ${now2}`,
|
|
86963
|
-
params.featureId !== undefined ? `feature_id: ${params.featureId}` : null,
|
|
86964
|
-
params.parentWbs !== undefined ? `parent_wbs: "${params.parentWbs}"` : null
|
|
86965
|
-
].filter(Boolean).join(`
|
|
86966
|
-
`);
|
|
86967
|
-
const content = buildTaskSkeleton({
|
|
86968
|
-
wbs,
|
|
86969
|
-
title: params.title,
|
|
86970
|
-
frontmatter,
|
|
86971
|
-
sections: this.sectionsForStatus(variant, status),
|
|
86972
|
-
bodies: this.bodiesFor(variant, background !== "" ? { Background: background } : {})
|
|
86973
|
-
});
|
|
87663
|
+
const content = this.renderCandidate(params.title, wbs, shape);
|
|
86974
87664
|
const ref = { kind: "task", id: wbs, filePath, folder };
|
|
86975
87665
|
return { ref, content };
|
|
86976
87666
|
});
|
|
@@ -87389,11 +88079,37 @@ class TaskService {
|
|
|
87389
88079
|
${issues}`);
|
|
87390
88080
|
}
|
|
87391
88081
|
const items = result.data;
|
|
88082
|
+
const prepared = [];
|
|
88083
|
+
for (const [index2, item] of items.entries()) {
|
|
88084
|
+
const shape = await this.resolveCandidateShape({
|
|
88085
|
+
title: item.name,
|
|
88086
|
+
featureId: item.feature_id ?? undefined,
|
|
88087
|
+
parentWbs: item.parent_wbs ?? undefined,
|
|
88088
|
+
template: item.template,
|
|
88089
|
+
explicitStatus: undefined,
|
|
88090
|
+
background: item.background,
|
|
88091
|
+
requirements: item.requirements,
|
|
88092
|
+
design: item.design,
|
|
88093
|
+
plan: item.plan,
|
|
88094
|
+
acceptanceCriteria: item.acceptance_criteria,
|
|
88095
|
+
priority: item.priority,
|
|
88096
|
+
tags: item.tags
|
|
88097
|
+
});
|
|
88098
|
+
try {
|
|
88099
|
+
this.validateCandidateOrThrow(item.name, shape);
|
|
88100
|
+
} catch (err) {
|
|
88101
|
+
if (err instanceof TaskCandidateInvalidError) {
|
|
88102
|
+
throw new TaskCandidateInvalidError(item.name, err.findings, `batch item ${index2 + 1}/${items.length}`);
|
|
88103
|
+
}
|
|
88104
|
+
throw err;
|
|
88105
|
+
}
|
|
88106
|
+
prepared.push({ item, shape });
|
|
88107
|
+
}
|
|
87392
88108
|
const writeResults = [];
|
|
87393
88109
|
const createdRefs = [];
|
|
87394
88110
|
try {
|
|
87395
|
-
for (const item of
|
|
87396
|
-
const wr = await this.createBatchItem(item);
|
|
88111
|
+
for (const { item, shape } of prepared) {
|
|
88112
|
+
const wr = await this.createBatchItem(item, shape);
|
|
87397
88113
|
writeResults.push(wr);
|
|
87398
88114
|
createdRefs.push(wr.ref);
|
|
87399
88115
|
}
|
|
@@ -87449,54 +88165,12 @@ ${issues}`);
|
|
|
87449
88165
|
}
|
|
87450
88166
|
return out;
|
|
87451
88167
|
}
|
|
87452
|
-
async createBatchItem(item) {
|
|
88168
|
+
async createBatchItem(item, shape) {
|
|
87453
88169
|
const folder = this.ctx.tasksDir;
|
|
87454
|
-
let background = item.background ?? "";
|
|
87455
|
-
if (!background && item.feature_id !== undefined && item.feature_id !== null) {
|
|
87456
|
-
background = await this.deriveBackground(item.feature_id);
|
|
87457
|
-
}
|
|
87458
|
-
const hasSpec = background !== "" || (item.requirements ?? "").trim() !== "" || (item.design ?? "").trim() !== "" || (item.plan ?? "").trim() !== "" || (item.acceptance_criteria ?? "").trim() !== "";
|
|
87459
|
-
const status = hasSpec ? "todo" : "backlog";
|
|
87460
|
-
const variant = item.template ?? (item.feature_id !== undefined && item.feature_id !== null ? "feature-impl" : DEFAULT_TASK_VARIANT);
|
|
87461
88170
|
return this.writeService.createAllocated(folder, async () => {
|
|
87462
88171
|
const slug = this.slugify(item.name);
|
|
87463
88172
|
const { wbs, filePath } = await this.allocateWbsChecked(slug);
|
|
87464
|
-
const
|
|
87465
|
-
const fmLines = [
|
|
87466
|
-
"schema_version: 1",
|
|
87467
|
-
`name: "${item.name}"`,
|
|
87468
|
-
`status: ${status}`,
|
|
87469
|
-
`template: ${variant}`,
|
|
87470
|
-
`created_at: ${now2}`,
|
|
87471
|
-
`updated_at: ${now2}`,
|
|
87472
|
-
item.feature_id !== undefined ? `feature_id: ${item.feature_id}` : null,
|
|
87473
|
-
item.parent_wbs !== undefined ? `parent_wbs: "${item.parent_wbs}"` : null,
|
|
87474
|
-
item.priority !== undefined ? `priority: ${item.priority}` : null,
|
|
87475
|
-
item.tags !== undefined && item.tags.length > 0 ? `tags: [${item.tags.map((t) => `"${t}"`).join(", ")}]` : null
|
|
87476
|
-
].filter(Boolean).join(`
|
|
87477
|
-
`);
|
|
87478
|
-
const taskBodies = {};
|
|
87479
|
-
if (background !== "")
|
|
87480
|
-
taskBodies.Background = background;
|
|
87481
|
-
if ((item.requirements ?? "").trim() !== "") {
|
|
87482
|
-
taskBodies.Requirements = bulletizeRequirements(item.requirements ?? "");
|
|
87483
|
-
}
|
|
87484
|
-
if ((item.design ?? "").trim() !== "") {
|
|
87485
|
-
taskBodies.Design = (item.design ?? "").trim();
|
|
87486
|
-
}
|
|
87487
|
-
if ((item.plan ?? "").trim() !== "") {
|
|
87488
|
-
taskBodies.Plan = (item.plan ?? "").trim();
|
|
87489
|
-
}
|
|
87490
|
-
if ((item.acceptance_criteria ?? "").trim() !== "") {
|
|
87491
|
-
taskBodies["Acceptance Criteria"] = normalizeAcFence((item.acceptance_criteria ?? "").trim());
|
|
87492
|
-
}
|
|
87493
|
-
const content = buildTaskSkeleton({
|
|
87494
|
-
wbs,
|
|
87495
|
-
title: item.name,
|
|
87496
|
-
frontmatter: fmLines,
|
|
87497
|
-
sections: this.sectionsForStatus(variant, status),
|
|
87498
|
-
bodies: this.bodiesFor(variant, taskBodies)
|
|
87499
|
-
});
|
|
88173
|
+
const content = this.renderCandidate(item.name, wbs, shape);
|
|
87500
88174
|
const ref = { kind: "task", id: wbs, filePath, folder };
|
|
87501
88175
|
return { ref, content };
|
|
87502
88176
|
});
|
|
@@ -87688,7 +88362,7 @@ ${block}` : block);
|
|
|
87688
88362
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
87689
88363
|
}
|
|
87690
88364
|
}
|
|
87691
|
-
var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, WbsCollisionError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS;
|
|
88365
|
+
var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, WbsCollisionError, TaskCandidateInvalidError, CANDIDATE_PROBE_WBS = "0000", ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS;
|
|
87692
88366
|
var init_task_service = __esm(() => {
|
|
87693
88367
|
init_src2();
|
|
87694
88368
|
init_errors7();
|
|
@@ -87737,6 +88411,16 @@ var init_task_service = __esm(() => {
|
|
|
87737
88411
|
this.attemptedPath = attemptedPath;
|
|
87738
88412
|
}
|
|
87739
88413
|
};
|
|
88414
|
+
TaskCandidateInvalidError = class TaskCandidateInvalidError extends Error {
|
|
88415
|
+
findings;
|
|
88416
|
+
constructor(title, findings, context4) {
|
|
88417
|
+
const where = context4 !== undefined ? ` (${context4})` : "";
|
|
88418
|
+
const detail = findings.filter((f) => f.severity === "error").map((f) => `${f.layer}${f.section !== "" ? `/${f.section}` : ""}: ${f.message}`).join("; ");
|
|
88419
|
+
super(`candidate task "${title}"${where} rejected before persistence \u2014 it would fail \`spur task check\`: ${detail}`);
|
|
88420
|
+
this.name = "TaskCandidateInvalidError";
|
|
88421
|
+
this.findings = findings;
|
|
88422
|
+
}
|
|
88423
|
+
};
|
|
87740
88424
|
ROSTER_REGION_RE = /<!--[ \t]*AUTO-GENERATED[^>]*-->[\s\S]*?<!--[ \t]*END[ \t]*AUTO-GENERATED[^>]*-->/g;
|
|
87741
88425
|
TASK_ACTION_COMMANDS = {
|
|
87742
88426
|
refine: (wbs) => `/sp:dev-refine ${wbs} --auto`,
|
|
@@ -89959,8 +90643,93 @@ var init_child_env = __esm(() => {
|
|
|
89959
90643
|
SHIM_INTERNAL_ENV = /^PROTO_(SHIM|INTERNAL)_/;
|
|
89960
90644
|
});
|
|
89961
90645
|
|
|
90646
|
+
// ../../packages/app/src/workflow/actions/run-path.ts
|
|
90647
|
+
import { dirname as dirname16, join as join22, normalize, relative as relative5, resolve as resolve9, sep as sep2 } from "path";
|
|
90648
|
+
function within(child, ancestor) {
|
|
90649
|
+
return child === ancestor || child.startsWith(`${ancestor}${sep2}`);
|
|
90650
|
+
}
|
|
90651
|
+
async function resolveRunArtifactPath(fileSystem, workdir, pathRaw) {
|
|
90652
|
+
if (typeof fileSystem.realPath !== "function") {
|
|
90653
|
+
throw new RunArtifactPathError("physical path confinement requires a FileSystem with realPath support \u2014 refusing to skip confinement (0785 R2)");
|
|
90654
|
+
}
|
|
90655
|
+
const lexicalTarget = normalize(resolve9(workdir, pathRaw));
|
|
90656
|
+
const lexicalRunRoot = normalize(join22(resolve9(workdir), ".spur", "run"));
|
|
90657
|
+
if (!lexicalTarget.startsWith(`${lexicalRunRoot}${sep2}`)) {
|
|
90658
|
+
throw new RunArtifactPathError(`must resolve beneath .spur/run/ (got ${pathRaw})`);
|
|
90659
|
+
}
|
|
90660
|
+
const workdirAbs = resolve9(workdir);
|
|
90661
|
+
let canonicalWorkdir;
|
|
90662
|
+
try {
|
|
90663
|
+
canonicalWorkdir = fileSystem.realPath(workdirAbs);
|
|
90664
|
+
} catch (error51) {
|
|
90665
|
+
throw new RunArtifactPathError(`project workdir could not be canonicalized: ${error51.message}`);
|
|
90666
|
+
}
|
|
90667
|
+
const canonicalRunRootCandidate = join22(canonicalWorkdir, ".spur", "run");
|
|
90668
|
+
let canonicalRunRoot = canonicalRunRootCandidate;
|
|
90669
|
+
const rootStat = await fileSystem.stat(canonicalRunRootCandidate);
|
|
90670
|
+
if (rootStat !== null) {
|
|
90671
|
+
try {
|
|
90672
|
+
canonicalRunRoot = fileSystem.realPath(canonicalRunRootCandidate);
|
|
90673
|
+
} catch (error51) {
|
|
90674
|
+
throw new RunArtifactPathError(`.spur/run could not be canonicalized: ${error51.message}`);
|
|
90675
|
+
}
|
|
90676
|
+
if (!within(canonicalRunRoot, canonicalWorkdir)) {
|
|
90677
|
+
throw new RunArtifactPathError(".spur/run resolves outside the project workdir through a symlink");
|
|
90678
|
+
}
|
|
90679
|
+
}
|
|
90680
|
+
let probe = lexicalTarget;
|
|
90681
|
+
for (;; ) {
|
|
90682
|
+
const stat = await fileSystem.stat(probe);
|
|
90683
|
+
if (stat !== null)
|
|
90684
|
+
break;
|
|
90685
|
+
const parent = dirname16(probe);
|
|
90686
|
+
let listed;
|
|
90687
|
+
try {
|
|
90688
|
+
listed = await fileSystem.readDir(parent);
|
|
90689
|
+
} catch (error51) {
|
|
90690
|
+
if (String(error51.message).includes("ENOENT")) {
|
|
90691
|
+
listed = null;
|
|
90692
|
+
} else {
|
|
90693
|
+
throw new RunArtifactPathError(`ancestor ${parent} is unreadable: ${error51.message}`);
|
|
90694
|
+
}
|
|
90695
|
+
}
|
|
90696
|
+
const base2 = probe.slice(parent.length + 1);
|
|
90697
|
+
if (listed?.includes(base2)) {
|
|
90698
|
+
throw new RunArtifactPathError(`dangling symlink at ${probe} \u2014 refusing artifact path`);
|
|
90699
|
+
}
|
|
90700
|
+
const parentNorm = normalize(parent);
|
|
90701
|
+
if (parentNorm === probe) {
|
|
90702
|
+
throw new RunArtifactPathError(`no existing ancestor found for ${pathRaw}`);
|
|
90703
|
+
}
|
|
90704
|
+
probe = parentNorm;
|
|
90705
|
+
}
|
|
90706
|
+
let anchor;
|
|
90707
|
+
try {
|
|
90708
|
+
anchor = fileSystem.realPath(probe);
|
|
90709
|
+
} catch (error51) {
|
|
90710
|
+
throw new RunArtifactPathError(`ancestor ${probe} could not be canonicalized: ${error51.message}`);
|
|
90711
|
+
}
|
|
90712
|
+
if (!within(anchor, canonicalWorkdir)) {
|
|
90713
|
+
throw new RunArtifactPathError(`ancestor ${probe} escapes the project workdir through a symlink`);
|
|
90714
|
+
}
|
|
90715
|
+
const reconstructed = normalize(join22(anchor, relative5(probe, lexicalTarget)));
|
|
90716
|
+
if (!reconstructed.startsWith(`${canonicalRunRoot}${sep2}`)) {
|
|
90717
|
+
throw new RunArtifactPathError(`path escapes .spur/run/ through a symlink (got ${pathRaw})`);
|
|
90718
|
+
}
|
|
90719
|
+
return reconstructed;
|
|
90720
|
+
}
|
|
90721
|
+
var RunArtifactPathError;
|
|
90722
|
+
var init_run_path = __esm(() => {
|
|
90723
|
+
RunArtifactPathError = class RunArtifactPathError extends Error {
|
|
90724
|
+
constructor(message) {
|
|
90725
|
+
super(message);
|
|
90726
|
+
this.name = "RunArtifactPathError";
|
|
90727
|
+
}
|
|
90728
|
+
};
|
|
90729
|
+
});
|
|
90730
|
+
|
|
89962
90731
|
// ../../packages/app/src/workflow/actions/command-gate.ts
|
|
89963
|
-
import {
|
|
90732
|
+
import { join as join23, normalize as normalize2 } from "path";
|
|
89964
90733
|
function sleep3(ms) {
|
|
89965
90734
|
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
89966
90735
|
}
|
|
@@ -90012,13 +90781,13 @@ class CommandGateActionRunner {
|
|
|
90012
90781
|
};
|
|
90013
90782
|
}
|
|
90014
90783
|
const workdir = context4.workdir ?? process.cwd();
|
|
90015
|
-
|
|
90016
|
-
|
|
90017
|
-
|
|
90018
|
-
|
|
90784
|
+
let normalized;
|
|
90785
|
+
try {
|
|
90786
|
+
normalized = await resolveRunArtifactPath(this.fileSystem, workdir, resultFileRaw);
|
|
90787
|
+
} catch (error51) {
|
|
90019
90788
|
return {
|
|
90020
90789
|
ok: false,
|
|
90021
|
-
error: `resultFile
|
|
90790
|
+
error: `resultFile ${error51.message}`
|
|
90022
90791
|
};
|
|
90023
90792
|
}
|
|
90024
90793
|
const split = splitLaunchCommand(executable, 'command.gate "executable"');
|
|
@@ -90032,7 +90801,7 @@ class CommandGateActionRunner {
|
|
|
90032
90801
|
const retryOn = Array.isArray(retry.on) ? retry.on : [];
|
|
90033
90802
|
const timeoutMs = typeof options.timeoutMs === "number" ? options.timeoutMs : undefined;
|
|
90034
90803
|
const env = childProcessEnv(context4.vars);
|
|
90035
|
-
await this.fileSystem.ensureDir(
|
|
90804
|
+
await this.fileSystem.ensureDir(normalize2(join23(normalized, "..")));
|
|
90036
90805
|
let lastStdout = "";
|
|
90037
90806
|
let lastStderr = "";
|
|
90038
90807
|
let lastExitCode = -1;
|
|
@@ -90102,10 +90871,11 @@ var init_command_gate = __esm(() => {
|
|
|
90102
90871
|
init_dist5();
|
|
90103
90872
|
init_split_launch_command();
|
|
90104
90873
|
init_child_env();
|
|
90874
|
+
init_run_path();
|
|
90105
90875
|
});
|
|
90106
90876
|
|
|
90107
90877
|
// ../../packages/app/src/workflow/actions/doctor-probe.ts
|
|
90108
|
-
import { normalize as
|
|
90878
|
+
import { normalize as normalize3, resolve as resolve10, sep as sep3 } from "path";
|
|
90109
90879
|
function stringOption2(options, key2, fallback) {
|
|
90110
90880
|
const value = options[key2];
|
|
90111
90881
|
if (typeof value === "string")
|
|
@@ -90155,8 +90925,8 @@ class DoctorProbeActionRunner {
|
|
|
90155
90925
|
const resolvedAgentVar = stringOption2(options, "resolvedAgentVar", "");
|
|
90156
90926
|
const workdir = context4.workdir ?? process.cwd();
|
|
90157
90927
|
const allowedDir = resolve10(workdir, ".spur", "run");
|
|
90158
|
-
const normalized =
|
|
90159
|
-
const withinAllowed = normalized === allowedDir || normalized.startsWith(allowedDir +
|
|
90928
|
+
const normalized = normalize3(resolve10(workdir, resultFileRaw));
|
|
90929
|
+
const withinAllowed = normalized === allowedDir || normalized.startsWith(allowedDir + sep3);
|
|
90160
90930
|
if (!withinAllowed) {
|
|
90161
90931
|
return {
|
|
90162
90932
|
ok: false,
|
|
@@ -90753,11 +91523,59 @@ var init_composition_baseline = () => {};
|
|
|
90753
91523
|
// ../../packages/app/src/workflow/proof-input-fingerprint.ts
|
|
90754
91524
|
import crypto4 from "crypto";
|
|
90755
91525
|
import { tmpdir as tmpdir2 } from "os";
|
|
90756
|
-
import { dirname as
|
|
91526
|
+
import { dirname as dirname17, join as join24, resolve as resolve11, sep as sep4 } from "path";
|
|
91527
|
+
async function readProofInputContents(fileSystem, workdir, options) {
|
|
91528
|
+
for (const name of ["taskFile", "featureFile"]) {
|
|
91529
|
+
const value = options[name];
|
|
91530
|
+
if (value !== undefined && typeof value !== "string") {
|
|
91531
|
+
return { ok: false, error: `${name} must be a string when supplied (got ${typeof value})` };
|
|
91532
|
+
}
|
|
91533
|
+
}
|
|
91534
|
+
const readOne = async (name) => {
|
|
91535
|
+
const raw = name === "taskFile" ? options.taskFile : options.featureFile;
|
|
91536
|
+
if (raw === undefined || raw === "")
|
|
91537
|
+
return {};
|
|
91538
|
+
const workdirAbs = resolve11(workdir);
|
|
91539
|
+
const resolved = resolve11(workdirAbs, raw);
|
|
91540
|
+
if (!(resolved === workdirAbs || resolved.startsWith(`${workdirAbs}${sep4}`))) {
|
|
91541
|
+
return { error: `${name} must resolve under the workflow workdir (${workdirAbs}): ${raw}` };
|
|
91542
|
+
}
|
|
91543
|
+
let stat;
|
|
91544
|
+
try {
|
|
91545
|
+
stat = await fileSystem.stat(resolved);
|
|
91546
|
+
} catch (error51) {
|
|
91547
|
+
return { error: `${name} could not be inspected: ${error51.message}` };
|
|
91548
|
+
}
|
|
91549
|
+
if (stat === null) {
|
|
91550
|
+
return {
|
|
91551
|
+
error: `${name} does not exist: ${raw} (resolved ${resolved}) \u2014 an explicitly supplied spec must be readable, not silently omitted`
|
|
91552
|
+
};
|
|
91553
|
+
}
|
|
91554
|
+
if (!stat.isFile()) {
|
|
91555
|
+
return { error: `${name} is not a regular file: ${resolved}` };
|
|
91556
|
+
}
|
|
91557
|
+
try {
|
|
91558
|
+
return { content: await fileSystem.readFile(resolved) };
|
|
91559
|
+
} catch (error51) {
|
|
91560
|
+
return { error: `${name} is not readable: ${error51.message}` };
|
|
91561
|
+
}
|
|
91562
|
+
};
|
|
91563
|
+
const task = await readOne("taskFile");
|
|
91564
|
+
if (task.error !== undefined)
|
|
91565
|
+
return { ok: false, error: task.error };
|
|
91566
|
+
const feature = await readOne("featureFile");
|
|
91567
|
+
if (feature.error !== undefined)
|
|
91568
|
+
return { ok: false, error: feature.error };
|
|
91569
|
+
return {
|
|
91570
|
+
ok: true,
|
|
91571
|
+
...task.content !== undefined ? { taskContent: task.content } : {},
|
|
91572
|
+
...feature.content !== undefined ? { featureContent: feature.content } : {}
|
|
91573
|
+
};
|
|
91574
|
+
}
|
|
90757
91575
|
async function createGitAlternateTree(cwd, excludeGlobs = DEFAULT_EXCLUDE_GLOBS, executor = new NodeProcessExecutor3, fs3 = createNodeFileSystem3()) {
|
|
90758
|
-
const indexFile =
|
|
91576
|
+
const indexFile = join24(tmpdir2(), `spur-proof-fingerprint-${crypto4.randomUUID()}.index`);
|
|
90759
91577
|
try {
|
|
90760
|
-
await fs3.ensureDir(
|
|
91578
|
+
await fs3.ensureDir(dirname17(indexFile));
|
|
90761
91579
|
const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
90762
91580
|
env.GIT_INDEX_FILE = indexFile;
|
|
90763
91581
|
const common4 = { cwd, env, forceBuffered: true, rejectOnError: false };
|
|
@@ -90874,15 +91692,6 @@ var init_proof_input_fingerprint = __esm(() => {
|
|
|
90874
91692
|
});
|
|
90875
91693
|
|
|
90876
91694
|
// ../../packages/app/src/workflow/actions/proof-fingerprint.ts
|
|
90877
|
-
async function readOptional(fileSystem, path9) {
|
|
90878
|
-
if (path9 === undefined || path9 === "")
|
|
90879
|
-
return;
|
|
90880
|
-
const stat = await fileSystem.stat(path9);
|
|
90881
|
-
if (stat === null)
|
|
90882
|
-
return;
|
|
90883
|
-
return await fileSystem.readFile(path9);
|
|
90884
|
-
}
|
|
90885
|
-
|
|
90886
91695
|
class ProofFingerprintActionRunner {
|
|
90887
91696
|
fileSystem;
|
|
90888
91697
|
processExecutor;
|
|
@@ -90901,14 +91710,16 @@ class ProofFingerprintActionRunner {
|
|
|
90901
91710
|
if (!VAR_NAME_RE.test(varName)) {
|
|
90902
91711
|
return { ok: false, error: `${KIND11}: var name must match ${VAR_NAME_RE}, got "${varName}"` };
|
|
90903
91712
|
}
|
|
90904
|
-
const
|
|
90905
|
-
|
|
91713
|
+
const inputs = await readProofInputContents(this.fileSystem, context4.workdir ?? ".", options);
|
|
91714
|
+
if (!inputs.ok) {
|
|
91715
|
+
return { ok: false, error: `${KIND11}: ${inputs.error}` };
|
|
91716
|
+
}
|
|
90906
91717
|
let digest;
|
|
90907
91718
|
try {
|
|
90908
91719
|
digest = await computeProofInputFingerprint({
|
|
90909
91720
|
cwd: context4.workdir ?? ".",
|
|
90910
|
-
...taskContent !== undefined ? { taskContent } : {},
|
|
90911
|
-
...featureContent !== undefined ? { featureContent } : {},
|
|
91721
|
+
...inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {},
|
|
91722
|
+
...inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {},
|
|
90912
91723
|
...this.processExecutor !== undefined ? { processExecutor: this.processExecutor } : {},
|
|
90913
91724
|
fileSystem: this.fileSystem
|
|
90914
91725
|
});
|
|
@@ -91055,17 +91866,19 @@ var init_rule_check = __esm(() => {
|
|
|
91055
91866
|
});
|
|
91056
91867
|
|
|
91057
91868
|
// ../../packages/app/src/workflow/actions/run-artifact.ts
|
|
91058
|
-
import {
|
|
91869
|
+
import { join as join25, resolve as resolve12 } from "path";
|
|
91059
91870
|
|
|
91060
91871
|
class RunArtifactActionRunner {
|
|
91061
91872
|
getDb;
|
|
91062
91873
|
fileSystem;
|
|
91063
91874
|
artifactDao;
|
|
91875
|
+
processExecutor;
|
|
91064
91876
|
kind = KIND14;
|
|
91065
|
-
constructor(getDb, fileSystem = createNodeFileSystem3(), artifactDao) {
|
|
91877
|
+
constructor(getDb, fileSystem = createNodeFileSystem3(), artifactDao, processExecutor) {
|
|
91066
91878
|
this.getDb = getDb;
|
|
91067
91879
|
this.fileSystem = fileSystem;
|
|
91068
91880
|
this.artifactDao = artifactDao;
|
|
91881
|
+
this.processExecutor = processExecutor;
|
|
91069
91882
|
}
|
|
91070
91883
|
async execute(options, context4) {
|
|
91071
91884
|
const pathRaw = options.path;
|
|
@@ -91082,39 +91895,36 @@ class RunArtifactActionRunner {
|
|
|
91082
91895
|
error: 'Action option "artifactKind" must be a non-empty string'
|
|
91083
91896
|
};
|
|
91084
91897
|
}
|
|
91085
|
-
const
|
|
91086
|
-
|
|
91087
|
-
const resolvedPath = resolve11(workdir, pathRaw);
|
|
91088
|
-
const normalized = normalize3(resolvedPath);
|
|
91089
|
-
if (!normalized.startsWith(allowedDir)) {
|
|
91898
|
+
const proofBinding = options.proofBinding;
|
|
91899
|
+
if (proofBinding !== undefined && proofBinding !== "current") {
|
|
91090
91900
|
return {
|
|
91091
91901
|
ok: false,
|
|
91092
|
-
error:
|
|
91902
|
+
error: `${KIND14}: unsupported proofBinding "${String(proofBinding)}" - only "current" is defined (ADR-071)`
|
|
91093
91903
|
};
|
|
91094
91904
|
}
|
|
91905
|
+
const workdir = context4.workdir ?? process.cwd();
|
|
91906
|
+
if (proofBinding === "current") {
|
|
91907
|
+
return await this.executeBound(pathRaw, artifactKind, options, context4, workdir);
|
|
91908
|
+
}
|
|
91909
|
+
let normalized;
|
|
91910
|
+
try {
|
|
91911
|
+
normalized = await resolveRunArtifactPath(this.fileSystem, workdir, pathRaw);
|
|
91912
|
+
} catch (error51) {
|
|
91913
|
+
return { ok: false, error: `path ${error51.message}` };
|
|
91914
|
+
}
|
|
91095
91915
|
const requireExisting = options.requireExisting !== false;
|
|
91096
91916
|
if (requireExisting) {
|
|
91097
|
-
const
|
|
91098
|
-
if (
|
|
91917
|
+
const stat = await this.fileSystem.stat(normalized);
|
|
91918
|
+
if (stat === null) {
|
|
91099
91919
|
return {
|
|
91100
91920
|
ok: false,
|
|
91101
91921
|
error: `run.artifact required file does not exist: ${pathRaw} (${normalized})`
|
|
91102
91922
|
};
|
|
91103
91923
|
}
|
|
91104
|
-
|
|
91105
|
-
const proofBinding = options.proofBinding;
|
|
91106
|
-
if (proofBinding !== undefined) {
|
|
91107
|
-
if (proofBinding !== "current") {
|
|
91108
|
-
return {
|
|
91109
|
-
ok: false,
|
|
91110
|
-
error: `${KIND14}: unsupported proofBinding "${String(proofBinding)}" - only "current" is defined (ADR-071)`
|
|
91111
|
-
};
|
|
91112
|
-
}
|
|
91113
|
-
const digest = context4.vars.proofDigestNow ?? context4.vars.proofDigest;
|
|
91114
|
-
if (typeof digest !== "string" || !PROOF_DIGEST_RE.test(digest)) {
|
|
91924
|
+
if (!stat.isFile()) {
|
|
91115
91925
|
return {
|
|
91116
91926
|
ok: false,
|
|
91117
|
-
error:
|
|
91927
|
+
error: `run.artifact required file is not a regular file: ${pathRaw} (${normalized})`
|
|
91118
91928
|
};
|
|
91119
91929
|
}
|
|
91120
91930
|
}
|
|
@@ -91138,9 +91948,232 @@ class RunArtifactActionRunner {
|
|
|
91138
91948
|
id: recordId,
|
|
91139
91949
|
path: normalized,
|
|
91140
91950
|
kind: artifactKind,
|
|
91141
|
-
runId: context4.runId
|
|
91142
|
-
|
|
91951
|
+
runId: context4.runId
|
|
91952
|
+
}
|
|
91953
|
+
};
|
|
91954
|
+
}
|
|
91955
|
+
async executeBound(pathRaw, artifactKind, options, context4, workdir) {
|
|
91956
|
+
if (artifactKind !== "verify-verdict") {
|
|
91957
|
+
return {
|
|
91958
|
+
ok: false,
|
|
91959
|
+
error: `${KIND14}: proofBinding "current" is only supported for the verify-verdict artifact kind (got "${artifactKind}")`
|
|
91960
|
+
};
|
|
91961
|
+
}
|
|
91962
|
+
if (!this.getDb) {
|
|
91963
|
+
return {
|
|
91964
|
+
ok: false,
|
|
91965
|
+
error: `${KIND14}: proofBinding "current" requires the composed database context (RunDao run identity) \u2014 refusing an unverifiable binding`
|
|
91966
|
+
};
|
|
91967
|
+
}
|
|
91968
|
+
if (typeof options.taskFile !== "string" || options.taskFile.trim() === "") {
|
|
91969
|
+
return {
|
|
91970
|
+
ok: false,
|
|
91971
|
+
error: `${KIND14}: proofBinding "current" requires a non-empty "taskFile" option \u2014 the fresh digest must cover the canonical task spec`
|
|
91972
|
+
};
|
|
91973
|
+
}
|
|
91974
|
+
const inputs = await readProofInputContents(this.fileSystem, workdir, options);
|
|
91975
|
+
if (!inputs.ok) {
|
|
91976
|
+
return { ok: false, error: `${KIND14}: ${inputs.error}` };
|
|
91977
|
+
}
|
|
91978
|
+
let digest;
|
|
91979
|
+
try {
|
|
91980
|
+
digest = await computeProofInputFingerprint({
|
|
91981
|
+
cwd: workdir,
|
|
91982
|
+
...inputs.taskContent !== undefined ? { taskContent: inputs.taskContent } : {},
|
|
91983
|
+
...inputs.featureContent !== undefined ? { featureContent: inputs.featureContent } : {},
|
|
91984
|
+
...this.processExecutor !== undefined ? { processExecutor: this.processExecutor } : {},
|
|
91985
|
+
fileSystem: this.fileSystem
|
|
91986
|
+
});
|
|
91987
|
+
} catch (error51) {
|
|
91988
|
+
return { ok: false, error: `${KIND14}: fresh proof capture failed: ${error51.message}` };
|
|
91989
|
+
}
|
|
91990
|
+
const varDigest = context4.vars.proofDigestNow ?? context4.vars.proofDigest;
|
|
91991
|
+
if (typeof varDigest !== "string" || !PROOF_DIGEST_RE.test(varDigest)) {
|
|
91992
|
+
return {
|
|
91993
|
+
ok: false,
|
|
91994
|
+
error: `${KIND14}: proofBinding "current" does not hold - the run has no current proof input ` + `digest (vars.proofDigestNow/proofDigest missing or malformed), so the artifact cannot be ` + `bound to a proof that was never captured; capture one with proof.fingerprint first`
|
|
91995
|
+
};
|
|
91996
|
+
}
|
|
91997
|
+
if (varDigest !== digest) {
|
|
91998
|
+
return {
|
|
91999
|
+
ok: false,
|
|
92000
|
+
error: `${KIND14}: the run's declared proof digest (${varDigest}) does not match the freshly ` + `captured digest (${digest}) \u2014 a stale or forged var cannot bless artifact content (0785 R3)`
|
|
92001
|
+
};
|
|
92002
|
+
}
|
|
92003
|
+
let normalized;
|
|
92004
|
+
try {
|
|
92005
|
+
normalized = await resolveRunArtifactPath(this.fileSystem, workdir, pathRaw);
|
|
92006
|
+
} catch (error51) {
|
|
92007
|
+
return { ok: false, error: `path ${error51.message}` };
|
|
92008
|
+
}
|
|
92009
|
+
const requireExisting = options.requireExisting !== false;
|
|
92010
|
+
if (requireExisting) {
|
|
92011
|
+
const stat = await this.fileSystem.stat(normalized);
|
|
92012
|
+
if (stat === null) {
|
|
92013
|
+
return {
|
|
92014
|
+
ok: false,
|
|
92015
|
+
error: `run.artifact required file does not exist: ${pathRaw} (${normalized})`
|
|
92016
|
+
};
|
|
92017
|
+
}
|
|
92018
|
+
if (!stat.isFile()) {
|
|
92019
|
+
return {
|
|
92020
|
+
ok: false,
|
|
92021
|
+
error: `run.artifact required file is not a regular file: ${pathRaw} (${normalized})`
|
|
92022
|
+
};
|
|
92023
|
+
}
|
|
92024
|
+
}
|
|
92025
|
+
let raw;
|
|
92026
|
+
try {
|
|
92027
|
+
raw = await this.fileSystem.readFile(normalized);
|
|
92028
|
+
} catch (error51) {
|
|
92029
|
+
return { ok: false, error: `${KIND14}: verdict artifact unreadable: ${error51.message}` };
|
|
92030
|
+
}
|
|
92031
|
+
const taskWbs = String(context4.vars.wbs ?? "");
|
|
92032
|
+
const outcome = parseVerifyVerdict(raw, taskWbs);
|
|
92033
|
+
if (outcome.kind !== "valid") {
|
|
92034
|
+
const detail = outcome.kind === "invalid" && outcome.reason !== undefined ? `: ${outcome.reason}` : "";
|
|
92035
|
+
return {
|
|
92036
|
+
ok: false,
|
|
92037
|
+
error: `${KIND14}: verdict artifact is ${outcome.kind}${detail} \u2014 refusing to bind (0785 R3)`
|
|
92038
|
+
};
|
|
92039
|
+
}
|
|
92040
|
+
const verdict = outcome.verdict;
|
|
92041
|
+
if (verdict.wbs !== taskWbs) {
|
|
92042
|
+
return {
|
|
92043
|
+
ok: false,
|
|
92044
|
+
error: `${KIND14}: verdict artifact names wbs "${verdict.wbs}" but the run carries task "${taskWbs}"`
|
|
92045
|
+
};
|
|
92046
|
+
}
|
|
92047
|
+
if (verdict.verdict !== "PASS") {
|
|
92048
|
+
return {
|
|
92049
|
+
ok: false,
|
|
92050
|
+
error: `${KIND14}: verdict aggregate is ${verdict.verdict}, not PASS \u2014 refusing to bind`
|
|
92051
|
+
};
|
|
92052
|
+
}
|
|
92053
|
+
let root;
|
|
92054
|
+
try {
|
|
92055
|
+
root = JSON.parse(raw);
|
|
92056
|
+
} catch {
|
|
92057
|
+
return { ok: false, error: `${KIND14}: verdict artifact is malformed JSON` };
|
|
92058
|
+
}
|
|
92059
|
+
const proof = root.proof;
|
|
92060
|
+
if (proof === null || typeof proof !== "object" || Array.isArray(proof)) {
|
|
92061
|
+
return {
|
|
92062
|
+
ok: false,
|
|
92063
|
+
error: `${KIND14}: verdict artifact carries no raw proof block \u2014 binding refused (0785 R3)`
|
|
92064
|
+
};
|
|
92065
|
+
}
|
|
92066
|
+
const p = proof;
|
|
92067
|
+
const stageOf = (name) => {
|
|
92068
|
+
const stages = p.stages;
|
|
92069
|
+
if (stages === null || typeof stages !== "object" || Array.isArray(stages))
|
|
92070
|
+
return null;
|
|
92071
|
+
const s3 = stages[name];
|
|
92072
|
+
return s3 !== null && typeof s3 === "object" && !Array.isArray(s3) ? s3 : null;
|
|
92073
|
+
};
|
|
92074
|
+
if (p.digest !== digest) {
|
|
92075
|
+
return {
|
|
92076
|
+
ok: false,
|
|
92077
|
+
error: `${KIND14}: proof.digest ${String(p.digest)} does not equal the freshly captured digest (${digest})`
|
|
92078
|
+
};
|
|
92079
|
+
}
|
|
92080
|
+
if (p.runId !== context4.runId) {
|
|
92081
|
+
return {
|
|
92082
|
+
ok: false,
|
|
92083
|
+
error: `${KIND14}: proof.runId ${String(p.runId)} does not match the certifying run ${context4.runId}`
|
|
92084
|
+
};
|
|
92085
|
+
}
|
|
92086
|
+
const db2 = await this.getDb();
|
|
92087
|
+
const runRow = await new RunDao(db2).traceRowById(context4.runId);
|
|
92088
|
+
if (runRow === undefined) {
|
|
92089
|
+
return {
|
|
92090
|
+
ok: false,
|
|
92091
|
+
error: `${KIND14}: run ${context4.runId} has no authoritative row \u2014 refusing binding (0785 R3)`
|
|
92092
|
+
};
|
|
92093
|
+
}
|
|
92094
|
+
let metadata = {};
|
|
92095
|
+
try {
|
|
92096
|
+
metadata = JSON.parse(runRow.metadata_json || "{}");
|
|
92097
|
+
} catch {
|
|
92098
|
+
return { ok: false, error: `${KIND14}: run ${context4.runId} metadata_json is malformed \u2014 refusing binding` };
|
|
92099
|
+
}
|
|
92100
|
+
const resumeDigest = metadata.resumeDefinitionDigest;
|
|
92101
|
+
const expectedDefinition = typeof resumeDigest === "string" && resumeDigest !== "" ? resumeDigest : metadata.definitionDigest;
|
|
92102
|
+
if (typeof expectedDefinition !== "string" || p.definitionDigest !== expectedDefinition) {
|
|
92103
|
+
return {
|
|
92104
|
+
ok: false,
|
|
92105
|
+
error: `${KIND14}: proof.definitionDigest ${String(p.definitionDigest)} does not match the run's ` + `${typeof resumeDigest === "string" && resumeDigest !== "" ? "resume " : ""}definition digest \u2014 ` + `a stale-definition artifact cannot certify this run (0785 R3)`
|
|
92106
|
+
};
|
|
92107
|
+
}
|
|
92108
|
+
const qualityGate = stageOf("qualityGate");
|
|
92109
|
+
const review = stageOf("review");
|
|
92110
|
+
const verification = stageOf("verification");
|
|
92111
|
+
if (qualityGate === null || review === null || verification === null) {
|
|
92112
|
+
return {
|
|
92113
|
+
ok: false,
|
|
92114
|
+
error: `${KIND14}: proof.stages is missing qualityGate/review/verification evidence \u2014 binding refused`
|
|
92115
|
+
};
|
|
92116
|
+
}
|
|
92117
|
+
for (const [name, stage] of [
|
|
92118
|
+
["qualityGate", qualityGate],
|
|
92119
|
+
["review", review],
|
|
92120
|
+
["verification", verification]
|
|
92121
|
+
]) {
|
|
92122
|
+
if (stage.digest !== digest) {
|
|
92123
|
+
return {
|
|
92124
|
+
ok: false,
|
|
92125
|
+
error: `${KIND14}: proof.stages.${name}.digest does not equal the freshly captured digest`
|
|
92126
|
+
};
|
|
91143
92127
|
}
|
|
92128
|
+
}
|
|
92129
|
+
if (qualityGate.status !== "PASS") {
|
|
92130
|
+
return {
|
|
92131
|
+
ok: false,
|
|
92132
|
+
error: `${KIND14}: proof.stages.qualityGate.status is ${String(qualityGate.status)}, not PASS`
|
|
92133
|
+
};
|
|
92134
|
+
}
|
|
92135
|
+
if (verification.status !== "PASS") {
|
|
92136
|
+
return {
|
|
92137
|
+
ok: false,
|
|
92138
|
+
error: `${KIND14}: proof.stages.verification.status is ${String(verification.status)}, not PASS`
|
|
92139
|
+
};
|
|
92140
|
+
}
|
|
92141
|
+
if (review.status !== "completed") {
|
|
92142
|
+
return {
|
|
92143
|
+
ok: false,
|
|
92144
|
+
error: `${KIND14}: proof.stages.review.status is "${String(review.status)}" \u2014 a skipped or stale review is not completed evidence (0785 R4)`
|
|
92145
|
+
};
|
|
92146
|
+
}
|
|
92147
|
+
const markerPath = join25(resolve12(workdir), ".spur", "run", `${context4.runId}-review-proof.digest`);
|
|
92148
|
+
let marker = null;
|
|
92149
|
+
try {
|
|
92150
|
+
marker = await this.fileSystem.readFile(markerPath);
|
|
92151
|
+
} catch {
|
|
92152
|
+
marker = null;
|
|
92153
|
+
}
|
|
92154
|
+
if (marker === null || marker.trim() !== digest) {
|
|
92155
|
+
return {
|
|
92156
|
+
ok: false,
|
|
92157
|
+
error: `${KIND14}: review-proof marker ${context4.runId}-review-proof.digest is missing or does not name ` + `the current digest \u2014 review completion is not evidenced for this run (0785 R4)`
|
|
92158
|
+
};
|
|
92159
|
+
}
|
|
92160
|
+
const dao2 = this.artifactDao ?? new ArtifactDao(db2);
|
|
92161
|
+
const record2 = await dao2.record({
|
|
92162
|
+
path: normalized,
|
|
92163
|
+
kind: artifactKind,
|
|
92164
|
+
runId: context4.runId
|
|
92165
|
+
});
|
|
92166
|
+
return {
|
|
92167
|
+
ok: true,
|
|
92168
|
+
data: {
|
|
92169
|
+
id: record2.id,
|
|
92170
|
+
path: normalized,
|
|
92171
|
+
kind: artifactKind,
|
|
92172
|
+
runId: context4.runId,
|
|
92173
|
+
proofBinding: "current",
|
|
92174
|
+
proofDigest: digest
|
|
92175
|
+
},
|
|
92176
|
+
setVars: { proofDigestNow: digest }
|
|
91144
92177
|
};
|
|
91145
92178
|
}
|
|
91146
92179
|
}
|
|
@@ -91148,6 +92181,9 @@ var KIND14 = "run.artifact", PROOF_DIGEST_RE;
|
|
|
91148
92181
|
var init_run_artifact = __esm(() => {
|
|
91149
92182
|
init_src2();
|
|
91150
92183
|
init_dist5();
|
|
92184
|
+
init_verify_verdict();
|
|
92185
|
+
init_proof_input_fingerprint();
|
|
92186
|
+
init_run_path();
|
|
91151
92187
|
PROOF_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
91152
92188
|
});
|
|
91153
92189
|
|
|
@@ -91332,7 +92368,7 @@ function registerSpurBuiltins(host, options) {
|
|
|
91332
92368
|
host.registerAction(new HitlInputActionRunner(options.hitlResponder), "builtin");
|
|
91333
92369
|
host.registerAction(new CommandGateActionRunner(options.processExecutor ?? new NodeProcessExecutor3, fileSystem), "builtin");
|
|
91334
92370
|
host.registerAction(new DoctorProbeActionRunner(options.processExecutor ?? new NodeProcessExecutor3, fileSystem, options.observabilityBus), "builtin");
|
|
91335
|
-
host.registerAction(new RunArtifactActionRunner(options.getDb, fileSystem, options.artifactDao), "builtin");
|
|
92371
|
+
host.registerAction(new RunArtifactActionRunner(options.getDb, fileSystem, options.artifactDao, options.processExecutor), "builtin");
|
|
91336
92372
|
host.registerAction(new ProofFingerprintActionRunner(fileSystem, options.processExecutor, options.observabilityBus), "builtin");
|
|
91337
92373
|
if (options.httpRequester) {
|
|
91338
92374
|
host.registerAction(new HttpRequestActionRunner(options.httpRequester, options.hostAllowlist ?? new Set), "builtin");
|
|
@@ -91377,11 +92413,11 @@ function parseCheckpointMetadata(raw) {
|
|
|
91377
92413
|
continue;
|
|
91378
92414
|
}
|
|
91379
92415
|
inArtifacts = false;
|
|
91380
|
-
const
|
|
91381
|
-
if (
|
|
92416
|
+
const sep5 = line.indexOf(":");
|
|
92417
|
+
if (sep5 <= 0)
|
|
91382
92418
|
continue;
|
|
91383
|
-
const key2 = line.slice(0,
|
|
91384
|
-
const value = line.slice(
|
|
92419
|
+
const key2 = line.slice(0, sep5).trim();
|
|
92420
|
+
const value = line.slice(sep5 + 1).trim().replace(/^"|"$/g, "");
|
|
91385
92421
|
if (key2 === "artifacts") {
|
|
91386
92422
|
inArtifacts = true;
|
|
91387
92423
|
if (value !== "" && value !== "[]") {
|
|
@@ -91451,7 +92487,7 @@ var init_checkpoint_contract = __esm(() => {
|
|
|
91451
92487
|
});
|
|
91452
92488
|
|
|
91453
92489
|
// ../../packages/app/src/workflow/workflow-resolver.ts
|
|
91454
|
-
import { basename as basename7, isAbsolute as isAbsolute6, join as
|
|
92490
|
+
import { basename as basename7, isAbsolute as isAbsolute6, join as join26, resolve as resolve13 } from "path";
|
|
91455
92491
|
function createEmbeddedSchemaOptions(embedded) {
|
|
91456
92492
|
if (embedded === undefined || embedded.size === 0)
|
|
91457
92493
|
return;
|
|
@@ -91482,12 +92518,12 @@ async function readWorkflowNameFast(fs3, filePath) {
|
|
|
91482
92518
|
}
|
|
91483
92519
|
async function scanWorkflowByName(cwd, name) {
|
|
91484
92520
|
const fs3 = createNodeFileSystem3();
|
|
91485
|
-
const projectSpurDir =
|
|
92521
|
+
const projectSpurDir = resolve13(cwd, ".spur", "workflows");
|
|
91486
92522
|
if (fs3.exists(projectSpurDir)) {
|
|
91487
92523
|
try {
|
|
91488
92524
|
const files = (await fs3.readDir(projectSpurDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
91489
92525
|
for (const f of files) {
|
|
91490
|
-
const abs =
|
|
92526
|
+
const abs = join26(projectSpurDir, f);
|
|
91491
92527
|
const wfName = await readWorkflowNameFast(fs3, abs);
|
|
91492
92528
|
if (wfName === name) {
|
|
91493
92529
|
return { path: abs, source: "project" };
|
|
@@ -91497,12 +92533,12 @@ async function scanWorkflowByName(cwd, name) {
|
|
|
91497
92533
|
}
|
|
91498
92534
|
const bundledRoot = bundledConfigRoot();
|
|
91499
92535
|
if (bundledRoot !== null) {
|
|
91500
|
-
const bundledWorkflowsDir =
|
|
92536
|
+
const bundledWorkflowsDir = join26(bundledRoot, "workflows");
|
|
91501
92537
|
if (fs3.exists(bundledWorkflowsDir)) {
|
|
91502
92538
|
try {
|
|
91503
92539
|
const files = (await fs3.readDir(bundledWorkflowsDir)).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
91504
92540
|
for (const f of files) {
|
|
91505
|
-
const abs =
|
|
92541
|
+
const abs = join26(bundledWorkflowsDir, f);
|
|
91506
92542
|
const wfName = await readWorkflowNameFast(fs3, abs);
|
|
91507
92543
|
if (wfName === name) {
|
|
91508
92544
|
return { path: abs, source: "bundled" };
|
|
@@ -91515,7 +92551,7 @@ async function scanWorkflowByName(cwd, name) {
|
|
|
91515
92551
|
}
|
|
91516
92552
|
function resolveWorkflowFile(cwd, file2) {
|
|
91517
92553
|
const fs3 = createNodeFileSystem3();
|
|
91518
|
-
const projectPath =
|
|
92554
|
+
const projectPath = resolve13(cwd, file2);
|
|
91519
92555
|
if (fs3.exists(projectPath)) {
|
|
91520
92556
|
return { path: projectPath, source: "project" };
|
|
91521
92557
|
}
|
|
@@ -91523,20 +92559,20 @@ function resolveWorkflowFile(cwd, file2) {
|
|
|
91523
92559
|
const base2 = basename7(file2);
|
|
91524
92560
|
if (!isAbsolute6(file2)) {
|
|
91525
92561
|
const withYaml = isYaml ? file2 : `${file2}.yaml`;
|
|
91526
|
-
const projectSpur =
|
|
92562
|
+
const projectSpur = resolve13(cwd, ".spur", "workflows", withYaml);
|
|
91527
92563
|
if (fs3.exists(projectSpur)) {
|
|
91528
92564
|
return { path: projectSpur, source: "project" };
|
|
91529
92565
|
}
|
|
91530
92566
|
if (!isYaml) {
|
|
91531
|
-
const projectDirect =
|
|
92567
|
+
const projectDirect = resolve13(cwd, `${file2}.yaml`);
|
|
91532
92568
|
if (fs3.exists(projectDirect)) {
|
|
91533
92569
|
return { path: projectDirect, source: "project" };
|
|
91534
92570
|
}
|
|
91535
|
-
const projectSpurPipeline =
|
|
92571
|
+
const projectSpurPipeline = resolve13(cwd, ".spur", "workflows", `${file2}-pipeline.yaml`);
|
|
91536
92572
|
if (fs3.exists(projectSpurPipeline)) {
|
|
91537
92573
|
return { path: projectSpurPipeline, source: "project" };
|
|
91538
92574
|
}
|
|
91539
|
-
const projectDirectPipeline =
|
|
92575
|
+
const projectDirectPipeline = resolve13(cwd, `${file2}-pipeline.yaml`);
|
|
91540
92576
|
if (fs3.exists(projectDirectPipeline)) {
|
|
91541
92577
|
return { path: projectDirectPipeline, source: "project" };
|
|
91542
92578
|
}
|
|
@@ -91545,16 +92581,16 @@ function resolveWorkflowFile(cwd, file2) {
|
|
|
91545
92581
|
const bundledRoot = bundledConfigRoot();
|
|
91546
92582
|
if (bundledRoot !== null) {
|
|
91547
92583
|
const bundledName = isYaml ? base2 : `${base2}.yaml`;
|
|
91548
|
-
const bundledPath =
|
|
92584
|
+
const bundledPath = join26(bundledRoot, "workflows", bundledName);
|
|
91549
92585
|
if (fs3.exists(bundledPath)) {
|
|
91550
92586
|
return { path: bundledPath, source: "bundled" };
|
|
91551
92587
|
}
|
|
91552
92588
|
if (!isYaml) {
|
|
91553
|
-
const bundledPipeline =
|
|
92589
|
+
const bundledPipeline = join26(bundledRoot, "workflows", `${base2}-pipeline.yaml`);
|
|
91554
92590
|
if (fs3.exists(bundledPipeline)) {
|
|
91555
92591
|
return { path: bundledPipeline, source: "bundled" };
|
|
91556
92592
|
}
|
|
91557
|
-
const bundledLiteral =
|
|
92593
|
+
const bundledLiteral = join26(bundledRoot, "workflows", base2);
|
|
91558
92594
|
if (fs3.exists(bundledLiteral)) {
|
|
91559
92595
|
return { path: bundledLiteral, source: "bundled" };
|
|
91560
92596
|
}
|
|
@@ -91602,7 +92638,7 @@ var init_workflow_resolver = __esm(() => {
|
|
|
91602
92638
|
// ../../packages/app/src/services/workflow-service.ts
|
|
91603
92639
|
import { realpathSync as realpathSync5 } from "fs";
|
|
91604
92640
|
import { homedir as homedir8 } from "os";
|
|
91605
|
-
import { basename as basename8, dirname as
|
|
92641
|
+
import { basename as basename8, dirname as dirname18, isAbsolute as isAbsolute7, join as join27, resolve as resolve14, sep as sep5 } from "path";
|
|
91606
92642
|
function signalSubprocess(pid) {
|
|
91607
92643
|
if (!Number.isInteger(pid) || pid <= 1)
|
|
91608
92644
|
return false;
|
|
@@ -91647,7 +92683,7 @@ function withSelfPidRecording(inner, db2) {
|
|
|
91647
92683
|
}
|
|
91648
92684
|
function withRunIdentityRecording(inner, db2, identity7) {
|
|
91649
92685
|
const stamp = async (runId) => {
|
|
91650
|
-
await new RunDao(db2).stampRunIdentity(runId, identity7.definitionDigest, identity7.workflowVersion);
|
|
92686
|
+
await new RunDao(db2).stampRunIdentity(runId, identity7.definitionDigest, identity7.workflowVersion, identity7.source);
|
|
91651
92687
|
};
|
|
91652
92688
|
return new Proxy(inner, {
|
|
91653
92689
|
get(target, prop, receiver) {
|
|
@@ -91659,8 +92695,10 @@ function withRunIdentityRecording(inner, db2, identity7) {
|
|
|
91659
92695
|
}
|
|
91660
92696
|
if (prop === "createOrAttachRun") {
|
|
91661
92697
|
return async (record2) => {
|
|
92698
|
+
const existing = await new RunDao(db2).traceRowById(record2.id);
|
|
91662
92699
|
const result = await target.createOrAttachRun(record2);
|
|
91663
|
-
|
|
92700
|
+
if (existing === undefined)
|
|
92701
|
+
await stamp(result.id);
|
|
91664
92702
|
return result;
|
|
91665
92703
|
};
|
|
91666
92704
|
}
|
|
@@ -91761,7 +92799,8 @@ class WorkflowAppService {
|
|
|
91761
92799
|
recordSelfPid: opts.recordSelfPid === true,
|
|
91762
92800
|
events: eventsBus,
|
|
91763
92801
|
steeringController: opts.steeringController,
|
|
91764
|
-
extensions: { workflow, file: absolute }
|
|
92802
|
+
extensions: { workflow, file: absolute },
|
|
92803
|
+
definitionSource: { path: absolute, layer: resolved.layer, workdir: this.ctx.cwd }
|
|
91765
92804
|
});
|
|
91766
92805
|
const runId = opts.runId ?? crypto.randomUUID();
|
|
91767
92806
|
const isDry = opts.dryRun === true;
|
|
@@ -91862,7 +92901,7 @@ class WorkflowAppService {
|
|
|
91862
92901
|
};
|
|
91863
92902
|
}
|
|
91864
92903
|
async cleanRunLogs(retentionDays = 30, dryRun = false) {
|
|
91865
|
-
const runDir =
|
|
92904
|
+
const runDir = join27(this.ctx.cwd, ".spur", "run");
|
|
91866
92905
|
const fs3 = createNodeFileSystem3();
|
|
91867
92906
|
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
91868
92907
|
const reclaimed = [];
|
|
@@ -91874,7 +92913,7 @@ class WorkflowAppService {
|
|
|
91874
92913
|
return { retentionDays, dryRun, reclaimed, failures };
|
|
91875
92914
|
}
|
|
91876
92915
|
for (const name of entries) {
|
|
91877
|
-
const path9 =
|
|
92916
|
+
const path9 = join27(runDir, name);
|
|
91878
92917
|
const stat = await fs3.stat(path9);
|
|
91879
92918
|
if (stat === null || !stat.isFile() || stat.mtimeMs >= cutoffMs)
|
|
91880
92919
|
continue;
|
|
@@ -91893,13 +92932,13 @@ class WorkflowAppService {
|
|
|
91893
92932
|
return { retentionDays, dryRun, reclaimed, failures };
|
|
91894
92933
|
}
|
|
91895
92934
|
async cleanCheckpoints(retentionDays = 30, dryRun = false) {
|
|
91896
|
-
const sessionsDir =
|
|
92935
|
+
const sessionsDir = join27(this.ctx.cwd, ".spur", "memory", "sessions");
|
|
91897
92936
|
const fs3 = createNodeFileSystem3();
|
|
91898
92937
|
let sessionsReal;
|
|
91899
92938
|
try {
|
|
91900
92939
|
sessionsReal = realpathSync5(sessionsDir);
|
|
91901
92940
|
} catch {
|
|
91902
|
-
sessionsReal =
|
|
92941
|
+
sessionsReal = resolve14(sessionsDir);
|
|
91903
92942
|
}
|
|
91904
92943
|
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
91905
92944
|
const reclaimed = [];
|
|
@@ -91913,14 +92952,14 @@ class WorkflowAppService {
|
|
|
91913
92952
|
}
|
|
91914
92953
|
const activeRunIds = new Set((await new RunDao(await this.ctx.getDb()).listActiveRuns()).map((r) => r.id));
|
|
91915
92954
|
for (const name of entries) {
|
|
91916
|
-
const path9 =
|
|
92955
|
+
const path9 = join27(sessionsDir, name);
|
|
91917
92956
|
let resolved;
|
|
91918
92957
|
try {
|
|
91919
92958
|
resolved = realpathSync5(path9);
|
|
91920
92959
|
} catch {
|
|
91921
|
-
resolved =
|
|
92960
|
+
resolved = resolve14(path9);
|
|
91922
92961
|
}
|
|
91923
|
-
if (!resolved.startsWith(sessionsReal +
|
|
92962
|
+
if (!resolved.startsWith(sessionsReal + sep5)) {
|
|
91924
92963
|
skipped.push({ name, reason: "path-confinement: resolved outside .spur/memory/sessions" });
|
|
91925
92964
|
continue;
|
|
91926
92965
|
}
|
|
@@ -91990,22 +93029,60 @@ class WorkflowAppService {
|
|
|
91990
93029
|
if (row?.status !== "paused") {
|
|
91991
93030
|
throw new Error(`Run "${runId}" is not paused (or does not exist) - nothing to continue.`);
|
|
91992
93031
|
}
|
|
91993
|
-
await this.validateResumeCheckpointFreshness(runId, row);
|
|
91994
|
-
let resolved;
|
|
91995
|
-
try {
|
|
91996
|
-
resolved = await resolveWorkflowDefinition(this.ctx.cwd, row.workflow_name, {
|
|
91997
|
-
validateSchema: true,
|
|
91998
|
-
embeddedSchemas: this.ctx.embeddedSchemas?.()
|
|
91999
|
-
});
|
|
92000
|
-
} catch {
|
|
92001
|
-
throw new Error(`Cannot resume run "${runId}": workflow definition "${row.workflow_name}" not found in the workflow search paths.`);
|
|
92002
|
-
}
|
|
92003
93032
|
let rawMeta = {};
|
|
92004
93033
|
try {
|
|
92005
93034
|
rawMeta = JSON.parse(row.metadata_json || "{}");
|
|
92006
93035
|
} catch {
|
|
92007
93036
|
rawMeta = {};
|
|
92008
93037
|
}
|
|
93038
|
+
const warnings = [];
|
|
93039
|
+
const emitWarning = (message) => {
|
|
93040
|
+
warnings.push(message);
|
|
93041
|
+
try {
|
|
93042
|
+
this.ctx.warn?.(message);
|
|
93043
|
+
} catch {}
|
|
93044
|
+
};
|
|
93045
|
+
let pinned = null;
|
|
93046
|
+
const rawSource = rawMeta.definitionSource;
|
|
93047
|
+
if (rawSource !== undefined) {
|
|
93048
|
+
if (rawSource === null || typeof rawSource !== "object" || Array.isArray(rawSource)) {
|
|
93049
|
+
throw new Error(`Cannot resume run "${runId}": recorded definitionSource metadata is malformed (expected {path, layer, workdir}). Resume refused.`);
|
|
93050
|
+
}
|
|
93051
|
+
const candidate = rawSource;
|
|
93052
|
+
const sourcePath = candidate.path;
|
|
93053
|
+
const sourceLayer = candidate.layer;
|
|
93054
|
+
const sourceWorkdir = candidate.workdir;
|
|
93055
|
+
if (typeof sourcePath !== "string" || sourcePath === "" || sourceLayer !== "project" && sourceLayer !== "bundled" || typeof sourceWorkdir !== "string" || sourceWorkdir === "") {
|
|
93056
|
+
throw new Error(`Cannot resume run "${runId}": recorded definitionSource metadata is malformed (expected an absolute path, a project|bundled layer, and a workdir). Resume refused.`);
|
|
93057
|
+
}
|
|
93058
|
+
pinned = { path: sourcePath, layer: sourceLayer, workdir: sourceWorkdir };
|
|
93059
|
+
}
|
|
93060
|
+
const launchWorkdir = pinned?.workdir ?? this.ctx.cwd;
|
|
93061
|
+
await this.validateResumeCheckpointFreshness(runId, row, launchWorkdir);
|
|
93062
|
+
let resolved;
|
|
93063
|
+
if (pinned !== null) {
|
|
93064
|
+
const fs3 = createNodeFileSystem3();
|
|
93065
|
+
if (!fs3.exists(pinned.path)) {
|
|
93066
|
+
throw new Error(`Cannot resume run "${runId}": recorded launch source "${pinned.path}" no longer exists. Repair the source or start a new run; name-based fallback is refused.`);
|
|
93067
|
+
}
|
|
93068
|
+
resolved = await resolveWorkflowDefinition(pinned.workdir, pinned.path, {
|
|
93069
|
+
validateSchema: true,
|
|
93070
|
+
embeddedSchemas: this.ctx.embeddedSchemas?.()
|
|
93071
|
+
});
|
|
93072
|
+
if (resolved.path !== pinned.path) {
|
|
93073
|
+
throw new Error(`Cannot resume run "${runId}": definition resolved to "${resolved.path}" instead of the recorded launch source "${pinned.path}". Resume refused.`);
|
|
93074
|
+
}
|
|
93075
|
+
} else {
|
|
93076
|
+
try {
|
|
93077
|
+
resolved = await resolveWorkflowDefinition(this.ctx.cwd, row.workflow_name, {
|
|
93078
|
+
validateSchema: true,
|
|
93079
|
+
embeddedSchemas: this.ctx.embeddedSchemas?.()
|
|
93080
|
+
});
|
|
93081
|
+
} catch {
|
|
93082
|
+
throw new Error(`Cannot resume run "${runId}": workflow definition "${row.workflow_name}" not found in the workflow search paths.`);
|
|
93083
|
+
}
|
|
93084
|
+
emitWarning(`Run "${runId}" resumed by workflow name "${row.workflow_name}" (no recorded launch source \u2014 pre-source-pin run); identity checks run against the name-resolved definition.`);
|
|
93085
|
+
}
|
|
92009
93086
|
const persistedDigest = typeof rawMeta.definitionDigest === "string" ? rawMeta.definitionDigest : null;
|
|
92010
93087
|
const identityStamped = "workflowVersion" in rawMeta;
|
|
92011
93088
|
const persistedVersion = identityStamped && (typeof rawMeta.workflowVersion === "string" || rawMeta.workflowVersion === null) ? rawMeta.workflowVersion : undefined;
|
|
@@ -92025,6 +93102,18 @@ class WorkflowAppService {
|
|
|
92025
93102
|
throw new Error(`Cannot resume run "${runId}": workflow definition drift detected (launched with ${persistedDigest}, currently ${resolved.digest}). Resume refused.`);
|
|
92026
93103
|
}
|
|
92027
93104
|
}
|
|
93105
|
+
const db2 = await this.ctx.getDb();
|
|
93106
|
+
await new RunDao(db2).mergeMetadata(runId, {
|
|
93107
|
+
resumeDefinitionDigest: resolved.digest,
|
|
93108
|
+
resumeWorkflowVersion: workflowVersionLiteral(resolved.workflow)
|
|
93109
|
+
});
|
|
93110
|
+
emitWarning(`Consented definition drift on resume of run "${runId}": launched ${persistedDigest}, executing ${resolved.digest}; launch identity preserved, proof vars carry the executed digest.`);
|
|
93111
|
+
} else if ((("resumeDefinitionDigest" in rawMeta) || ("resumeWorkflowVersion" in rawMeta)) && (persistedDigest === null || persistedDigest === resolved.digest)) {
|
|
93112
|
+
const db2 = await this.ctx.getDb();
|
|
93113
|
+
await new RunDao(db2).mergeMetadata(runId, {
|
|
93114
|
+
resumeDefinitionDigest: null,
|
|
93115
|
+
resumeWorkflowVersion: null
|
|
93116
|
+
});
|
|
92028
93117
|
}
|
|
92029
93118
|
const svc = await this.createEngineService({
|
|
92030
93119
|
events: eventsBus,
|
|
@@ -92032,10 +93121,15 @@ class WorkflowAppService {
|
|
|
92032
93121
|
});
|
|
92033
93122
|
const workflow = resolved.workflow;
|
|
92034
93123
|
const hitlVar = opts?.hitlVar ?? "__hitlAnswer";
|
|
92035
|
-
const resumeVars =
|
|
93124
|
+
const resumeVars = {};
|
|
93125
|
+
if (opts?.hitlAnswer !== undefined)
|
|
93126
|
+
resumeVars[hitlVar] = opts.hitlAnswer;
|
|
93127
|
+
if (persistedDigest !== null && persistedDigest !== resolved.digest && "resumeDefinitionDigest" in rawMeta) {
|
|
93128
|
+
resumeVars.__definitionDigest = resolved.digest;
|
|
93129
|
+
}
|
|
92036
93130
|
const result = await svc.resumeRun(workflow, runId, {
|
|
92037
|
-
workdir:
|
|
92038
|
-
...resumeVars
|
|
93131
|
+
workdir: launchWorkdir,
|
|
93132
|
+
...Object.keys(resumeVars).length > 0 ? { vars: resumeVars } : {},
|
|
92039
93133
|
...eventsBus !== undefined ? {
|
|
92040
93134
|
events: withWorkflowIdentity(bridgeEventBus(eventsBus), workflow)
|
|
92041
93135
|
} : {}
|
|
@@ -92045,6 +93139,8 @@ class WorkflowAppService {
|
|
|
92045
93139
|
runResult.definitionDigest = persistedDigest;
|
|
92046
93140
|
if (persistedVersion !== undefined)
|
|
92047
93141
|
runResult.version = persistedVersion;
|
|
93142
|
+
if (warnings.length > 0)
|
|
93143
|
+
runResult.warnings = warnings;
|
|
92048
93144
|
return runResult;
|
|
92049
93145
|
}
|
|
92050
93146
|
async stampFailureReason(runId, result) {
|
|
@@ -92054,8 +93150,8 @@ class WorkflowAppService {
|
|
|
92054
93150
|
const db2 = await this.ctx.getDb();
|
|
92055
93151
|
await new RunDao(db2).stampFailureReason(runId, result.reason);
|
|
92056
93152
|
}
|
|
92057
|
-
async validateResumeCheckpointFreshness(runId, row) {
|
|
92058
|
-
const sessionsDir =
|
|
93153
|
+
async validateResumeCheckpointFreshness(runId, row, workdir) {
|
|
93154
|
+
const sessionsDir = join27(workdir, ".spur", "memory", "sessions");
|
|
92059
93155
|
const fs3 = createNodeFileSystem3();
|
|
92060
93156
|
let entries;
|
|
92061
93157
|
try {
|
|
@@ -92070,8 +93166,26 @@ class WorkflowAppService {
|
|
|
92070
93166
|
rawMeta = {};
|
|
92071
93167
|
}
|
|
92072
93168
|
const wbs = typeof rawMeta.wbs === "string" ? rawMeta.wbs : undefined;
|
|
93169
|
+
const executor = this.ctx.processExecutor?.() ?? new NodeProcessExecutor3;
|
|
93170
|
+
let head = null;
|
|
93171
|
+
const resolveHead = async () => {
|
|
93172
|
+
if (head !== null)
|
|
93173
|
+
return head;
|
|
93174
|
+
const probe = await executor.run({
|
|
93175
|
+
command: "git",
|
|
93176
|
+
args: ["rev-parse", "HEAD"],
|
|
93177
|
+
cwd: workdir,
|
|
93178
|
+
forceBuffered: true,
|
|
93179
|
+
rejectOnError: false
|
|
93180
|
+
});
|
|
93181
|
+
if (probe.exitCode !== 0 || probe.stdout.trim() === "") {
|
|
93182
|
+
throw new Error(`Cannot resume run "${runId}": checkpoint freshness requires the launch workdir's git HEAD, but "git rev-parse HEAD" failed in "${workdir}". Refusing rather than skipping the commit-drift check.`);
|
|
93183
|
+
}
|
|
93184
|
+
head = probe.stdout.trim();
|
|
93185
|
+
return head;
|
|
93186
|
+
};
|
|
92073
93187
|
for (const name of entries) {
|
|
92074
|
-
const filePath =
|
|
93188
|
+
const filePath = join27(sessionsDir, name);
|
|
92075
93189
|
let raw;
|
|
92076
93190
|
try {
|
|
92077
93191
|
raw = await fs3.readFile(filePath);
|
|
@@ -92090,10 +93204,20 @@ class WorkflowAppService {
|
|
|
92090
93204
|
if (meta3.workflow !== "" && meta3.workflow !== row.workflow_name) {
|
|
92091
93205
|
throw new Error(`Cannot resume run "${runId}": checkpoint is stale (workflow-mismatch: checkpoint workflow "${meta3.workflow}" != "${row.workflow_name}").`);
|
|
92092
93206
|
}
|
|
92093
|
-
if (meta3.status
|
|
92094
|
-
throw new Error(`Cannot resume run "${runId}": checkpoint is stale (status-
|
|
93207
|
+
if (meta3.status === "") {
|
|
93208
|
+
throw new Error(`Cannot resume run "${runId}": checkpoint is stale (status-missing: checkpoint carries no status).`);
|
|
92095
93209
|
}
|
|
92096
|
-
|
|
93210
|
+
if (isTerminalCheckpointStatus(meta3.status)) {
|
|
93211
|
+
throw new Error(`Cannot resume run "${runId}": checkpoint is stale (terminal: status=${meta3.status}).`);
|
|
93212
|
+
}
|
|
93213
|
+
if (meta3.status !== "pending" && meta3.status !== "running" && meta3.status !== "approved") {
|
|
93214
|
+
throw new Error(`Cannot resume run "${runId}": checkpoint is stale (unknown checkpoint status "${meta3.status}" \u2014 expected pending/running/approved).`);
|
|
93215
|
+
}
|
|
93216
|
+
const staleness = checkpointStaleness(meta3, {
|
|
93217
|
+
...wbs !== undefined ? { taskWbs: wbs } : {},
|
|
93218
|
+
...meta3.sourceCommit === "" ? {} : { sourceCommit: await resolveHead() },
|
|
93219
|
+
artifactExists: (p) => fs3.exists(isAbsolute7(p) ? p : join27(workdir, p))
|
|
93220
|
+
});
|
|
92097
93221
|
if (staleness.stale) {
|
|
92098
93222
|
throw new Error(`Cannot resume run "${runId}": checkpoint is stale (${staleness.reason}).`);
|
|
92099
93223
|
}
|
|
@@ -92102,12 +93226,12 @@ class WorkflowAppService {
|
|
|
92102
93226
|
}
|
|
92103
93227
|
async list(workflowPaths = [".spur/workflows/"]) {
|
|
92104
93228
|
const projectRoot = this.ctx.cwd;
|
|
92105
|
-
const globalRoot =
|
|
93229
|
+
const globalRoot = join27(homedir8(), ".config", "spur");
|
|
92106
93230
|
const layers = [];
|
|
92107
93231
|
const entries = [];
|
|
92108
93232
|
const scannedPaths = new Set;
|
|
92109
93233
|
for (const relPath of workflowPaths) {
|
|
92110
|
-
const absPath =
|
|
93234
|
+
const absPath = resolve14(projectRoot, relPath);
|
|
92111
93235
|
layers.push({ id: "project", path: absPath });
|
|
92112
93236
|
scannedPaths.add(absPath);
|
|
92113
93237
|
try {
|
|
@@ -92116,7 +93240,7 @@ class WorkflowAppService {
|
|
|
92116
93240
|
} catch {}
|
|
92117
93241
|
}
|
|
92118
93242
|
for (const relPath of workflowPaths) {
|
|
92119
|
-
const absPath =
|
|
93243
|
+
const absPath = resolve14(globalRoot, relPath);
|
|
92120
93244
|
if (scannedPaths.has(absPath))
|
|
92121
93245
|
continue;
|
|
92122
93246
|
layers.push({ id: "global", path: absPath });
|
|
@@ -92302,14 +93426,15 @@ class WorkflowAppService {
|
|
|
92302
93426
|
if (opts.extensions !== undefined) {
|
|
92303
93427
|
persistence2 = withRunIdentityRecording(persistence2, db2, {
|
|
92304
93428
|
definitionDigest: computeDefinitionDigest(opts.extensions.workflow),
|
|
92305
|
-
workflowVersion: workflowVersionLiteral(opts.extensions.workflow)
|
|
93429
|
+
workflowVersion: workflowVersionLiteral(opts.extensions.workflow),
|
|
93430
|
+
...opts.definitionSource !== undefined ? { source: opts.definitionSource } : {}
|
|
92306
93431
|
});
|
|
92307
93432
|
}
|
|
92308
93433
|
const adapter = bus ? new ObservableWorkflowAdapter(persistence2, opts.extensions !== undefined ? withWorkflowIdentity(bus, opts.extensions.workflow) : bus) : persistence2;
|
|
92309
93434
|
return new WorkflowService(host, adapter);
|
|
92310
93435
|
}
|
|
92311
93436
|
async loadWorkflowExtensions(host, workflow, file2) {
|
|
92312
|
-
const refs = collectWorkflowExtensions(workflow.name,
|
|
93437
|
+
const refs = collectWorkflowExtensions(workflow.name, dirname18(file2), workflow.extensions);
|
|
92313
93438
|
if (refs.length === 0)
|
|
92314
93439
|
return;
|
|
92315
93440
|
const nodeFs = createNodeFileSystem3();
|
|
@@ -92576,8 +93701,8 @@ function resolveOutputLogConfig(config4) {
|
|
|
92576
93701
|
};
|
|
92577
93702
|
}
|
|
92578
93703
|
async function outputArtifactForRun(cwd, runId) {
|
|
92579
|
-
const
|
|
92580
|
-
return await fileExists(
|
|
93704
|
+
const relative6 = join27(".spur", "run", `${runId}.log`);
|
|
93705
|
+
return await fileExists(join27(cwd, relative6)) ? relative6 : undefined;
|
|
92581
93706
|
}
|
|
92582
93707
|
async function scanWorkflowFiles(rootPath, source, loadEntry) {
|
|
92583
93708
|
const entries = [];
|
|
@@ -92590,7 +93715,7 @@ async function scanWorkflowFiles(rootPath, source, loadEntry) {
|
|
|
92590
93715
|
async function walk3(dirPath) {
|
|
92591
93716
|
const names = await fs3.readDir(dirPath);
|
|
92592
93717
|
for (const name of names) {
|
|
92593
|
-
const fullPath =
|
|
93718
|
+
const fullPath = resolve14(dirPath, name);
|
|
92594
93719
|
const st = await fs3.stat(fullPath);
|
|
92595
93720
|
if (!st)
|
|
92596
93721
|
continue;
|
|
@@ -92762,8 +93887,8 @@ function projectActionTraceResult(resultJson, secretValues = []) {
|
|
|
92762
93887
|
async function partialArtifactForAction(cwd, runId, node, ok) {
|
|
92763
93888
|
if (ok !== false || !TRACE_IDENTIFIER.test(runId) || !TRACE_IDENTIFIER.test(node))
|
|
92764
93889
|
return;
|
|
92765
|
-
const relativePath2 =
|
|
92766
|
-
return await fileExists(
|
|
93890
|
+
const relativePath2 = join27(".spur", "run", `${runId}-${node}-partial.md`);
|
|
93891
|
+
return await fileExists(resolve14(cwd, relativePath2)) ? relativePath2 : undefined;
|
|
92767
93892
|
}
|
|
92768
93893
|
var TASK_PIPELINE_WORKFLOW = "task-pipeline", PIPELINE_LINK_KIND = "pipeline", TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
|
|
92769
93894
|
var init_workflow_service = __esm(() => {
|
|
@@ -92801,7 +93926,7 @@ var init_workflow_service = __esm(() => {
|
|
|
92801
93926
|
});
|
|
92802
93927
|
|
|
92803
93928
|
// ../../packages/app/src/workflow/idea-handoff.ts
|
|
92804
|
-
import { resolve as
|
|
93929
|
+
import { resolve as resolve15 } from "path";
|
|
92805
93930
|
function processEvidence(r) {
|
|
92806
93931
|
return `exit=${r.exitCode ?? "null"}${r.signal ? ` signal=${r.signal}` : ""}: ${r.stderr.trim() || "no stderr"}`;
|
|
92807
93932
|
}
|
|
@@ -92811,11 +93936,12 @@ async function finalizeIdeaHandoff(options) {
|
|
|
92811
93936
|
const executor = options.processExecutor ?? new NodeProcessExecutor3;
|
|
92812
93937
|
const spurBin = options.spurBin ?? "spur";
|
|
92813
93938
|
const { runId, featureId: featureId2 } = options;
|
|
92814
|
-
const runDir =
|
|
92815
|
-
const batchPath =
|
|
92816
|
-
const resultPath =
|
|
92817
|
-
const orderPath =
|
|
92818
|
-
const
|
|
93939
|
+
const runDir = resolve15(root, ".spur", "run");
|
|
93940
|
+
const batchPath = resolve15(runDir, `${runId}-idea-task-batch.json`);
|
|
93941
|
+
const resultPath = resolve15(runDir, `${runId}-idea-batch-create-result.json`);
|
|
93942
|
+
const orderPath = resolve15(runDir, `${runId}-idea-task-order.json`);
|
|
93943
|
+
const readyPath = resolve15(runDir, `${runId}-idea-ready.json`);
|
|
93944
|
+
const reportPath = resolve15(runDir, `${runId}-idea-handoff.md`);
|
|
92819
93945
|
const spurSplit = splitLaunchCommand(spurBin, 'idea-handoff "spurBin"');
|
|
92820
93946
|
if ("error" in spurSplit) {
|
|
92821
93947
|
return {
|
|
@@ -92841,6 +93967,23 @@ async function finalizeIdeaHandoff(options) {
|
|
|
92841
93967
|
const batch = JSON.parse(await fs3.readFile(batchPath));
|
|
92842
93968
|
const result = JSON.parse(await fs3.readFile(resultPath));
|
|
92843
93969
|
const order = JSON.parse(await fs3.readFile(orderPath));
|
|
93970
|
+
const sidecarExists = await fs3.exists(readyPath);
|
|
93971
|
+
const evidenceRows = await (async () => {
|
|
93972
|
+
if (!sidecarExists)
|
|
93973
|
+
return [];
|
|
93974
|
+
try {
|
|
93975
|
+
const parsed = JSON.parse(await fs3.readFile(readyPath));
|
|
93976
|
+
const tasks = typeof parsed === "object" && parsed !== null && "tasks" in parsed ? parsed.tasks : undefined;
|
|
93977
|
+
return Array.isArray(tasks) ? tasks : [];
|
|
93978
|
+
} catch {
|
|
93979
|
+
return [];
|
|
93980
|
+
}
|
|
93981
|
+
})();
|
|
93982
|
+
const evidenceByWbs = new Map;
|
|
93983
|
+
for (const row of evidenceRows) {
|
|
93984
|
+
if (typeof row?.wbs === "string")
|
|
93985
|
+
evidenceByWbs.set(row.wbs, row);
|
|
93986
|
+
}
|
|
92844
93987
|
if (!Array.isArray(batch) || !Array.isArray(result?.wbs) || !Array.isArray(order)) {
|
|
92845
93988
|
return {
|
|
92846
93989
|
ok: false,
|
|
@@ -92932,23 +94075,92 @@ async function finalizeIdeaHandoff(options) {
|
|
|
92932
94075
|
}
|
|
92933
94076
|
const checkResults = [];
|
|
92934
94077
|
for (const wbs of result.wbs) {
|
|
92935
|
-
const
|
|
94078
|
+
const pathRes = await executor.run({
|
|
92936
94079
|
command: spurCommand,
|
|
92937
|
-
args: [...spurArgsPrefix, "task", "
|
|
94080
|
+
args: [...spurArgsPrefix, "task", "path", wbs, "--json"],
|
|
92938
94081
|
cwd: root,
|
|
92939
94082
|
forceBuffered: true,
|
|
92940
94083
|
rejectOnError: false
|
|
92941
94084
|
});
|
|
92942
|
-
if (
|
|
94085
|
+
if (pathRes.exitCode === null) {
|
|
92943
94086
|
return {
|
|
92944
94087
|
ok: false,
|
|
92945
94088
|
wbsList: result.wbs,
|
|
92946
94089
|
nextCommand: "",
|
|
92947
94090
|
reportPath,
|
|
92948
|
-
error: `Task
|
|
94091
|
+
error: `Task path for ${wbs} could not be spawned (${spurCommand}): ${processEvidence(pathRes)}`
|
|
92949
94092
|
};
|
|
92950
94093
|
}
|
|
92951
|
-
|
|
94094
|
+
let reason;
|
|
94095
|
+
if (pathRes.exitCode !== 0) {
|
|
94096
|
+
reason = `task path resolution failed: ${processEvidence(pathRes)}`;
|
|
94097
|
+
} else {
|
|
94098
|
+
let filePath;
|
|
94099
|
+
try {
|
|
94100
|
+
const parsed = JSON.parse(pathRes.stdout);
|
|
94101
|
+
if (typeof parsed === "object" && parsed !== null && "filePath" in parsed) {
|
|
94102
|
+
const fp = parsed.filePath;
|
|
94103
|
+
if (typeof fp === "string")
|
|
94104
|
+
filePath = fp;
|
|
94105
|
+
}
|
|
94106
|
+
} catch {
|
|
94107
|
+
filePath = undefined;
|
|
94108
|
+
}
|
|
94109
|
+
if (filePath === undefined) {
|
|
94110
|
+
reason = "task path output carried no filePath";
|
|
94111
|
+
} else {
|
|
94112
|
+
let digest;
|
|
94113
|
+
try {
|
|
94114
|
+
digest = computePlanningDigest(await fs3.readFile(filePath));
|
|
94115
|
+
} catch {
|
|
94116
|
+
digest = undefined;
|
|
94117
|
+
}
|
|
94118
|
+
if (digest === undefined) {
|
|
94119
|
+
reason = `task file unreadable at ${filePath}`;
|
|
94120
|
+
} else {
|
|
94121
|
+
const row = evidenceByWbs.get(wbs);
|
|
94122
|
+
if (row === undefined) {
|
|
94123
|
+
reason = "no ready evidence recorded for this run";
|
|
94124
|
+
} else if (row.status !== "ready") {
|
|
94125
|
+
reason = `ready evidence status is ${String(row.status)}`;
|
|
94126
|
+
} else if (typeof row.planningDigest !== "string" || row.planningDigest !== digest) {
|
|
94127
|
+
reason = "planning digest stale \u2014 task content changed after preparation";
|
|
94128
|
+
} else {
|
|
94129
|
+
const verdict = verifyReadyChecks(Array.isArray(row.checks) ? row.checks : undefined);
|
|
94130
|
+
if (!verdict.ok)
|
|
94131
|
+
reason = verdict.reason;
|
|
94132
|
+
}
|
|
94133
|
+
}
|
|
94134
|
+
}
|
|
94135
|
+
}
|
|
94136
|
+
if (reason === undefined) {
|
|
94137
|
+
const checkRes = await executor.run({
|
|
94138
|
+
command: spurCommand,
|
|
94139
|
+
args: [...spurArgsPrefix, "task", "check", wbs, "--json"],
|
|
94140
|
+
cwd: root,
|
|
94141
|
+
forceBuffered: true,
|
|
94142
|
+
rejectOnError: false
|
|
94143
|
+
});
|
|
94144
|
+
if (checkRes.exitCode === null) {
|
|
94145
|
+
return {
|
|
94146
|
+
ok: false,
|
|
94147
|
+
wbsList: result.wbs,
|
|
94148
|
+
nextCommand: "",
|
|
94149
|
+
reportPath,
|
|
94150
|
+
error: `Task check for ${wbs} could not be spawned (${spurCommand}): ${processEvidence(checkRes)}`
|
|
94151
|
+
};
|
|
94152
|
+
}
|
|
94153
|
+
if (checkRes.exitCode !== 0) {
|
|
94154
|
+
reason = `deterministic task check failed (${processEvidence(checkRes)})`;
|
|
94155
|
+
}
|
|
94156
|
+
}
|
|
94157
|
+
const pass = reason === undefined;
|
|
94158
|
+
checkResults.push({
|
|
94159
|
+
wbs,
|
|
94160
|
+
pass,
|
|
94161
|
+
status: pass ? "ready" : "unready",
|
|
94162
|
+
...pass ? {} : { reason, action: readyRefineCommand(wbs) }
|
|
94163
|
+
});
|
|
92952
94164
|
}
|
|
92953
94165
|
const anyFailed = checkResults.some((c3) => !c3.pass);
|
|
92954
94166
|
const nextCommand = anyFailed ? `/sp:dev-refineall --feature ${featureId2} --auto --depth ready` : `/sp:dev-runall --feature ${featureId2} --auto`;
|
|
@@ -92961,12 +94173,18 @@ async function finalizeIdeaHandoff(options) {
|
|
|
92961
94173
|
"## Created tasks",
|
|
92962
94174
|
...result.wbs.map((w) => ` - ${w}`),
|
|
92963
94175
|
"",
|
|
92964
|
-
"## Per-task readiness (
|
|
94176
|
+
"## Per-task readiness (evidence + digest + task check)",
|
|
92965
94177
|
"",
|
|
92966
|
-
"| WBS | Outcome |",
|
|
92967
|
-
"
|
|
92968
|
-
...checkResults.map((c3) => `| ${c3.wbs} | ${c3.pass ? "
|
|
94178
|
+
"| WBS | Outcome | Reason |",
|
|
94179
|
+
"|-----|---------|--------|",
|
|
94180
|
+
...checkResults.map((c3) => `| ${c3.wbs} | ${c3.pass ? "READY" : "UNREADY"} | ${c3.reason ?? "-"} |`),
|
|
92969
94181
|
"",
|
|
94182
|
+
...anyFailed ? [
|
|
94183
|
+
"## Preparation actions",
|
|
94184
|
+
"",
|
|
94185
|
+
...checkResults.filter((c3) => !c3.pass).map((c3) => ` - ${c3.wbs}: ${c3.action}`),
|
|
94186
|
+
""
|
|
94187
|
+
] : [],
|
|
92970
94188
|
"## Next command",
|
|
92971
94189
|
"",
|
|
92972
94190
|
nextCommand,
|
|
@@ -92993,6 +94211,7 @@ async function finalizeIdeaHandoff(options) {
|
|
|
92993
94211
|
}
|
|
92994
94212
|
var init_idea_handoff = __esm(() => {
|
|
92995
94213
|
init_dist5();
|
|
94214
|
+
init_task_readiness();
|
|
92996
94215
|
init_split_launch_command();
|
|
92997
94216
|
});
|
|
92998
94217
|
|
|
@@ -93515,7 +94734,7 @@ var init_progress_follow = __esm(() => {
|
|
|
93515
94734
|
});
|
|
93516
94735
|
|
|
93517
94736
|
// ../../packages/app/src/workflow/trace-writer.ts
|
|
93518
|
-
import { dirname as
|
|
94737
|
+
import { dirname as dirname19, join as join28 } from "path";
|
|
93519
94738
|
|
|
93520
94739
|
class WorkflowTraceWriter {
|
|
93521
94740
|
path;
|
|
@@ -93523,7 +94742,7 @@ class WorkflowTraceWriter {
|
|
|
93523
94742
|
pending = Promise.resolve();
|
|
93524
94743
|
constructor(cwd, runId) {
|
|
93525
94744
|
const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
93526
|
-
this.path =
|
|
94745
|
+
this.path = join28(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
|
|
93527
94746
|
}
|
|
93528
94747
|
attach(bus) {
|
|
93529
94748
|
bus.on("workflow.run.started", (event) => this.enqueue("workflow.run.started", event));
|
|
@@ -93549,7 +94768,7 @@ class WorkflowTraceWriter {
|
|
|
93549
94768
|
`;
|
|
93550
94769
|
this.pending = this.pending.then(async () => {
|
|
93551
94770
|
const fs3 = createNodeFileSystem3();
|
|
93552
|
-
await fs3.ensureDir(
|
|
94771
|
+
await fs3.ensureDir(dirname19(this.path));
|
|
93553
94772
|
await fs3.appendFile(this.path, line);
|
|
93554
94773
|
});
|
|
93555
94774
|
}
|
|
@@ -93563,6 +94782,7 @@ var exports_src2 = {};
|
|
|
93563
94782
|
__export(exports_src2, {
|
|
93564
94783
|
writeJsonError: () => writeJsonError,
|
|
93565
94784
|
waitForOccupant: () => waitForOccupant,
|
|
94785
|
+
verifyReadyChecks: () => verifyReadyChecks,
|
|
93566
94786
|
verdictRowsMatchScenarios: () => verdictRowsMatchScenarios,
|
|
93567
94787
|
validateSchedulerCustomJobPayload: () => validateSchedulerCustomJobPayload,
|
|
93568
94788
|
validateHistoryRefreshPayload: () => validateHistoryRefreshPayload,
|
|
@@ -93605,6 +94825,8 @@ __export(exports_src2, {
|
|
|
93605
94825
|
registerSpurBuiltins: () => registerSpurBuiltins,
|
|
93606
94826
|
refreshHistoryRollups: () => refreshHistoryRollups,
|
|
93607
94827
|
redactAndBound: () => redactAndBound,
|
|
94828
|
+
readyRefineCommand: () => readyRefineCommand,
|
|
94829
|
+
readyRefineAllCommand: () => readyRefineAllCommand,
|
|
93608
94830
|
readVerdictArtifact: () => readVerdictArtifact,
|
|
93609
94831
|
readVerdict: () => readVerdict,
|
|
93610
94832
|
qualifySectionBody: () => qualifySectionBody,
|
|
@@ -93615,6 +94837,8 @@ __export(exports_src2, {
|
|
|
93615
94837
|
projectStoredSystemEventEnvelope: () => projectStoredSystemEventEnvelope,
|
|
93616
94838
|
projectLifecycle: () => projectLifecycle,
|
|
93617
94839
|
probePort: () => probePort,
|
|
94840
|
+
prepareCreatedTaskReady: () => prepareCreatedTaskReady,
|
|
94841
|
+
prepareBatchTaskReady: () => prepareBatchTaskReady,
|
|
93618
94842
|
portBindingAvailable: () => portBindingAvailable,
|
|
93619
94843
|
permissionFailureEvidence: () => permissionFailureEvidence,
|
|
93620
94844
|
parseVerdict: () => parseVerdict,
|
|
@@ -93654,6 +94878,7 @@ __export(exports_src2, {
|
|
|
93654
94878
|
extractResolvedWorkflowFacts: () => extractResolvedWorkflowFacts,
|
|
93655
94879
|
extractProofDigest: () => extractProofDigest,
|
|
93656
94880
|
extractFeatureProofData: () => extractFeatureProofData,
|
|
94881
|
+
extractBatchArray: () => extractBatchArray,
|
|
93657
94882
|
evaluateDoneTransition: () => evaluateDoneTransition,
|
|
93658
94883
|
escalationFingerprint: () => escalationFingerprint,
|
|
93659
94884
|
envelopeEnabled: () => envelopeEnabled,
|
|
@@ -93670,6 +94895,7 @@ __export(exports_src2, {
|
|
|
93670
94895
|
createFileAgentInstanceStore: () => createFileAgentInstanceStore,
|
|
93671
94896
|
configuredSecretValues: () => configuredSecretValues,
|
|
93672
94897
|
computeProofInputFingerprint: () => computeProofInputFingerprint,
|
|
94898
|
+
computePlanningDigest: () => computePlanningDigest,
|
|
93673
94899
|
computeExitCode: () => computeExitCode,
|
|
93674
94900
|
computeDefinitionDigest: () => computeDefinitionDigest,
|
|
93675
94901
|
computeAggregate: () => computeAggregate,
|
|
@@ -93702,8 +94928,10 @@ __export(exports_src2, {
|
|
|
93702
94928
|
TeamService: () => TeamService,
|
|
93703
94929
|
TaskService: () => TaskService,
|
|
93704
94930
|
TaskScaffoldService: () => TaskScaffoldService,
|
|
94931
|
+
TaskPreparationError: () => TaskPreparationError,
|
|
93705
94932
|
TaskLocator: () => TaskLocator,
|
|
93706
94933
|
TaskCheckService: () => TaskCheckService,
|
|
94934
|
+
TaskCandidateInvalidError: () => TaskCandidateInvalidError,
|
|
93707
94935
|
TOKEN_LEDGER_RELATIVE_PATH: () => TOKEN_LEDGER_RELATIVE_PATH,
|
|
93708
94936
|
TOKEN_LEDGER_MAX_LIMIT: () => TOKEN_LEDGER_MAX_LIMIT,
|
|
93709
94937
|
TOKEN_LEDGER_DEFAULT_LIMIT: () => TOKEN_LEDGER_DEFAULT_LIMIT,
|
|
@@ -93735,6 +94963,9 @@ __export(exports_src2, {
|
|
|
93735
94963
|
RUN_STORE_WBS_DEFAULT_LIMIT: () => RUN_STORE_WBS_DEFAULT_LIMIT,
|
|
93736
94964
|
RUN_STORE_LIST_MAX_LIMIT: () => RUN_STORE_LIST_MAX_LIMIT,
|
|
93737
94965
|
RUN_STORE_LIST_DEFAULT_LIMIT: () => RUN_STORE_LIST_DEFAULT_LIMIT,
|
|
94966
|
+
READY_SKIPPED: () => READY_SKIPPED,
|
|
94967
|
+
READY_DONE: () => READY_DONE,
|
|
94968
|
+
READY_CHECKLIST_IDS: () => READY_CHECKLIST_IDS,
|
|
93738
94969
|
ProofInputFingerprint: () => ProofInputFingerprint,
|
|
93739
94970
|
ProofCaptureError: () => ProofCaptureError,
|
|
93740
94971
|
ProjectRegistry: () => ProjectRegistry,
|
|
@@ -93744,6 +94975,7 @@ __export(exports_src2, {
|
|
|
93744
94975
|
PS_LIST_ARGV: () => PS_LIST_ARGV,
|
|
93745
94976
|
POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
|
|
93746
94977
|
PLANNING_EVENT_NAMES: () => PLANNING_EVENT_NAMES,
|
|
94978
|
+
PLANNING_DIGEST_SECTIONS: () => PLANNING_DIGEST_SECTIONS,
|
|
93747
94979
|
ObservableWorkflowAdapter: () => ObservableWorkflowAdapter,
|
|
93748
94980
|
MockHistoryBoardService: () => MockHistoryBoardService,
|
|
93749
94981
|
MIN_SAFE_PI_BASH_IMPORTER_VERSION: () => MIN_SAFE_PI_BASH_IMPORTER_VERSION,
|
|
@@ -93776,6 +95008,7 @@ __export(exports_src2, {
|
|
|
93776
95008
|
DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA: () => DEFAULT_SYSTEM_EVENT_RETENTION_QUOTA,
|
|
93777
95009
|
DEFAULT_STALL_MS: () => DEFAULT_STALL_MS,
|
|
93778
95010
|
DEFAULT_RUN_LOG_MAX_BYTES: () => DEFAULT_RUN_LOG_MAX_BYTES,
|
|
95011
|
+
DEFAULT_READY_PREPARE_TIMEOUT_MS: () => DEFAULT_READY_PREPARE_TIMEOUT_MS,
|
|
93779
95012
|
DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX,
|
|
93780
95013
|
CorpusMigrator: () => CorpusMigrator,
|
|
93781
95014
|
CommandGateActionRunner: () => CommandGateActionRunner,
|
|
@@ -93821,6 +95054,7 @@ var init_src3 = __esm(async () => {
|
|
|
93821
95054
|
init_rule_service();
|
|
93822
95055
|
init_run_session_observer();
|
|
93823
95056
|
init_run_store_service();
|
|
95057
|
+
init_scheduler_custom_job_service();
|
|
93824
95058
|
init_supervisor_service();
|
|
93825
95059
|
init_system_event_emitter();
|
|
93826
95060
|
init_system_event_follow();
|
|
@@ -93829,6 +95063,7 @@ var init_src3 = __esm(async () => {
|
|
|
93829
95063
|
init_task_attribution2();
|
|
93830
95064
|
init_task_check();
|
|
93831
95065
|
init_task_locator();
|
|
95066
|
+
init_task_readiness();
|
|
93832
95067
|
init_task_record();
|
|
93833
95068
|
init_task_scaffold();
|
|
93834
95069
|
init_task_service();
|
|
@@ -101924,15 +103159,15 @@ function parseTimeout(raw) {
|
|
|
101924
103159
|
return n3;
|
|
101925
103160
|
}
|
|
101926
103161
|
function loopSleep(ms, signal) {
|
|
101927
|
-
return new Promise((
|
|
103162
|
+
return new Promise((resolve16) => {
|
|
101928
103163
|
if (signal?.aborted) {
|
|
101929
|
-
|
|
103164
|
+
resolve16();
|
|
101930
103165
|
return;
|
|
101931
103166
|
}
|
|
101932
|
-
const timer = setTimeout(
|
|
103167
|
+
const timer = setTimeout(resolve16, ms);
|
|
101933
103168
|
signal?.addEventListener("abort", () => {
|
|
101934
103169
|
clearTimeout(timer);
|
|
101935
|
-
|
|
103170
|
+
resolve16();
|
|
101936
103171
|
}, { once: true });
|
|
101937
103172
|
});
|
|
101938
103173
|
}
|
|
@@ -102064,7 +103299,7 @@ function waitFail(context4, options, code, message) {
|
|
|
102064
103299
|
// src/release-ops.ts
|
|
102065
103300
|
init_dist5();
|
|
102066
103301
|
import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync10 } from "fs";
|
|
102067
|
-
import { join as
|
|
103302
|
+
import { join as join29 } from "path";
|
|
102068
103303
|
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
102069
103304
|
function releaseTag(config4, version3) {
|
|
102070
103305
|
return `${config4.packageName}${config4.tagVersionSeparator}${version3}`;
|
|
@@ -102110,7 +103345,7 @@ async function readJson(path9) {
|
|
|
102110
103345
|
function rootWorkspaceGlobs(repoRoot) {
|
|
102111
103346
|
let root;
|
|
102112
103347
|
try {
|
|
102113
|
-
root = JSON.parse(readFileSync10(
|
|
103348
|
+
root = JSON.parse(readFileSync10(join29(repoRoot, "package.json"), "utf8"));
|
|
102114
103349
|
} catch {
|
|
102115
103350
|
return [];
|
|
102116
103351
|
}
|
|
@@ -102127,11 +103362,11 @@ function workspaceDirs(repoRoot) {
|
|
|
102127
103362
|
if (!glob.endsWith("/*"))
|
|
102128
103363
|
continue;
|
|
102129
103364
|
const parent = glob.slice(0, -2);
|
|
102130
|
-
const parentAbs =
|
|
103365
|
+
const parentAbs = join29(repoRoot, parent);
|
|
102131
103366
|
if (!existsSync10(parentAbs))
|
|
102132
103367
|
continue;
|
|
102133
103368
|
for (const entry of readdirSync7(parentAbs, { withFileTypes: true })) {
|
|
102134
|
-
if (entry.isDirectory() && existsSync10(
|
|
103369
|
+
if (entry.isDirectory() && existsSync10(join29(parentAbs, entry.name, "package.json"))) {
|
|
102135
103370
|
dirs.push(`${parent}/${entry.name}`);
|
|
102136
103371
|
}
|
|
102137
103372
|
}
|
|
@@ -102139,14 +103374,14 @@ function workspaceDirs(repoRoot) {
|
|
|
102139
103374
|
return dirs;
|
|
102140
103375
|
}
|
|
102141
103376
|
async function releaseContext(repoRoot) {
|
|
102142
|
-
const rootManifest = await readJson(
|
|
103377
|
+
const rootManifest = await readJson(join29(repoRoot, "package.json"));
|
|
102143
103378
|
if (rootManifest === null) {
|
|
102144
103379
|
throw new Error(`no package.json found at ${repoRoot} \u2014 builder operates on a package workspace root.`);
|
|
102145
103380
|
}
|
|
102146
103381
|
const rootName = typeof rootManifest.name === "string" ? rootManifest.name : "root";
|
|
102147
103382
|
const workspaces2 = [];
|
|
102148
103383
|
for (const dir of workspaceDirs(repoRoot)) {
|
|
102149
|
-
const manifest = await readJson(
|
|
103384
|
+
const manifest = await readJson(join29(repoRoot, dir, "package.json"));
|
|
102150
103385
|
if (manifest === null)
|
|
102151
103386
|
continue;
|
|
102152
103387
|
workspaces2.push({
|
|
@@ -102159,7 +103394,7 @@ async function releaseContext(repoRoot) {
|
|
|
102159
103394
|
const shortName = (name) => name.split("/").pop() ?? name;
|
|
102160
103395
|
const packages = new Map;
|
|
102161
103396
|
for (const ws of workspaces2) {
|
|
102162
|
-
const versionSourceFile = existsSync10(
|
|
103397
|
+
const versionSourceFile = existsSync10(join29(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
|
|
102163
103398
|
packages.set(shortName(ws.name), {
|
|
102164
103399
|
packageDir: ws.dir,
|
|
102165
103400
|
packageName: ws.name,
|
|
@@ -102194,7 +103429,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
|
|
|
102194
103429
|
const changed = [];
|
|
102195
103430
|
for (const dir of workspaceDirs(ctx.repoRoot)) {
|
|
102196
103431
|
const relPath = `${dir}/package.json`;
|
|
102197
|
-
const manifest = await readJson(
|
|
103432
|
+
const manifest = await readJson(join29(ctx.repoRoot, relPath));
|
|
102198
103433
|
if (manifest === null)
|
|
102199
103434
|
continue;
|
|
102200
103435
|
let dirty = false;
|
|
@@ -102211,7 +103446,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
|
|
|
102211
103446
|
}
|
|
102212
103447
|
}
|
|
102213
103448
|
if (dirty) {
|
|
102214
|
-
await Bun.write(
|
|
103449
|
+
await Bun.write(join29(ctx.repoRoot, relPath), `${JSON.stringify(manifest, null, 4)}
|
|
102215
103450
|
`);
|
|
102216
103451
|
output2.write(` \u21B3 ${relPath}: ${pkgName} workspace pin ${oldVersion} \u2192 ${newVersion}`);
|
|
102217
103452
|
changed.push(relPath);
|
|
@@ -102220,7 +103455,7 @@ async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2
|
|
|
102220
103455
|
return changed;
|
|
102221
103456
|
}
|
|
102222
103457
|
async function updateVersionSourceFile(ctx, filePath, previous, next, output2) {
|
|
102223
|
-
const absPath =
|
|
103458
|
+
const absPath = join29(ctx.repoRoot, filePath);
|
|
102224
103459
|
const content = await Bun.file(absPath).text();
|
|
102225
103460
|
const updated = content.replace(/(binaryVersion:\s*['"])\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?(['"])/, `$1${next}$2`);
|
|
102226
103461
|
if (updated === content) {
|
|
@@ -102232,7 +103467,7 @@ async function updateVersionSourceFile(ctx, filePath, previous, next, output2) {
|
|
|
102232
103467
|
return true;
|
|
102233
103468
|
}
|
|
102234
103469
|
async function syncMarketplaceAndPlugins(ctx, version3, staged, output2) {
|
|
102235
|
-
const marketplacePath =
|
|
103470
|
+
const marketplacePath = join29(ctx.repoRoot, ".claude-plugin", "marketplace.json");
|
|
102236
103471
|
const marketplaceFile = Bun.file(marketplacePath);
|
|
102237
103472
|
if (!await marketplaceFile.exists())
|
|
102238
103473
|
return;
|
|
@@ -102262,7 +103497,7 @@ async function syncMarketplaceAndPlugins(ctx, version3, staged, output2) {
|
|
|
102262
103497
|
output2.write(`Bumped marketplace plugins to ${version3}`);
|
|
102263
103498
|
}
|
|
102264
103499
|
for (const entry of plugins) {
|
|
102265
|
-
const pluginJsonPath =
|
|
103500
|
+
const pluginJsonPath = join29(ctx.repoRoot, entry.source, "plugin.json");
|
|
102266
103501
|
const pluginJson = await readJson(pluginJsonPath);
|
|
102267
103502
|
if (pluginJson === null) {
|
|
102268
103503
|
output2.write(` \u26A0 plugin.json not found at ${entry.source}/plugin.json \u2014 skipping`);
|
|
@@ -102311,7 +103546,7 @@ async function bumpVersion(ctx, config4, version3, options, output2) {
|
|
|
102311
103546
|
if (await npmViewVersion(ctx.repoRoot, config4.packageName, version3)) {
|
|
102312
103547
|
throw new Error(`${config4.packageName}@${version3} is already published on npm. Use a new version.`);
|
|
102313
103548
|
}
|
|
102314
|
-
const manifestPath =
|
|
103549
|
+
const manifestPath = join29(ctx.repoRoot, config4.packageDir, "package.json");
|
|
102315
103550
|
const manifest = await readJson(manifestPath);
|
|
102316
103551
|
if (manifest === null)
|
|
102317
103552
|
throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
|
|
@@ -102328,7 +103563,7 @@ async function bumpVersion(ctx, config4, version3, options, output2) {
|
|
|
102328
103563
|
}
|
|
102329
103564
|
staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
|
|
102330
103565
|
await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
|
|
102331
|
-
const lockPath =
|
|
103566
|
+
const lockPath = join29(ctx.repoRoot, "bun.lock");
|
|
102332
103567
|
if (existsSync10(lockPath) && Bun.file(lockPath).size > 0)
|
|
102333
103568
|
staged.push("bun.lock");
|
|
102334
103569
|
await git2(ctx.repoRoot, ["add", ...staged]);
|
|
@@ -102384,7 +103619,7 @@ async function bumpAll(ctx, version3, options, output2) {
|
|
|
102384
103619
|
}
|
|
102385
103620
|
const staged = [];
|
|
102386
103621
|
for (const config4 of configs) {
|
|
102387
|
-
const manifestPath =
|
|
103622
|
+
const manifestPath = join29(ctx.repoRoot, config4.packageDir, "package.json");
|
|
102388
103623
|
const manifest = await readJson(manifestPath);
|
|
102389
103624
|
if (manifest === null) {
|
|
102390
103625
|
throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
|
|
@@ -102403,7 +103638,7 @@ async function bumpAll(ctx, version3, options, output2) {
|
|
|
102403
103638
|
staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
|
|
102404
103639
|
}
|
|
102405
103640
|
await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
|
|
102406
|
-
const lockPath =
|
|
103641
|
+
const lockPath = join29(ctx.repoRoot, "bun.lock");
|
|
102407
103642
|
if (existsSync10(lockPath) && Bun.file(lockPath).size > 0)
|
|
102408
103643
|
staged.push("bun.lock");
|
|
102409
103644
|
await git2(ctx.repoRoot, ["add", ...staged]);
|
|
@@ -103064,7 +104299,7 @@ import { createRequire } from "module";
|
|
|
103064
104299
|
var CLI_CONFIG = {
|
|
103065
104300
|
binaryName: "spur",
|
|
103066
104301
|
binaryLabel: "spur",
|
|
103067
|
-
binaryVersion: "0.3.
|
|
104302
|
+
binaryVersion: "0.3.75",
|
|
103068
104303
|
configDir: ".spur",
|
|
103069
104304
|
configFile: ".spur/config.yaml",
|
|
103070
104305
|
databaseFile: ".spur/spur.db"
|
|
@@ -103449,7 +104684,7 @@ init_loader();
|
|
|
103449
104684
|
init_src2();
|
|
103450
104685
|
init_dist10();
|
|
103451
104686
|
import { homedir as homedir9 } from "os";
|
|
103452
|
-
import { join as
|
|
104687
|
+
import { join as join30, resolve as resolve16 } from "path";
|
|
103453
104688
|
|
|
103454
104689
|
// src/config/scaffold-manifest.ts
|
|
103455
104690
|
var SCAFFOLD_MANIFEST = [
|
|
@@ -103478,8 +104713,8 @@ var SCAFFOLD_MANIFEST = [
|
|
|
103478
104713
|
];
|
|
103479
104714
|
|
|
103480
104715
|
// src/commands/init.ts
|
|
103481
|
-
var GLOBAL_CONFIG_DIR =
|
|
103482
|
-
var GLOBAL_RULES_DIR2 =
|
|
104716
|
+
var GLOBAL_CONFIG_DIR = join30(".config", "spur");
|
|
104717
|
+
var GLOBAL_RULES_DIR2 = join30(GLOBAL_CONFIG_DIR, "rules");
|
|
103483
104718
|
var GLOBAL_CONFIG_EXAMPLE = BUNDLED_GLOBAL_CONFIG;
|
|
103484
104719
|
var GLOBAL_CONFIG_FILE2 = "config.yaml";
|
|
103485
104720
|
var INDEXED_CONTEXT_MARKER = "## Indexed context";
|
|
@@ -103510,7 +104745,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
|
|
|
103510
104745
|
`;
|
|
103511
104746
|
function globalRulesRoot(context4) {
|
|
103512
104747
|
const override = context4.env.SPUR_GLOBAL_RULES_DIR;
|
|
103513
|
-
return override !== undefined && override.length > 0 ?
|
|
104748
|
+
return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) : join30(homedir9(), GLOBAL_RULES_DIR2);
|
|
103514
104749
|
}
|
|
103515
104750
|
async function writeIfNew(context4, path9, content, force, result) {
|
|
103516
104751
|
if (!force && await context4.fs.exists(path9)) {
|
|
@@ -103527,18 +104762,18 @@ async function seedGlobalRules(context4) {
|
|
|
103527
104762
|
const target = globalRulesRoot(context4);
|
|
103528
104763
|
let written = 0;
|
|
103529
104764
|
for (const relPath of await listBundledRuleFiles()) {
|
|
103530
|
-
const destination =
|
|
104765
|
+
const destination = join30(target, relPath);
|
|
103531
104766
|
if (await context4.fs.exists(destination))
|
|
103532
104767
|
continue;
|
|
103533
|
-
await context4.fs.ensureDir(
|
|
103534
|
-
await context4.fs.writeFile(destination, await context4.fs.readFile(
|
|
104768
|
+
await context4.fs.ensureDir(join30(target, ...relPath.split("/").slice(0, -1)));
|
|
104769
|
+
await context4.fs.writeFile(destination, await context4.fs.readFile(join30(source, relPath)));
|
|
103535
104770
|
written += 1;
|
|
103536
104771
|
}
|
|
103537
104772
|
return written;
|
|
103538
104773
|
}
|
|
103539
104774
|
function resolveGlobalConfigDir(context4) {
|
|
103540
104775
|
const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
|
|
103541
|
-
return globalOverride !== undefined && globalOverride.length > 0 ?
|
|
104776
|
+
return globalOverride !== undefined && globalOverride.length > 0 ? resolve16(context4.cwd, globalOverride) : join30(homedir9(), GLOBAL_CONFIG_DIR);
|
|
103542
104777
|
}
|
|
103543
104778
|
async function seedGlobalConfig(context4) {
|
|
103544
104779
|
const source = bundledConfigRoot();
|
|
@@ -103549,15 +104784,15 @@ async function seedGlobalConfig(context4) {
|
|
|
103549
104784
|
for (const relPath of listBundledConfigFiles()) {
|
|
103550
104785
|
if (relPath === GLOBAL_CONFIG_EXAMPLE)
|
|
103551
104786
|
continue;
|
|
103552
|
-
const destination =
|
|
104787
|
+
const destination = join30(target, relPath);
|
|
103553
104788
|
if (await context4.fs.exists(destination))
|
|
103554
104789
|
continue;
|
|
103555
|
-
await context4.fs.ensureDir(
|
|
103556
|
-
await context4.fs.writeFile(destination, await context4.fs.readFile(
|
|
104790
|
+
await context4.fs.ensureDir(join30(target, ...relPath.split("/").slice(0, -1)));
|
|
104791
|
+
await context4.fs.writeFile(destination, await context4.fs.readFile(join30(source, relPath)));
|
|
103557
104792
|
written += 1;
|
|
103558
104793
|
}
|
|
103559
|
-
const examplePath =
|
|
103560
|
-
const globalConfigPath =
|
|
104794
|
+
const examplePath = join30(source, GLOBAL_CONFIG_EXAMPLE);
|
|
104795
|
+
const globalConfigPath = join30(target, GLOBAL_CONFIG_FILE2);
|
|
103561
104796
|
if (await context4.fs.exists(examplePath) && !await context4.fs.exists(globalConfigPath)) {
|
|
103562
104797
|
await context4.fs.ensureDir(target);
|
|
103563
104798
|
await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
|
|
@@ -103572,10 +104807,10 @@ function registerInitCommand(program2, context4, options = {}) {
|
|
|
103572
104807
|
const minimal = options2.minimal === true;
|
|
103573
104808
|
const adoptGlobal = options2.adoptGlobalConfig === true;
|
|
103574
104809
|
const projectName = options2.name ?? "default";
|
|
103575
|
-
const configPath =
|
|
104810
|
+
const configPath = join30(context4.cwd, CLI_CONFIG.configFile);
|
|
103576
104811
|
const result = { created: [], skipped: [] };
|
|
103577
104812
|
const globalConfigDir = resolveGlobalConfigDir(context4);
|
|
103578
|
-
const globalConfigPath =
|
|
104813
|
+
const globalConfigPath = join30(globalConfigDir, GLOBAL_CONFIG_FILE2);
|
|
103579
104814
|
const globalConfigExists = await context4.fs.exists(globalConfigPath);
|
|
103580
104815
|
let misplacedKeys = [];
|
|
103581
104816
|
if (globalConfigExists) {
|
|
@@ -103588,20 +104823,20 @@ function registerInitCommand(program2, context4, options = {}) {
|
|
|
103588
104823
|
const configRoot = bundledConfigRoot();
|
|
103589
104824
|
if (configRoot !== null && !minimal) {
|
|
103590
104825
|
for (const relPath of listBundledProjectSeedFiles()) {
|
|
103591
|
-
const sourcePath =
|
|
104826
|
+
const sourcePath = join30(configRoot, relPath);
|
|
103592
104827
|
if (!await context4.fs.exists(sourcePath))
|
|
103593
104828
|
continue;
|
|
103594
|
-
const targetPath =
|
|
103595
|
-
await context4.fs.ensureDir(
|
|
104829
|
+
const targetPath = join30(context4.cwd, CLI_CONFIG.configDir, relPath);
|
|
104830
|
+
await context4.fs.ensureDir(join30(targetPath, ".."));
|
|
103596
104831
|
await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), false, result);
|
|
103597
104832
|
}
|
|
103598
104833
|
for (const entry of SCAFFOLD_MANIFEST) {
|
|
103599
|
-
const sourcePath =
|
|
104834
|
+
const sourcePath = join30(configRoot, entry.source);
|
|
103600
104835
|
if (!await context4.fs.exists(sourcePath))
|
|
103601
104836
|
continue;
|
|
103602
|
-
const baseDir = entry.root === true ? context4.cwd :
|
|
103603
|
-
const targetPath =
|
|
103604
|
-
await context4.fs.ensureDir(
|
|
104837
|
+
const baseDir = entry.root === true ? context4.cwd : join30(context4.cwd, CLI_CONFIG.configDir);
|
|
104838
|
+
const targetPath = join30(baseDir, entry.target);
|
|
104839
|
+
await context4.fs.ensureDir(join30(targetPath, ".."));
|
|
103605
104840
|
const entryForce = false;
|
|
103606
104841
|
let body = await context4.fs.readFile(sourcePath);
|
|
103607
104842
|
if (entry.target === "AGENTS.md") {
|
|
@@ -103618,7 +104853,7 @@ function registerInitCommand(program2, context4, options = {}) {
|
|
|
103618
104853
|
const backupPath = `${globalConfigPath}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
103619
104854
|
await context4.fs.writeFile(backupPath, await context4.fs.readFile(globalConfigPath));
|
|
103620
104855
|
const bundledRoot = bundledConfigRoot();
|
|
103621
|
-
const examplePath = bundledRoot !== null ?
|
|
104856
|
+
const examplePath = bundledRoot !== null ? join30(bundledRoot, GLOBAL_CONFIG_EXAMPLE) : null;
|
|
103622
104857
|
if (examplePath !== null && await context4.fs.exists(examplePath)) {
|
|
103623
104858
|
await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
|
|
103624
104859
|
adoptedGlobal = true;
|
|
@@ -103688,13 +104923,13 @@ function registerInitCommand(program2, context4, options = {}) {
|
|
|
103688
104923
|
].join(`
|
|
103689
104924
|
`)}
|
|
103690
104925
|
`;
|
|
103691
|
-
await context4.fs.ensureDir(
|
|
104926
|
+
await context4.fs.ensureDir(join30(context4.cwd, CLI_CONFIG.configDir));
|
|
103692
104927
|
await context4.fs.writeFile(configPath, configYaml);
|
|
103693
104928
|
result.created.push(configPath);
|
|
103694
|
-
const agentsDir =
|
|
104929
|
+
const agentsDir = join30(context4.cwd, CLI_CONFIG.configDir, "agents");
|
|
103695
104930
|
await context4.fs.ensureDir(agentsDir);
|
|
103696
|
-
await writeIfNew(context4,
|
|
103697
|
-
const gitignorePath =
|
|
104931
|
+
await writeIfNew(context4, join30(agentsDir, ".gitkeep"), "", force, result);
|
|
104932
|
+
const gitignorePath = join30(context4.cwd, ".gitignore");
|
|
103698
104933
|
const contextEntry = ".spur/context/";
|
|
103699
104934
|
if (await context4.fs.exists(gitignorePath)) {
|
|
103700
104935
|
const existing = await context4.fs.readFile(gitignorePath);
|
|
@@ -103714,20 +104949,20 @@ ${contextEntry}
|
|
|
103714
104949
|
const configRoot = bundledConfigRoot();
|
|
103715
104950
|
if (configRoot !== null) {
|
|
103716
104951
|
for (const relPath of listBundledProjectSeedFiles()) {
|
|
103717
|
-
const sourcePath =
|
|
104952
|
+
const sourcePath = join30(configRoot, relPath);
|
|
103718
104953
|
if (!await context4.fs.exists(sourcePath))
|
|
103719
104954
|
continue;
|
|
103720
|
-
const targetPath =
|
|
103721
|
-
await context4.fs.ensureDir(
|
|
104955
|
+
const targetPath = join30(context4.cwd, CLI_CONFIG.configDir, relPath);
|
|
104956
|
+
await context4.fs.ensureDir(join30(targetPath, ".."));
|
|
103722
104957
|
await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), force, result);
|
|
103723
104958
|
}
|
|
103724
104959
|
for (const entry of SCAFFOLD_MANIFEST) {
|
|
103725
|
-
const sourcePath =
|
|
104960
|
+
const sourcePath = join30(configRoot, entry.source);
|
|
103726
104961
|
if (!await context4.fs.exists(sourcePath))
|
|
103727
104962
|
continue;
|
|
103728
|
-
const baseDir = entry.root === true ? context4.cwd :
|
|
103729
|
-
const targetPath =
|
|
103730
|
-
await context4.fs.ensureDir(
|
|
104963
|
+
const baseDir = entry.root === true ? context4.cwd : join30(context4.cwd, CLI_CONFIG.configDir);
|
|
104964
|
+
const targetPath = join30(baseDir, entry.target);
|
|
104965
|
+
await context4.fs.ensureDir(join30(targetPath, ".."));
|
|
103731
104966
|
const entryForce = entry.preserve === true ? false : force;
|
|
103732
104967
|
let body = await context4.fs.readFile(sourcePath);
|
|
103733
104968
|
if (entry.target === "AGENTS.md") {
|
|
@@ -103740,7 +104975,7 @@ ${contextEntry}
|
|
|
103740
104975
|
}
|
|
103741
104976
|
}
|
|
103742
104977
|
}
|
|
103743
|
-
const agentsMdPath =
|
|
104978
|
+
const agentsMdPath = join30(context4.cwd, "AGENTS.md");
|
|
103744
104979
|
if (await context4.fs.exists(agentsMdPath)) {
|
|
103745
104980
|
const existing = await context4.fs.readFile(agentsMdPath);
|
|
103746
104981
|
if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
|
|
@@ -103781,11 +105016,11 @@ ${contextEntry}
|
|
|
103781
105016
|
// src/commands/maintain.ts
|
|
103782
105017
|
init_src();
|
|
103783
105018
|
init_src2();
|
|
103784
|
-
import { join as
|
|
105019
|
+
import { join as join31 } from "path";
|
|
103785
105020
|
function registerMaintainCommand(program2, context4, options = {}) {
|
|
103786
105021
|
program2.command("maintain", { hidden: options.hidden === true }).summary("run database maintenance (optimize, WAL checkpoint truncation, optional VACUUM)").description("Run database maintenance: PRAGMA optimize, WAL checkpoint truncation; optionally VACUUM compaction.").option("--vacuum", "Run VACUUM defragmentation and page compaction", false).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
|
|
103787
105022
|
const db2 = await context4.getDb();
|
|
103788
|
-
const dbPath = context4.env.DATABASE_URL ??
|
|
105023
|
+
const dbPath = context4.env.DATABASE_URL ?? join31(context4.cwd, DEFAULT_DATABASE_URL);
|
|
103789
105024
|
const result = await maintainDatabase(db2, {
|
|
103790
105025
|
vacuum: options2.vacuum === true,
|
|
103791
105026
|
dbPath: dbPath.startsWith(":memory:") ? undefined : dbPath
|
|
@@ -104084,15 +105319,15 @@ async function runMessageWatch(svc, output2, options, runtime = {}) {
|
|
|
104084
105319
|
}
|
|
104085
105320
|
}
|
|
104086
105321
|
function defaultSleep(ms, signal) {
|
|
104087
|
-
return new Promise((
|
|
105322
|
+
return new Promise((resolve17) => {
|
|
104088
105323
|
if (signal?.aborted) {
|
|
104089
|
-
|
|
105324
|
+
resolve17();
|
|
104090
105325
|
return;
|
|
104091
105326
|
}
|
|
104092
|
-
const timer = setTimeout(
|
|
105327
|
+
const timer = setTimeout(resolve17, ms);
|
|
104093
105328
|
signal?.addEventListener("abort", () => {
|
|
104094
105329
|
clearTimeout(timer);
|
|
104095
|
-
|
|
105330
|
+
resolve17();
|
|
104096
105331
|
}, { once: true });
|
|
104097
105332
|
});
|
|
104098
105333
|
}
|
|
@@ -104119,15 +105354,15 @@ function parseTimeout2(raw) {
|
|
|
104119
105354
|
return n3;
|
|
104120
105355
|
}
|
|
104121
105356
|
function sendSleep(ms, signal) {
|
|
104122
|
-
return new Promise((
|
|
105357
|
+
return new Promise((resolve17) => {
|
|
104123
105358
|
if (signal?.aborted) {
|
|
104124
|
-
|
|
105359
|
+
resolve17();
|
|
104125
105360
|
return;
|
|
104126
105361
|
}
|
|
104127
|
-
const timer = setTimeout(
|
|
105362
|
+
const timer = setTimeout(resolve17, ms);
|
|
104128
105363
|
signal?.addEventListener("abort", () => {
|
|
104129
105364
|
clearTimeout(timer);
|
|
104130
|
-
|
|
105365
|
+
resolve17();
|
|
104131
105366
|
}, { once: true });
|
|
104132
105367
|
});
|
|
104133
105368
|
}
|
|
@@ -104185,10 +105420,10 @@ async function waitForPendingDrain(agentService, teamService, pin, timeoutMs, si
|
|
|
104185
105420
|
|
|
104186
105421
|
// src/commands/migrate.ts
|
|
104187
105422
|
init_src2();
|
|
104188
|
-
import { join as
|
|
105423
|
+
import { join as join32 } from "path";
|
|
104189
105424
|
function registerMigrateCommand(program2, context4, options = {}) {
|
|
104190
105425
|
program2.command("migrate", { hidden: options.hidden === true }).summary("apply CLI-owned schema migrations").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
|
|
104191
|
-
const migrations = await loadSqlMigrations(
|
|
105426
|
+
const migrations = await loadSqlMigrations(join32(context4.cwd, "drizzle")).catch(() => {
|
|
104192
105427
|
return;
|
|
104193
105428
|
});
|
|
104194
105429
|
const applied = await applyCliMigrations(await context4.getDb(), migrations);
|
|
@@ -104200,12 +105435,12 @@ function registerMigrateCommand(program2, context4, options = {}) {
|
|
|
104200
105435
|
// src/commands/projects.ts
|
|
104201
105436
|
init_dist5();
|
|
104202
105437
|
await init_src3();
|
|
104203
|
-
import { basename as basename10, resolve as
|
|
105438
|
+
import { basename as basename10, resolve as resolve17 } from "path";
|
|
104204
105439
|
function registerProjectsCommand(program2, context4) {
|
|
104205
105440
|
const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
|
|
104206
105441
|
projectsCmd.command("add").argument("<path>", "Project root directory path").option(...SHARED_OPTIONS.nameProjectDisplay).option(...SHARED_OPTIONS.jsonProjectsResponse).option(...SHARED_OPTIONS.jsonEnvelope).action(async (pathArg, options) => {
|
|
104207
105442
|
try {
|
|
104208
|
-
const absolutePath =
|
|
105443
|
+
const absolutePath = resolve17(context4.cwd, pathArg);
|
|
104209
105444
|
if (!await context4.fs.exists(absolutePath)) {
|
|
104210
105445
|
throw new Error(`Directory does not exist: ${absolutePath}`);
|
|
104211
105446
|
}
|
|
@@ -104627,7 +105862,7 @@ function formatTraceDetail(detail) {
|
|
|
104627
105862
|
|
|
104628
105863
|
// src/commands/serve.ts
|
|
104629
105864
|
init_src();
|
|
104630
|
-
import { join as
|
|
105865
|
+
import { join as join36 } from "path";
|
|
104631
105866
|
|
|
104632
105867
|
// ../server/src/index.ts
|
|
104633
105868
|
init_src();
|
|
@@ -104637,11 +105872,11 @@ init_src();
|
|
|
104637
105872
|
init_loader();
|
|
104638
105873
|
init_src2();
|
|
104639
105874
|
await init_src3();
|
|
104640
|
-
import { basename as basename12, dirname as
|
|
105875
|
+
import { basename as basename12, dirname as dirname21, isAbsolute as isAbsolute8, join as join35 } from "path";
|
|
104641
105876
|
init_dist5();
|
|
104642
105877
|
|
|
104643
105878
|
// ../server/src/bootstrap.ts
|
|
104644
|
-
import { join as
|
|
105879
|
+
import { join as join33 } from "path";
|
|
104645
105880
|
|
|
104646
105881
|
// ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
|
|
104647
105882
|
var guard = (func, shouldGuard) => {
|
|
@@ -104914,8 +106149,8 @@ class EventPublisher {
|
|
|
104914
106149
|
if (bufferedEvents.length > 0) {
|
|
104915
106150
|
return { done: false, value: bufferedEvents.shift() };
|
|
104916
106151
|
}
|
|
104917
|
-
return new Promise((
|
|
104918
|
-
pullResolvers.push([
|
|
106152
|
+
return new Promise((resolve18, reject) => {
|
|
106153
|
+
pullResolvers.push([resolve18, reject]);
|
|
104919
106154
|
});
|
|
104920
106155
|
}, async () => {
|
|
104921
106156
|
unsubscribe();
|
|
@@ -111108,6 +112343,93 @@ var taskContract = {
|
|
|
111108
112343
|
tags: ["task"]
|
|
111109
112344
|
}).input(taskActionInputSchema).output(taskActionResponseSchema)
|
|
111110
112345
|
};
|
|
112346
|
+
// ../../packages/contracts/src/observability.ts
|
|
112347
|
+
init_zod();
|
|
112348
|
+
var observabilitySummaryKpisSchema = exports_external.object({
|
|
112349
|
+
totalEvents: exports_external.number(),
|
|
112350
|
+
activeJobs: exports_external.number(),
|
|
112351
|
+
completedJobs: exports_external.number(),
|
|
112352
|
+
failedJobs: exports_external.number(),
|
|
112353
|
+
successRatePct: exports_external.number(),
|
|
112354
|
+
errorEventCount: exports_external.number(),
|
|
112355
|
+
warningEventCount: exports_external.number()
|
|
112356
|
+
});
|
|
112357
|
+
var observabilityVolumeBucketSchema = exports_external.object({
|
|
112358
|
+
timestamp: exports_external.string(),
|
|
112359
|
+
total: exports_external.number(),
|
|
112360
|
+
byPrefix: exports_external.record(exports_external.string(), exports_external.number()),
|
|
112361
|
+
bySeverity: exports_external.object({
|
|
112362
|
+
info: exports_external.number(),
|
|
112363
|
+
warning: exports_external.number(),
|
|
112364
|
+
error: exports_external.number(),
|
|
112365
|
+
unknown: exports_external.number()
|
|
112366
|
+
})
|
|
112367
|
+
});
|
|
112368
|
+
var observabilityTopEventTypeSchema = exports_external.object({
|
|
112369
|
+
name: exports_external.string(),
|
|
112370
|
+
prefix: exports_external.string(),
|
|
112371
|
+
count: exports_external.number(),
|
|
112372
|
+
latestAt: exports_external.string()
|
|
112373
|
+
});
|
|
112374
|
+
var observabilityRecentErrorSchema = exports_external.object({
|
|
112375
|
+
id: exports_external.string(),
|
|
112376
|
+
source: exports_external.enum(["event", "job"]),
|
|
112377
|
+
name: exports_external.string(),
|
|
112378
|
+
occurredAt: exports_external.string(),
|
|
112379
|
+
message: exports_external.string(),
|
|
112380
|
+
refId: exports_external.string().optional()
|
|
112381
|
+
});
|
|
112382
|
+
var observabilitySummaryResponseSchema = exports_external.object({
|
|
112383
|
+
window: exports_external.object({
|
|
112384
|
+
since: exports_external.string(),
|
|
112385
|
+
until: exports_external.string(),
|
|
112386
|
+
range: exports_external.string()
|
|
112387
|
+
}),
|
|
112388
|
+
kpis: observabilitySummaryKpisSchema,
|
|
112389
|
+
eventVolumeBuckets: exports_external.array(observabilityVolumeBucketSchema),
|
|
112390
|
+
topEventTypes: exports_external.array(observabilityTopEventTypeSchema),
|
|
112391
|
+
recentErrors: exports_external.array(observabilityRecentErrorSchema)
|
|
112392
|
+
});
|
|
112393
|
+
var queueJobStatusSchema = exports_external.enum(["pending", "processing", "completed", "failed"]);
|
|
112394
|
+
var queueJobRowSchema = exports_external.object({
|
|
112395
|
+
id: exports_external.string(),
|
|
112396
|
+
type: exports_external.string(),
|
|
112397
|
+
status: queueJobStatusSchema,
|
|
112398
|
+
attempts: exports_external.number(),
|
|
112399
|
+
maxRetries: exports_external.number(),
|
|
112400
|
+
queuedAt: exports_external.string(),
|
|
112401
|
+
startedAt: exports_external.string().nullable(),
|
|
112402
|
+
endedAt: exports_external.string().nullable(),
|
|
112403
|
+
durationMs: exports_external.number().nullable(),
|
|
112404
|
+
lastError: exports_external.string().nullable(),
|
|
112405
|
+
payload: exports_external.record(exports_external.string(), exports_external.unknown()).nullable()
|
|
112406
|
+
});
|
|
112407
|
+
var queueJobStatusCountsSchema = exports_external.object({
|
|
112408
|
+
all: exports_external.number(),
|
|
112409
|
+
pending: exports_external.number(),
|
|
112410
|
+
processing: exports_external.number(),
|
|
112411
|
+
completed: exports_external.number(),
|
|
112412
|
+
failed: exports_external.number()
|
|
112413
|
+
});
|
|
112414
|
+
var queueJobListResponseSchema = exports_external.object({
|
|
112415
|
+
jobs: exports_external.array(queueJobRowSchema),
|
|
112416
|
+
total: exports_external.number(),
|
|
112417
|
+
hasMore: exports_external.boolean(),
|
|
112418
|
+
countsByStatus: queueJobStatusCountsSchema
|
|
112419
|
+
});
|
|
112420
|
+
var schedulerLastStatusSchema = exports_external.enum(["completed", "failed", "processing", "none"]);
|
|
112421
|
+
var schedulerScheduleRowSchema = exports_external.object({
|
|
112422
|
+
name: exports_external.string(),
|
|
112423
|
+
cron: exports_external.string(),
|
|
112424
|
+
cadence: exports_external.string(),
|
|
112425
|
+
nextFireAt: exports_external.string().nullable(),
|
|
112426
|
+
lastFiredAt: exports_external.string().nullable(),
|
|
112427
|
+
lastStatus: schedulerLastStatusSchema,
|
|
112428
|
+
source: exports_external.enum(["builtin", "config"])
|
|
112429
|
+
});
|
|
112430
|
+
var schedulerSchedulesResponseSchema = exports_external.object({
|
|
112431
|
+
schedules: exports_external.array(schedulerScheduleRowSchema)
|
|
112432
|
+
});
|
|
111111
112433
|
|
|
111112
112434
|
// ../../packages/contracts/src/index.ts
|
|
111113
112435
|
var healthResponseSchema = exports_external.object({
|
|
@@ -111374,6 +112696,18 @@ var historyModule = {
|
|
|
111374
112696
|
mount(_app, _ctx) {}
|
|
111375
112697
|
};
|
|
111376
112698
|
|
|
112699
|
+
// ../server/src/modules/jobs/index.ts
|
|
112700
|
+
init_src2();
|
|
112701
|
+
|
|
112702
|
+
// ../server/src/modules/jobs/schedule-registry.ts
|
|
112703
|
+
var registeredSchedules = [];
|
|
112704
|
+
function setRegisteredSchedules(entries) {
|
|
112705
|
+
registeredSchedules = [...entries];
|
|
112706
|
+
}
|
|
112707
|
+
function getRegisteredSchedules() {
|
|
112708
|
+
return [...registeredSchedules];
|
|
112709
|
+
}
|
|
112710
|
+
|
|
111377
112711
|
// ../server/src/modules/jobs/index.ts
|
|
111378
112712
|
var jobsModule = {
|
|
111379
112713
|
name: "jobs",
|
|
@@ -111385,6 +112719,100 @@ var jobsModule = {
|
|
|
111385
112719
|
const stats = await queue.stats();
|
|
111386
112720
|
return c3.json({ stats });
|
|
111387
112721
|
});
|
|
112722
|
+
app.get("/api/jobs", async (c3) => {
|
|
112723
|
+
try {
|
|
112724
|
+
const statusParam = c3.req.query("status");
|
|
112725
|
+
const sinceParam = c3.req.query("since");
|
|
112726
|
+
const limitParam = c3.req.query("limit");
|
|
112727
|
+
const offsetParam = c3.req.query("offset");
|
|
112728
|
+
const validStatuses = ["pending", "processing", "completed", "failed"];
|
|
112729
|
+
let status;
|
|
112730
|
+
if (statusParam !== undefined && statusParam !== "") {
|
|
112731
|
+
if (!validStatuses.includes(statusParam)) {
|
|
112732
|
+
return c3.json({
|
|
112733
|
+
error: `unknown status: "${statusParam}", expected one of: ${validStatuses.join(", ")}`,
|
|
112734
|
+
code: "VALIDATION_FAILED"
|
|
112735
|
+
}, 400);
|
|
112736
|
+
}
|
|
112737
|
+
status = statusParam;
|
|
112738
|
+
}
|
|
112739
|
+
let limit = 100;
|
|
112740
|
+
if (limitParam !== undefined && limitParam !== "") {
|
|
112741
|
+
const parsedLimit = Number(limitParam);
|
|
112742
|
+
if (!Number.isNaN(parsedLimit)) {
|
|
112743
|
+
limit = Math.min(500, Math.max(1, parsedLimit));
|
|
112744
|
+
}
|
|
112745
|
+
}
|
|
112746
|
+
let offset = 0;
|
|
112747
|
+
if (offsetParam !== undefined && offsetParam !== "") {
|
|
112748
|
+
const parsedOffset = Number(offsetParam);
|
|
112749
|
+
if (!Number.isNaN(parsedOffset)) {
|
|
112750
|
+
offset = Math.max(0, parsedOffset);
|
|
112751
|
+
}
|
|
112752
|
+
}
|
|
112753
|
+
const db2 = await ctx.getDb();
|
|
112754
|
+
const result = await queryQueueJobs(db2, {
|
|
112755
|
+
status,
|
|
112756
|
+
since: sinceParam,
|
|
112757
|
+
limit,
|
|
112758
|
+
offset
|
|
112759
|
+
});
|
|
112760
|
+
return c3.json(result);
|
|
112761
|
+
} catch (err) {
|
|
112762
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112763
|
+
return c3.json({ error: message }, 500);
|
|
112764
|
+
}
|
|
112765
|
+
});
|
|
112766
|
+
app.get("/api/jobs/schedules", async (c3) => {
|
|
112767
|
+
try {
|
|
112768
|
+
const registrations = getRegisteredSchedules();
|
|
112769
|
+
const db2 = await ctx.getDb();
|
|
112770
|
+
const now2 = Date.now();
|
|
112771
|
+
const schedules = await Promise.all(registrations.map(async (reg) => {
|
|
112772
|
+
const matchingRow = await queryScheduleLastExecution(db2, reg.source, reg.name);
|
|
112773
|
+
const lastFiredAt = matchingRow ? new Date(matchingRow.updated_at).toISOString() : null;
|
|
112774
|
+
let lastStatus = "none";
|
|
112775
|
+
if (matchingRow) {
|
|
112776
|
+
lastStatus = matchingRow.status === "pending" ? "processing" : matchingRow.status;
|
|
112777
|
+
}
|
|
112778
|
+
const intervalMs = Number(reg.schedule);
|
|
112779
|
+
const isInterval = !Number.isNaN(intervalMs) && intervalMs > 0 && /^\d+$/.test(reg.schedule);
|
|
112780
|
+
let nextFireAt = null;
|
|
112781
|
+
let cadence = reg.schedule;
|
|
112782
|
+
if (isInterval) {
|
|
112783
|
+
const elapsed = Math.max(0, now2 - reg.registeredAt);
|
|
112784
|
+
const steps = Math.ceil(elapsed / intervalMs);
|
|
112785
|
+
const nextTime = reg.registeredAt + (steps === 0 ? 1 : steps) * intervalMs;
|
|
112786
|
+
const resolvedNextTime = nextTime <= now2 ? nextTime + intervalMs : nextTime;
|
|
112787
|
+
nextFireAt = new Date(resolvedNextTime).toISOString();
|
|
112788
|
+
if (intervalMs >= 60000 && intervalMs % 60000 === 0) {
|
|
112789
|
+
cadence = `every ${intervalMs / 60000} minutes`;
|
|
112790
|
+
} else if (intervalMs >= 1000 && intervalMs % 1000 === 0) {
|
|
112791
|
+
cadence = `every ${intervalMs / 1000} seconds`;
|
|
112792
|
+
} else {
|
|
112793
|
+
cadence = `every ${intervalMs} ms`;
|
|
112794
|
+
}
|
|
112795
|
+
} else {
|
|
112796
|
+
nextFireAt = null;
|
|
112797
|
+
cadence = reg.schedule;
|
|
112798
|
+
}
|
|
112799
|
+
const row = {
|
|
112800
|
+
name: reg.name,
|
|
112801
|
+
cron: reg.schedule,
|
|
112802
|
+
cadence,
|
|
112803
|
+
nextFireAt,
|
|
112804
|
+
lastFiredAt,
|
|
112805
|
+
lastStatus,
|
|
112806
|
+
source: reg.source
|
|
112807
|
+
};
|
|
112808
|
+
return row;
|
|
112809
|
+
}));
|
|
112810
|
+
return c3.json({ schedules });
|
|
112811
|
+
} catch (err) {
|
|
112812
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
112813
|
+
return c3.json({ error: message }, 500);
|
|
112814
|
+
}
|
|
112815
|
+
});
|
|
111388
112816
|
}
|
|
111389
112817
|
};
|
|
111390
112818
|
|
|
@@ -111626,6 +113054,96 @@ function handleRoutingSummary(ctx) {
|
|
|
111626
113054
|
}
|
|
111627
113055
|
};
|
|
111628
113056
|
}
|
|
113057
|
+
function handleObservabilitySummary(ctx) {
|
|
113058
|
+
return async (c3) => {
|
|
113059
|
+
try {
|
|
113060
|
+
const sinceParam = c3.req.query("since");
|
|
113061
|
+
const untilParam = c3.req.query("until");
|
|
113062
|
+
const bucketParam = c3.req.query("bucket");
|
|
113063
|
+
let untilMs = Date.now();
|
|
113064
|
+
if (untilParam !== undefined && untilParam !== "") {
|
|
113065
|
+
untilMs = Date.parse(untilParam);
|
|
113066
|
+
if (Number.isNaN(untilMs)) {
|
|
113067
|
+
return c3.json({
|
|
113068
|
+
error: `malformed until: "${untilParam}" is not a valid ISO timestamp`,
|
|
113069
|
+
code: "MALFORMED_TIMESTAMP"
|
|
113070
|
+
}, 400);
|
|
113071
|
+
}
|
|
113072
|
+
}
|
|
113073
|
+
let sinceMs = untilMs - 14400000;
|
|
113074
|
+
if (sinceParam !== undefined && sinceParam !== "") {
|
|
113075
|
+
sinceMs = Date.parse(sinceParam);
|
|
113076
|
+
if (Number.isNaN(sinceMs)) {
|
|
113077
|
+
return c3.json({
|
|
113078
|
+
error: `malformed since: "${sinceParam}" is not a valid ISO timestamp`,
|
|
113079
|
+
code: "MALFORMED_TIMESTAMP"
|
|
113080
|
+
}, 400);
|
|
113081
|
+
}
|
|
113082
|
+
}
|
|
113083
|
+
if (untilMs < sinceMs) {
|
|
113084
|
+
return c3.json({
|
|
113085
|
+
error: "until timestamp must not precede since timestamp",
|
|
113086
|
+
code: "MALFORMED_RANGE"
|
|
113087
|
+
}, 400);
|
|
113088
|
+
}
|
|
113089
|
+
let bucketMs;
|
|
113090
|
+
if (bucketParam !== undefined && bucketParam !== "") {
|
|
113091
|
+
const parsed = Number(bucketParam);
|
|
113092
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
113093
|
+
bucketMs = parsed;
|
|
113094
|
+
}
|
|
113095
|
+
}
|
|
113096
|
+
const since = new Date(sinceMs).toISOString();
|
|
113097
|
+
const until = new Date(untilMs).toISOString();
|
|
113098
|
+
const dao2 = await ctx.systemEventDao();
|
|
113099
|
+
const db2 = await ctx.getDb();
|
|
113100
|
+
const [eventSummary, jobKpis] = await Promise.all([
|
|
113101
|
+
dao2.eventSummary({ since, until, bucketMs }),
|
|
113102
|
+
queueJobKpis(db2, sinceMs, untilMs)
|
|
113103
|
+
]);
|
|
113104
|
+
const combinedErrors = [
|
|
113105
|
+
...eventSummary.recentErrors.map((e) => ({
|
|
113106
|
+
id: e.id,
|
|
113107
|
+
source: "event",
|
|
113108
|
+
name: e.name,
|
|
113109
|
+
occurredAt: e.occurredAt,
|
|
113110
|
+
message: e.message,
|
|
113111
|
+
...e.refId ? { refId: e.refId } : {}
|
|
113112
|
+
})),
|
|
113113
|
+
...jobKpis.recentJobErrors.map((j) => ({
|
|
113114
|
+
id: j.id,
|
|
113115
|
+
source: "job",
|
|
113116
|
+
name: j.name,
|
|
113117
|
+
occurredAt: j.occurredAt,
|
|
113118
|
+
message: j.message
|
|
113119
|
+
}))
|
|
113120
|
+
].sort((a2, b) => Date.parse(b.occurredAt) - Date.parse(a2.occurredAt)).slice(0, 10);
|
|
113121
|
+
const payload = {
|
|
113122
|
+
window: {
|
|
113123
|
+
since,
|
|
113124
|
+
until,
|
|
113125
|
+
range: c3.req.query("range") ?? "custom"
|
|
113126
|
+
},
|
|
113127
|
+
kpis: {
|
|
113128
|
+
totalEvents: eventSummary.totalEvents,
|
|
113129
|
+
activeJobs: jobKpis.activeJobs,
|
|
113130
|
+
completedJobs: jobKpis.completedJobs,
|
|
113131
|
+
failedJobs: jobKpis.failedJobs,
|
|
113132
|
+
successRatePct: jobKpis.successRatePct,
|
|
113133
|
+
errorEventCount: eventSummary.errorEventCount,
|
|
113134
|
+
warningEventCount: eventSummary.warningEventCount
|
|
113135
|
+
},
|
|
113136
|
+
eventVolumeBuckets: eventSummary.eventVolumeBuckets,
|
|
113137
|
+
topEventTypes: eventSummary.topEventTypes,
|
|
113138
|
+
recentErrors: combinedErrors
|
|
113139
|
+
};
|
|
113140
|
+
return c3.json(payload);
|
|
113141
|
+
} catch (err) {
|
|
113142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113143
|
+
return c3.json({ error: message }, 500);
|
|
113144
|
+
}
|
|
113145
|
+
};
|
|
113146
|
+
}
|
|
111629
113147
|
var observabilityModule = {
|
|
111630
113148
|
name: "observability",
|
|
111631
113149
|
mount(app, ctx) {
|
|
@@ -111635,6 +113153,7 @@ var observabilityModule = {
|
|
|
111635
113153
|
app.get("/api/observability/tool-use", handleToolUseGet(ctx));
|
|
111636
113154
|
app.get("/api/observability/tool-use/stream", handleToolUseStream(ctx));
|
|
111637
113155
|
app.get("/api/observability/routing-summary", handleRoutingSummary(ctx));
|
|
113156
|
+
app.get("/api/observability/summary", handleObservabilitySummary(ctx));
|
|
111638
113157
|
}
|
|
111639
113158
|
};
|
|
111640
113159
|
|
|
@@ -114062,7 +115581,7 @@ function createApp(appRt, opts) {
|
|
|
114062
115581
|
app.use("*", async (c3, next) => {
|
|
114063
115582
|
const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
|
|
114064
115583
|
try {
|
|
114065
|
-
const file2 = Bun.file(
|
|
115584
|
+
const file2 = Bun.file(join33(webDistPath, pathname));
|
|
114066
115585
|
if (await file2.exists()) {
|
|
114067
115586
|
const headers = new Headers({ "content-type": file2.type });
|
|
114068
115587
|
return new Response(file2.stream(), { headers });
|
|
@@ -114075,7 +115594,7 @@ function createApp(appRt, opts) {
|
|
|
114075
115594
|
return c3.json({ error: "Not Found" }, 404);
|
|
114076
115595
|
}
|
|
114077
115596
|
try {
|
|
114078
|
-
const indexFile = Bun.file(
|
|
115597
|
+
const indexFile = Bun.file(join33(webDistPath, "index.html"));
|
|
114079
115598
|
if (await indexFile.exists()) {
|
|
114080
115599
|
return new Response(indexFile.stream(), {
|
|
114081
115600
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -114096,7 +115615,7 @@ init_src();
|
|
|
114096
115615
|
init_src2();
|
|
114097
115616
|
init_dist5();
|
|
114098
115617
|
await init_src3();
|
|
114099
|
-
import { dirname as
|
|
115618
|
+
import { dirname as dirname20, join as join34 } from "path";
|
|
114100
115619
|
var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
|
|
114101
115620
|
|
|
114102
115621
|
class LazyPlanningEventEmitter {
|
|
@@ -114132,7 +115651,7 @@ class NotConfiguredError extends Error {
|
|
|
114132
115651
|
function createServerContext(appRt, options) {
|
|
114133
115652
|
const cwd = options.cwd;
|
|
114134
115653
|
const fs3 = options.fs;
|
|
114135
|
-
const dbUrl = options.dbUrl ??
|
|
115654
|
+
const dbUrl = options.dbUrl ?? join34(cwd, DEFAULT_DATABASE_URL);
|
|
114136
115655
|
const eventsBus = options.eventsBus ?? appRt.events;
|
|
114137
115656
|
const jobQueueEnabled = options.jobQueueEnabled ?? false;
|
|
114138
115657
|
const eventProjectContext = systemEventProjectContext(cwd);
|
|
@@ -114170,7 +115689,7 @@ function createServerContext(appRt, options) {
|
|
|
114170
115689
|
if (!dbPromise) {
|
|
114171
115690
|
const created = (async () => {
|
|
114172
115691
|
if (dbUrl !== IN_MEMORY_DATABASE_URL) {
|
|
114173
|
-
await fs3.ensureDir(
|
|
115692
|
+
await fs3.ensureDir(dirname20(dbUrl));
|
|
114174
115693
|
}
|
|
114175
115694
|
return createMigratedDbViaRuntime({ url: dbUrl });
|
|
114176
115695
|
})();
|
|
@@ -114418,7 +115937,7 @@ async function loadServerSectionMatrix() {
|
|
|
114418
115937
|
}
|
|
114419
115938
|
const root = bundledConfigRoot();
|
|
114420
115939
|
if (root !== null) {
|
|
114421
|
-
const matrixPath =
|
|
115940
|
+
const matrixPath = join35(root, "tasks", "section-matrix.yaml");
|
|
114422
115941
|
if (await nodeFs.exists(matrixPath)) {
|
|
114423
115942
|
return await loadStructuredSpurConfig(matrixPath, {
|
|
114424
115943
|
validateJsonSchema: false
|
|
@@ -114427,7 +115946,7 @@ async function loadServerSectionMatrix() {
|
|
|
114427
115946
|
}
|
|
114428
115947
|
throw new Error(`no canonical section-matrix found for task creation (F92 R1); tried:
|
|
114429
115948
|
` + ` - ${localPath}
|
|
114430
|
-
` + (root !== null ? ` - ${
|
|
115949
|
+
` + (root !== null ? ` - ${join35(root, "tasks", "section-matrix.yaml")}
|
|
114431
115950
|
` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
|
|
114432
115951
|
}
|
|
114433
115952
|
var SYSTEM_EVENTS_PRUNE_JOB = "system-events-prune";
|
|
@@ -114446,6 +115965,8 @@ var defaultDeps = {
|
|
|
114446
115965
|
resolveConfigFile
|
|
114447
115966
|
};
|
|
114448
115967
|
function registerSchedulerEntries(scheduler3, ctx, jobs = []) {
|
|
115968
|
+
const registrations = [];
|
|
115969
|
+
const now2 = Date.now();
|
|
114449
115970
|
const register = (cron, name, action) => {
|
|
114450
115971
|
scheduler3.register(cron, async () => {
|
|
114451
115972
|
const startedAt2 = Date.now();
|
|
@@ -114469,17 +115990,48 @@ function registerSchedulerEntries(scheduler3, ctx, jobs = []) {
|
|
|
114469
115990
|
const queue = await ctx.jobQueue();
|
|
114470
115991
|
await queue.enqueue(SYSTEM_EVENTS_PRUNE_JOB, { source: "scheduler" }, { maxRetries: 1 });
|
|
114471
115992
|
});
|
|
115993
|
+
registrations.push({
|
|
115994
|
+
name: SYSTEM_EVENTS_PRUNE_JOB,
|
|
115995
|
+
schedule: SYSTEM_EVENTS_PRUNE_CRON,
|
|
115996
|
+
source: "builtin",
|
|
115997
|
+
registeredAt: now2
|
|
115998
|
+
});
|
|
114472
115999
|
register(SMOKE_CRON, SMOKE_JOB, async () => {
|
|
114473
116000
|
const queue = await ctx.jobQueue();
|
|
114474
116001
|
await queue.enqueue(SMOKE_JOB, { source: "scheduler" }, { maxRetries: 1 });
|
|
114475
116002
|
});
|
|
116003
|
+
registrations.push({
|
|
116004
|
+
name: SMOKE_JOB,
|
|
116005
|
+
schedule: SMOKE_CRON,
|
|
116006
|
+
source: "builtin",
|
|
116007
|
+
registeredAt: now2
|
|
116008
|
+
});
|
|
114476
116009
|
for (const job of jobs) {
|
|
114477
116010
|
const schedule = job.cron ?? String(job.intervalMinutes * 60000);
|
|
114478
116011
|
register(schedule, `${SCHEDULER_CUSTOM_JOB}:${job.name}`, async () => {
|
|
114479
116012
|
const queue = await ctx.jobQueue();
|
|
116013
|
+
const db2 = await ctx.getDb();
|
|
116014
|
+
const active = await findActiveSchedulerCustomJob(db2, job.name);
|
|
116015
|
+
if (active !== undefined) {
|
|
116016
|
+
ctx.eventBus().emit("scheduler.job.executed", {
|
|
116017
|
+
name: `${SCHEDULER_CUSTOM_JOB}:${job.name}`,
|
|
116018
|
+
durationMs: 0,
|
|
116019
|
+
severity: "info",
|
|
116020
|
+
skipped: true,
|
|
116021
|
+
reason: `active job ${active.id} already exists`
|
|
116022
|
+
});
|
|
116023
|
+
return;
|
|
116024
|
+
}
|
|
114480
116025
|
await queue.enqueue(SCHEDULER_CUSTOM_JOB, { name: job.name, command: job.command });
|
|
114481
116026
|
});
|
|
116027
|
+
registrations.push({
|
|
116028
|
+
name: job.name,
|
|
116029
|
+
schedule,
|
|
116030
|
+
source: "config",
|
|
116031
|
+
registeredAt: now2
|
|
116032
|
+
});
|
|
114482
116033
|
}
|
|
116034
|
+
setRegisteredSchedules(registrations);
|
|
114483
116035
|
}
|
|
114484
116036
|
function parseTaskActionJob(payload) {
|
|
114485
116037
|
if (typeof payload !== "object" || payload === null) {
|
|
@@ -114565,15 +116117,15 @@ async function handleFeatureActionJob(ctx, env, payload) {
|
|
|
114565
116117
|
await runFeatureActionJob(ctx, env, payload);
|
|
114566
116118
|
}
|
|
114567
116119
|
async function resolveWebDistPath(configuredPath) {
|
|
114568
|
-
const candidates = configuredPath && configuredPath.trim() !== "" ? [
|
|
114569
|
-
|
|
114570
|
-
|
|
114571
|
-
|
|
114572
|
-
|
|
114573
|
-
|
|
116120
|
+
const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute8(configuredPath) ? configuredPath : join35(process.cwd(), configuredPath)] : [
|
|
116121
|
+
join35(process.cwd(), "dist/web"),
|
|
116122
|
+
join35(import.meta.dir, "web"),
|
|
116123
|
+
join35(dirname21(process.execPath), "web"),
|
|
116124
|
+
join35(dirname21(process.execPath), "../web"),
|
|
116125
|
+
join35(import.meta.dir, "../../../dist/web")
|
|
114574
116126
|
];
|
|
114575
116127
|
for (const candidate of candidates) {
|
|
114576
|
-
if (await Bun.file(
|
|
116128
|
+
if (await Bun.file(join35(candidate, "index.html")).exists()) {
|
|
114577
116129
|
return candidate;
|
|
114578
116130
|
}
|
|
114579
116131
|
}
|
|
@@ -114589,7 +116141,7 @@ async function startServer(options, deps = defaultDeps) {
|
|
|
114589
116141
|
async start(appRt) {
|
|
114590
116142
|
const fs3 = deps.createNodeFileSystem(process.cwd());
|
|
114591
116143
|
if (options.dbUrl && options.dbUrl !== IN_MEMORY_DATABASE_URL) {
|
|
114592
|
-
await fs3.ensureDir(
|
|
116144
|
+
await fs3.ensureDir(dirname21(options.dbUrl));
|
|
114593
116145
|
}
|
|
114594
116146
|
const scheduler3 = appRt.config.scheduler.enabled ? appRt.scheduler : undefined;
|
|
114595
116147
|
const webDistPath = await resolveWebDistPath(options.webDistPath);
|
|
@@ -114659,6 +116211,12 @@ async function startServer(options, deps = defaultDeps) {
|
|
|
114659
116211
|
executor: childExecutor
|
|
114660
116212
|
}, job));
|
|
114661
116213
|
registry2.register(SCHEDULER_CUSTOM_JOB, (job) => handleSchedulerCustomJob({ cwd: ctx.cwd, executor: childExecutor }, job));
|
|
116214
|
+
const orphanCount = await failOrphanedProcessingJobs(await ctx.getDb(), Date.now());
|
|
116215
|
+
if (orphanCount > 0) {
|
|
116216
|
+
appRt.logger.warn("Swept orphaned processing jobs at startup", {
|
|
116217
|
+
count: orphanCount
|
|
116218
|
+
});
|
|
116219
|
+
}
|
|
114662
116220
|
jobWorker = new JobWorkerService({
|
|
114663
116221
|
consumer: await ctx.queueConsumer(),
|
|
114664
116222
|
registry: registry2
|
|
@@ -114724,11 +116282,11 @@ async function startServer(options, deps = defaultDeps) {
|
|
|
114724
116282
|
if (options.openBrowser) {
|
|
114725
116283
|
await deps.openUrl(webDistPath ? `${url2}/board` : `${url2}/api/health`);
|
|
114726
116284
|
}
|
|
114727
|
-
if (options.keepAlive !== false) {
|
|
114728
|
-
await new Promise(() => {});
|
|
114729
|
-
}
|
|
114730
116285
|
}
|
|
114731
116286
|
});
|
|
116287
|
+
if (options.keepAlive !== false) {
|
|
116288
|
+
await new Promise(() => {});
|
|
116289
|
+
}
|
|
114732
116290
|
}
|
|
114733
116291
|
// ../server/src/worker-app.ts
|
|
114734
116292
|
var startedAt2 = Date.now();
|
|
@@ -114737,7 +116295,7 @@ if (false) {}
|
|
|
114737
116295
|
|
|
114738
116296
|
// src/commands/serve.ts
|
|
114739
116297
|
function resolveServeDbUrl(cwd, env, configuredUrl) {
|
|
114740
|
-
return env.DATABASE_URL === undefined ?
|
|
116298
|
+
return env.DATABASE_URL === undefined ? join36(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
|
|
114741
116299
|
}
|
|
114742
116300
|
function registerServeCommand(program2, context4, options = {}) {
|
|
114743
116301
|
program2.command("serve", { hidden: options.hidden === true }).summary("start the Spur web server (local fallback)").option(...SHARED_OPTIONS.portServe, parseInt).option("--host <addr>", "Bind address (env: HOST, default: localhost)").option("--no-open", "Skip opening the browser").option(...SHARED_OPTIONS.cwdServe, context4.cwd).option(...SHARED_OPTIONS.jsonServePortUrl).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options2) => {
|
|
@@ -114777,7 +116335,7 @@ function registerServeCommand(program2, context4, options = {}) {
|
|
|
114777
116335
|
}
|
|
114778
116336
|
|
|
114779
116337
|
// src/commands/status.ts
|
|
114780
|
-
import { join as
|
|
116338
|
+
import { join as join37 } from "path";
|
|
114781
116339
|
|
|
114782
116340
|
// src/errors.ts
|
|
114783
116341
|
class CommandError extends Error {
|
|
@@ -114848,8 +116406,8 @@ function registerStatusCommand(program2, context4, options = {}) {
|
|
|
114848
116406
|
}
|
|
114849
116407
|
async function runStatusCore(path9, options, context4) {
|
|
114850
116408
|
const [packageJsonExists, spurConfigExists, git3, agentSpecs] = await Promise.all([
|
|
114851
|
-
context4.fs.exists(
|
|
114852
|
-
context4.fs.exists(
|
|
116409
|
+
context4.fs.exists(join37(context4.cwd, "package.json")),
|
|
116410
|
+
context4.fs.exists(join37(context4.cwd, ".spur", "config.yaml")),
|
|
114853
116411
|
gitContext(context4.cwd),
|
|
114854
116412
|
listAgentSpecIds(context4)
|
|
114855
116413
|
]);
|
|
@@ -114877,14 +116435,14 @@ async function runStatusCore(path9, options, context4) {
|
|
|
114877
116435
|
return status.ok ? 0 : 1;
|
|
114878
116436
|
}
|
|
114879
116437
|
async function listAgentSpecIds(context4) {
|
|
114880
|
-
const dir =
|
|
116438
|
+
const dir = join37(context4.cwd, ".spur", "agents");
|
|
114881
116439
|
if (!await context4.fs.exists(dir))
|
|
114882
116440
|
return [];
|
|
114883
116441
|
const entries = await context4.fs.readDir(dir);
|
|
114884
116442
|
return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
|
|
114885
116443
|
}
|
|
114886
116444
|
async function readTargetStatus(context4, targetPath) {
|
|
114887
|
-
const resolved =
|
|
116445
|
+
const resolved = join37(context4.cwd, targetPath);
|
|
114888
116446
|
const stat = await context4.fs.stat(resolved);
|
|
114889
116447
|
if (stat === null)
|
|
114890
116448
|
throw new CommandError(`status failed: path does not exist at ${resolved}`);
|
|
@@ -114897,7 +116455,9 @@ init_src2();
|
|
|
114897
116455
|
init_dist5();
|
|
114898
116456
|
await init_src3();
|
|
114899
116457
|
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
114900
|
-
import {
|
|
116458
|
+
import { rm, writeFile } from "fs/promises";
|
|
116459
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
116460
|
+
import { dirname as dirname22, join as join38 } from "path";
|
|
114901
116461
|
// schemas/section-matrix.schema.json
|
|
114902
116462
|
var section_matrix_schema_default = {
|
|
114903
116463
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -115832,16 +117392,33 @@ function renderMigrationReport(report, dryRun, corpusDir) {
|
|
|
115832
117392
|
return lines.join(`
|
|
115833
117393
|
`);
|
|
115834
117394
|
}
|
|
117395
|
+
function writeCreateJsonError(context4, options, code, message, extra) {
|
|
117396
|
+
const text4 = typeof message === "string" ? message : String(message);
|
|
117397
|
+
if (options.json === true) {
|
|
117398
|
+
const bare = text4.startsWith("Error: ") ? text4.slice("Error: ".length) : text4;
|
|
117399
|
+
context4.output.write(toEnvelopeJson({ ok: false, error: { code, message: bare, ...extra ?? {} } }, {
|
|
117400
|
+
enveloped: options.jsonEnvelope,
|
|
117401
|
+
error: { code: "INTERNAL_ERROR", message: bare, details: { cliCode: code, ...extra ?? {} } }
|
|
117402
|
+
}));
|
|
117403
|
+
} else {
|
|
117404
|
+
context4.output.error(text4.startsWith("Error: ") ? text4.slice("Error: ".length) : text4);
|
|
117405
|
+
}
|
|
117406
|
+
}
|
|
115835
117407
|
function registerTaskCommand(program2, context4) {
|
|
115836
117408
|
const task = program2.command("task").summary("manage tasks");
|
|
115837
|
-
task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(...SHARED_OPTIONS.featureTrace).option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option(...SHARED_OPTIONS.folderTasks).option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (title2, options) => {
|
|
117409
|
+
task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(...SHARED_OPTIONS.featureTrace).option("--parent <wbs>", "Parent WBS for sub-task grouping").option("--template <variant>", `Template variant (${TASK_VARIANTS.join("|")})`).option(...SHARED_OPTIONS.folderTasks).option("--dedupe-within <seconds>", "Override the default dedup window (seconds). Guard is on (300s) by default when --feature is set.", Number).option("--allow-duplicate-name", "Disable the dedup guard entirely (creates anyway)").option("--skip-ready", "Skip ready preparation (backlog capture only, no model dispatch)").option("--agent <selector>", "Agent used for ready preparation (defaults to the configured agent)").option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (title2, options) => {
|
|
115838
117410
|
if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
|
|
115839
|
-
|
|
117411
|
+
writeCreateJsonError(context4, options, "invalid-usage", `Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
|
|
117412
|
+
context4.setExitCode(2);
|
|
117413
|
+
return;
|
|
117414
|
+
}
|
|
117415
|
+
if (options.skipReady === true && options.agent !== undefined) {
|
|
117416
|
+
writeCreateJsonError(context4, options, "invalid-usage", "--agent has no effect with --skip-ready (no preparation runs)");
|
|
115840
117417
|
context4.setExitCode(2);
|
|
115841
117418
|
return;
|
|
115842
117419
|
}
|
|
115843
117420
|
if (options.dedupeWithin !== undefined && (!Number.isInteger(options.dedupeWithin) || options.dedupeWithin <= 0)) {
|
|
115844
|
-
|
|
117421
|
+
writeCreateJsonError(context4, options, "invalid-usage", "--dedupe-within must be a positive integer");
|
|
115845
117422
|
context4.setExitCode(2);
|
|
115846
117423
|
return;
|
|
115847
117424
|
}
|
|
@@ -115854,10 +117431,25 @@ function registerTaskCommand(program2, context4) {
|
|
|
115854
117431
|
template: options.template,
|
|
115855
117432
|
dedupeWithinSec: options.allowDuplicateName ? null : options.dedupeWithin
|
|
115856
117433
|
});
|
|
117434
|
+
let readiness = READY_SKIPPED;
|
|
117435
|
+
if (options.skipReady !== true) {
|
|
117436
|
+
const checkSvc = await makeCheckService(context4);
|
|
117437
|
+
readiness = await prepareCreatedTaskReady({
|
|
117438
|
+
wbs: result.ref.id,
|
|
117439
|
+
tasks: svc,
|
|
117440
|
+
agents: context4.agentService(),
|
|
117441
|
+
checkTask: (filePath, wbs) => checkSvc.check(filePath, wbs, { asStatus: "todo" }),
|
|
117442
|
+
agentSelector: options.agent,
|
|
117443
|
+
cwd: context4.cwd
|
|
117444
|
+
});
|
|
117445
|
+
}
|
|
115857
117446
|
if (options.json) {
|
|
115858
|
-
context4.output.write(toEnvelopeJson({ ...result, wbs: result.ref.id, filePath: result.ref.filePath }, { enveloped: options.jsonEnvelope }));
|
|
117447
|
+
context4.output.write(toEnvelopeJson({ ...result, wbs: result.ref.id, filePath: result.ref.filePath, ...readiness }, { enveloped: options.jsonEnvelope }));
|
|
115859
117448
|
} else {
|
|
115860
117449
|
context4.output.write(`Created task ${result.ref.id}: ${result.ref.filePath}`);
|
|
117450
|
+
if (readiness.readiness.status === "ready") {
|
|
117451
|
+
context4.output.write(`Ready: checklist + post-check passed (${result.ref.id} promoted to todo)`);
|
|
117452
|
+
}
|
|
115861
117453
|
}
|
|
115862
117454
|
} catch (err) {
|
|
115863
117455
|
if (err instanceof WbsCollisionError) {
|
|
@@ -115916,8 +117508,25 @@ function registerTaskCommand(program2, context4) {
|
|
|
115916
117508
|
context4.output.error(err.message);
|
|
115917
117509
|
}
|
|
115918
117510
|
context4.setExitCode(3);
|
|
117511
|
+
} else if (err instanceof TaskCandidateInvalidError) {
|
|
117512
|
+
writeCreateJsonError(context4, options, "candidate-invalid", err.message, {
|
|
117513
|
+
findings: err.findings
|
|
117514
|
+
});
|
|
117515
|
+
context4.setExitCode(1);
|
|
117516
|
+
} else if (err instanceof TaskPreparationError) {
|
|
117517
|
+
const message = !options.json && err.recoveryCommand !== undefined ? `${err.message}
|
|
117518
|
+
Recover with: ${err.recoveryCommand}` : err.message;
|
|
117519
|
+
writeCreateJsonError(context4, options, "preparation-failed", message, {
|
|
117520
|
+
wbs: err.wbs,
|
|
117521
|
+
filePath: err.filePath,
|
|
117522
|
+
failedStage: err.stage,
|
|
117523
|
+
...err.recoveryCommand !== undefined ? { recoveryCommand: err.recoveryCommand } : {},
|
|
117524
|
+
...err.findings !== undefined ? { findings: err.findings } : {},
|
|
117525
|
+
readiness: { status: "failed", depth: "ready" }
|
|
117526
|
+
});
|
|
117527
|
+
context4.setExitCode(1);
|
|
115919
117528
|
} else {
|
|
115920
|
-
|
|
117529
|
+
writeCreateJsonError(context4, options, "create-failed", String(err));
|
|
115921
117530
|
context4.setExitCode(1);
|
|
115922
117531
|
}
|
|
115923
117532
|
}
|
|
@@ -115999,7 +117608,7 @@ ${result.content}`);
|
|
|
115999
117608
|
let forcedDoneVerdict;
|
|
116000
117609
|
if (status === "done") {
|
|
116001
117610
|
const current = await svc.show(wbs);
|
|
116002
|
-
const verdictDir = options.verdictDir ??
|
|
117611
|
+
const verdictDir = options.verdictDir ?? join38(context4.cwd, ".spur", "run");
|
|
116003
117612
|
const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
|
|
116004
117613
|
const guardOutcome = evaluateDoneTransition({
|
|
116005
117614
|
wbs,
|
|
@@ -116235,7 +117844,7 @@ ${result.content}`);
|
|
|
116235
117844
|
dryRun,
|
|
116236
117845
|
projectRoot: context4.cwd,
|
|
116237
117846
|
write: async (filePath, wbs, section, newBody) => {
|
|
116238
|
-
const ref = { kind: "task", id: wbs, filePath, folder:
|
|
117847
|
+
const ref = { kind: "task", id: wbs, filePath, folder: dirname22(filePath) };
|
|
116239
117848
|
const ws = new PlanningWriteService({ fs: context4.fs, emitter: makePlanningEmitter(context4) });
|
|
116240
117849
|
await ws.updateSection(ref, section, newBody);
|
|
116241
117850
|
}
|
|
@@ -116289,13 +117898,36 @@ ${result.content}`);
|
|
|
116289
117898
|
context4.setExitCode(1);
|
|
116290
117899
|
}
|
|
116291
117900
|
});
|
|
116292
|
-
task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption(...SHARED_OPTIONS.fileTaskBatch).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
|
|
117901
|
+
task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption(...SHARED_OPTIONS.fileTaskBatch).option("--skip-ready", "Skip ready preparation (write supplied content as-is, no model dispatch)").option("--agent <selector>", "Agent used for ready preparation (defaults to the configured agent)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (options) => {
|
|
117902
|
+
if (options.skipReady === true && options.agent !== undefined) {
|
|
117903
|
+
writeCreateJsonError(context4, options, "invalid-usage", "--agent has no effect with --skip-ready (no preparation runs)");
|
|
117904
|
+
context4.setExitCode(2);
|
|
117905
|
+
return;
|
|
117906
|
+
}
|
|
116293
117907
|
const svc = await makeService2(context4, options.folder);
|
|
117908
|
+
let tmpPath;
|
|
116294
117909
|
try {
|
|
116295
|
-
|
|
117910
|
+
let batchFile = options.file;
|
|
117911
|
+
let readiness = READY_SKIPPED;
|
|
117912
|
+
if (options.skipReady !== true) {
|
|
117913
|
+
const batchSource = await context4.fs.readFile(options.file);
|
|
117914
|
+
const prepared = await prepareBatchTaskReady({
|
|
117915
|
+
batchPath: options.file,
|
|
117916
|
+
batchSource,
|
|
117917
|
+
agents: context4.agentService(),
|
|
117918
|
+
agentSelector: options.agent,
|
|
117919
|
+
cwd: context4.cwd
|
|
117920
|
+
});
|
|
117921
|
+
tmpPath = join38(tmpdir3(), `spur-ready-batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
117922
|
+
await writeFile(tmpPath, `${JSON.stringify(prepared.items, null, 2)}
|
|
117923
|
+
`);
|
|
117924
|
+
batchFile = tmpPath;
|
|
117925
|
+
readiness = READY_DONE;
|
|
117926
|
+
}
|
|
117927
|
+
const { children, parentsWired } = await svc.batchCreate(batchFile);
|
|
116296
117928
|
if (options.json) {
|
|
116297
117929
|
const ids = children.map((r2) => r2.ref.id);
|
|
116298
|
-
context4.output.write(toEnvelopeJson({ created: children.length, wbs: ids, parentsWired }, { enveloped: options.jsonEnvelope }));
|
|
117930
|
+
context4.output.write(toEnvelopeJson({ created: children.length, wbs: ids, parentsWired, ...readiness }, { enveloped: options.jsonEnvelope }));
|
|
116299
117931
|
} else {
|
|
116300
117932
|
context4.output.write(`Created ${children.length} task(s)`);
|
|
116301
117933
|
for (const r2 of children) {
|
|
@@ -116311,7 +117943,16 @@ ${result.content}`);
|
|
|
116311
117943
|
}
|
|
116312
117944
|
}
|
|
116313
117945
|
} catch (err) {
|
|
116314
|
-
if (err instanceof
|
|
117946
|
+
if (err instanceof TaskPreparationError) {
|
|
117947
|
+
const message = !options.json && err.recoveryCommand !== undefined ? `${err.message}
|
|
117948
|
+
Recover with: ${err.recoveryCommand}` : err.message;
|
|
117949
|
+
writeCreateJsonError(context4, options, "preparation-failed", message, {
|
|
117950
|
+
failedStage: err.stage,
|
|
117951
|
+
...err.recoveryCommand !== undefined ? { recoveryCommand: err.recoveryCommand } : {},
|
|
117952
|
+
readiness: { status: "failed", depth: "ready" }
|
|
117953
|
+
});
|
|
117954
|
+
context4.setExitCode(1);
|
|
117955
|
+
} else if (err instanceof WbsCollisionError) {
|
|
116315
117956
|
if (options.json) {
|
|
116316
117957
|
context4.output.write(toEnvelopeJson({
|
|
116317
117958
|
ok: false,
|
|
@@ -116339,10 +117980,19 @@ ${result.content}`);
|
|
|
116339
117980
|
context4.output.error(err.message);
|
|
116340
117981
|
}
|
|
116341
117982
|
context4.setExitCode(3);
|
|
117983
|
+
} else if (err instanceof TaskCandidateInvalidError) {
|
|
117984
|
+
writeCreateJsonError(context4, options, "candidate-invalid", err.message, {
|
|
117985
|
+
findings: err.findings
|
|
117986
|
+
});
|
|
117987
|
+
context4.setExitCode(1);
|
|
116342
117988
|
} else {
|
|
116343
|
-
|
|
117989
|
+
writeCreateJsonError(context4, options, "batch-create-failed", String(err));
|
|
116344
117990
|
context4.setExitCode(1);
|
|
116345
117991
|
}
|
|
117992
|
+
} finally {
|
|
117993
|
+
if (tmpPath !== undefined) {
|
|
117994
|
+
await rm(tmpPath, { force: true }).catch(() => {});
|
|
117995
|
+
}
|
|
116346
117996
|
}
|
|
116347
117997
|
});
|
|
116348
117998
|
task.command("record").summary("Record pipeline results into the task file \u2014 Testing, Review, and optional Solution backfill.").argument("<wbs>", "Task WBS number").option("--verdict-file <path>", "Path to verdict JSON (default: .spur/run/<wbs>-verdict.json)").option("--solution-from-diff", "Backfill Solution from git diff when bare").option("--transition <status>", "Optional lifecycle transition (e.g. testing)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
|
|
@@ -116549,9 +118199,8 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
116549
118199
|
const msg = `Duplicate WBS ${first.wbs} found in ${dup.length} files:
|
|
116550
118200
|
` + dup.map((h2) => ` ${h2.filePath}`).join(`
|
|
116551
118201
|
`);
|
|
116552
|
-
|
|
116553
|
-
|
|
116554
|
-
} else {
|
|
118202
|
+
results.push({ wbs: first.wbs, pass: false, status: "duplicate", findings: [msg] });
|
|
118203
|
+
if (!json3) {
|
|
116555
118204
|
context4.output.error(msg);
|
|
116556
118205
|
}
|
|
116557
118206
|
}
|
|
@@ -116682,13 +118331,13 @@ function loadTemplateBodies(projectRoot, variant) {
|
|
|
116682
118331
|
if (cached2 !== undefined)
|
|
116683
118332
|
return cached2;
|
|
116684
118333
|
let bodies = {};
|
|
116685
|
-
const localPath =
|
|
118334
|
+
const localPath = join38(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
|
|
116686
118335
|
if (existsSync11(localPath)) {
|
|
116687
118336
|
bodies = extractTemplateBodies(readFileSync11(localPath, "utf8"));
|
|
116688
118337
|
} else {
|
|
116689
118338
|
const root = bundledConfigRoot();
|
|
116690
118339
|
if (root !== null) {
|
|
116691
|
-
const templatePath =
|
|
118340
|
+
const templatePath = join38(root, "templates", "task", `${variant}.md`);
|
|
116692
118341
|
if (existsSync11(templatePath)) {
|
|
116693
118342
|
bodies = extractTemplateBodies(readFileSync11(templatePath, "utf8"));
|
|
116694
118343
|
}
|
|
@@ -116746,7 +118395,7 @@ async function loadSectionMatrixUncached(projectRoot) {
|
|
|
116746
118395
|
}
|
|
116747
118396
|
const root = bundledConfigRoot();
|
|
116748
118397
|
if (root !== null) {
|
|
116749
|
-
const matrixPath =
|
|
118398
|
+
const matrixPath = join38(root, "tasks", "section-matrix.yaml");
|
|
116750
118399
|
if (await fs3.exists(matrixPath)) {
|
|
116751
118400
|
const data = await loadStructuredSpurConfig(matrixPath, {
|
|
116752
118401
|
validateJsonSchema: true,
|
|
@@ -116757,7 +118406,7 @@ async function loadSectionMatrixUncached(projectRoot) {
|
|
|
116757
118406
|
}
|
|
116758
118407
|
throw new Error(`no canonical section-matrix found for task section authority (F92 R1); tried:
|
|
116759
118408
|
` + ` - ${localPath}
|
|
116760
|
-
` + (root !== null ? ` - ${
|
|
118409
|
+
` + (root !== null ? ` - ${join38(root, "tasks", "section-matrix.yaml")}
|
|
116761
118410
|
` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
|
|
116762
118411
|
}
|
|
116763
118412
|
|
|
@@ -117046,7 +118695,7 @@ init_dist7();
|
|
|
117046
118695
|
init_dist5();
|
|
117047
118696
|
await init_src3();
|
|
117048
118697
|
import { closeSync as closeSync5, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync3 } from "fs";
|
|
117049
|
-
import { join as
|
|
118698
|
+
import { join as join39 } from "path";
|
|
117050
118699
|
import { createInterface } from "readline";
|
|
117051
118700
|
import { setTimeout as sleep5 } from "timers/promises";
|
|
117052
118701
|
|
|
@@ -117224,11 +118873,11 @@ async function writeWorkflowPlanArtifact(cwd, runId, resolved, secretValues) {
|
|
|
117224
118873
|
...step.description !== undefined ? { description: redactAndBound(step.description, secretValues, 512) } : {}
|
|
117225
118874
|
}))
|
|
117226
118875
|
});
|
|
117227
|
-
const { mkdir, writeFile } = await import("fs/promises");
|
|
117228
|
-
const artifactsDir =
|
|
118876
|
+
const { mkdir, writeFile: writeFile2 } = await import("fs/promises");
|
|
118877
|
+
const artifactsDir = join39(cwd, ".spur", "run");
|
|
117229
118878
|
await mkdir(artifactsDir, { recursive: true });
|
|
117230
|
-
const artifactPath =
|
|
117231
|
-
await
|
|
118879
|
+
const artifactPath = join39(artifactsDir, `${runId}-workflow-plan.json`);
|
|
118880
|
+
await writeFile2(artifactPath, payload);
|
|
117232
118881
|
return artifactPath;
|
|
117233
118882
|
}
|
|
117234
118883
|
async function makeEscalationPacketSink(bus, context4) {
|
|
@@ -117285,7 +118934,7 @@ function resolveWorkflowPaths(config4) {
|
|
|
117285
118934
|
for (const path9 of paths) {
|
|
117286
118935
|
if (path9.startsWith(BUNDLED_PATH_PREFIX)) {
|
|
117287
118936
|
if (bundledRoot !== null)
|
|
117288
|
-
expanded.push(
|
|
118937
|
+
expanded.push(join39(bundledRoot, path9.slice(BUNDLED_PATH_PREFIX.length)));
|
|
117289
118938
|
} else {
|
|
117290
118939
|
expanded.push(path9);
|
|
117291
118940
|
}
|
|
@@ -117522,7 +119171,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
117522
119171
|
}
|
|
117523
119172
|
const runLog = options.log === false ? undefined : new WorkflowRunLogSink({
|
|
117524
119173
|
bus,
|
|
117525
|
-
dir:
|
|
119174
|
+
dir: join39(context4.cwd, ".spur", "run"),
|
|
117526
119175
|
runId,
|
|
117527
119176
|
...planPreview !== undefined ? { planPreview } : {},
|
|
117528
119177
|
...resolveOutputLogConfig(context4.spurConfig ?? null)
|
|
@@ -118049,7 +119698,7 @@ function readRunLogChunk(logPath, offset) {
|
|
|
118049
119698
|
}
|
|
118050
119699
|
}
|
|
118051
119700
|
async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => sleep5(ms)) {
|
|
118052
|
-
const logPath =
|
|
119701
|
+
const logPath = join39(dir, ".spur", "run", `${runId}.log`);
|
|
118053
119702
|
let offset = 0;
|
|
118054
119703
|
let everRead = false;
|
|
118055
119704
|
while (true) {
|
|
@@ -118112,7 +119761,7 @@ init_src();
|
|
|
118112
119761
|
init_src2();
|
|
118113
119762
|
init_dist5();
|
|
118114
119763
|
await init_src3();
|
|
118115
|
-
import { dirname as
|
|
119764
|
+
import { dirname as dirname23, join as join40, resolve as resolve18 } from "path";
|
|
118116
119765
|
import { isatty as isatty4 } from "tty";
|
|
118117
119766
|
|
|
118118
119767
|
// ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
|
|
@@ -119527,7 +121176,7 @@ function resolveAgentRoles(agentConfig) {
|
|
|
119527
121176
|
return resolved;
|
|
119528
121177
|
}
|
|
119529
121178
|
function createCliContext(options) {
|
|
119530
|
-
const cwd =
|
|
121179
|
+
const cwd = resolve18(options.cwd ?? process.cwd());
|
|
119531
121180
|
const env = options.env ?? process.env;
|
|
119532
121181
|
const fs3 = createNodeFileSystem3(cwd);
|
|
119533
121182
|
const agentConfig = options.agentConfig ?? options.spurConfig?.agent;
|
|
@@ -119575,10 +121224,10 @@ function createCliContext(options) {
|
|
|
119575
121224
|
function noopSetExitCode(_code) {}
|
|
119576
121225
|
async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
|
|
119577
121226
|
const config4 = buildConfigFromEnv(env);
|
|
119578
|
-
const configuredUrl = env.DATABASE_URL === undefined ?
|
|
121227
|
+
const configuredUrl = env.DATABASE_URL === undefined ? join40(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
|
|
119579
121228
|
const url2 = dbUrl ?? configuredUrl;
|
|
119580
121229
|
if (url2 !== IN_MEMORY_DATABASE_URL) {
|
|
119581
|
-
await createNodeFileSystem3().ensureDir(
|
|
121230
|
+
await createNodeFileSystem3().ensureDir(dirname23(url2));
|
|
119582
121231
|
}
|
|
119583
121232
|
return createMigratedDb({ url: url2 });
|
|
119584
121233
|
}
|
|
@@ -119694,8 +121343,7 @@ async function runCli() {
|
|
|
119694
121343
|
return main();
|
|
119695
121344
|
}
|
|
119696
121345
|
if (import.meta.main) {
|
|
119697
|
-
|
|
119698
|
-
process.exit(exitCode);
|
|
121346
|
+
process.exitCode = await runCli();
|
|
119699
121347
|
}
|
|
119700
121348
|
export {
|
|
119701
121349
|
shouldRenderBanner,
|