@gobing-ai/spur 0.3.55 → 0.3.57
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 +2 -3
- package/config/corpus-baseline.json +2521 -49
- package/config/rules/strict/runtime-boundaries.yaml +3 -0
- package/config/rules/surface/check-cli-surface.yaml +1 -0
- package/config/workflow-composition-baseline.json +241 -22
- package/package.json +1 -1
- package/plugins/sp/agents/expert-spur.md +3 -0
- package/plugins/sp/commands/dev-idea.md +7 -19
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/task-size-precheck.ts +11 -6
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +4 -4
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +4 -2
- package/plugins/sp/skills/issue-finding/SKILL.md +23 -11
- package/plugins/sp/skills/spur-cli/SKILL.md +23 -15
- package/plugins/sp/skills/spur-cli/references/agent.md +5 -0
- package/plugins/sp/skills/spur-cli/references/builder.md +49 -0
- package/plugins/sp/skills/spur-cli/references/features.md +6 -1
- package/plugins/sp/skills/spur-cli/references/message.md +5 -0
- package/plugins/sp/skills/spur-cli/references/rules.md +5 -0
- package/plugins/sp/skills/spur-cli/references/self.md +101 -0
- package/plugins/sp/skills/spur-cli/references/tasks.md +6 -1
- package/plugins/sp/skills/spur-cli/references/team.md +5 -0
- package/plugins/sp/skills/spur-cli/references/workflows.md +40 -0
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +9 -8
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +19 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +27 -5
- package/spur.js +1546 -314
package/spur.js
CHANGED
|
@@ -37430,7 +37430,10 @@ var init_finding_codes = __esm(() => {
|
|
|
37430
37430
|
"L2.missing-required-section",
|
|
37431
37431
|
"L2.forbidden-section",
|
|
37432
37432
|
"L2.disallowed-section",
|
|
37433
|
+
"L2.heading-level",
|
|
37434
|
+
"L2.section-order",
|
|
37433
37435
|
"L3.requirements-format",
|
|
37436
|
+
"L3.requirements-checkbox",
|
|
37434
37437
|
"L3.requirements-empty",
|
|
37435
37438
|
"L3.ac-empty",
|
|
37436
37439
|
"L3.ac-requirement-coverage",
|
|
@@ -37478,7 +37481,10 @@ var init_finding_codes = __esm(() => {
|
|
|
37478
37481
|
L2_MISSING_REQUIRED_SECTION: "L2.missing-required-section",
|
|
37479
37482
|
L2_FORBIDDEN_SECTION: "L2.forbidden-section",
|
|
37480
37483
|
L2_DISALLOWED_SECTION: "L2.disallowed-section",
|
|
37484
|
+
L2_HEADING_LEVEL: "L2.heading-level",
|
|
37485
|
+
L2_SECTION_ORDER: "L2.section-order",
|
|
37481
37486
|
L3_REQUIREMENTS_FORMAT: "L3.requirements-format",
|
|
37487
|
+
L3_REQUIREMENTS_CHECKBOX: "L3.requirements-checkbox",
|
|
37482
37488
|
L3_REQUIREMENTS_EMPTY: "L3.requirements-empty",
|
|
37483
37489
|
L3_AC_EMPTY: "L3.ac-empty",
|
|
37484
37490
|
L3_AC_REQUIREMENT_COVERAGE: "L3.ac-requirement-coverage",
|
|
@@ -50615,6 +50621,10 @@ async function countCheckpointsBySource(db2, source) {
|
|
|
50615
50621
|
const row = await db2.queryFirst("SELECT COUNT(*) AS cnt FROM history_import_checkpoint WHERE source = ?", source);
|
|
50616
50622
|
return row?.cnt ?? 0;
|
|
50617
50623
|
}
|
|
50624
|
+
async function countToolCallsSince(db2, source, runStartedAt) {
|
|
50625
|
+
const row = await db2.queryFirst("SELECT COUNT(*) AS cnt FROM history_tool_call WHERE source = ? AND imported_at >= ?", source, runStartedAt);
|
|
50626
|
+
return row?.cnt ?? 0;
|
|
50627
|
+
}
|
|
50618
50628
|
function withStepPredicates(wmWhere, predicates) {
|
|
50619
50629
|
return wmWhere === "" ? `WHERE ${predicates}` : `${wmWhere} AND ${predicates}`;
|
|
50620
50630
|
}
|
|
@@ -51040,7 +51050,7 @@ function renderBySession(artifact) {
|
|
|
51040
51050
|
}
|
|
51041
51051
|
for (const s of artifact.bySession) {
|
|
51042
51052
|
const tokens = (s.tokens / 1e6).toFixed(1);
|
|
51043
|
-
lines.push(` ${s.sessionId.slice(0, 18).padEnd(18)} ` + `${s.source.slice(0, 8).padEnd(8)} ` + `${String(s.messages).padStart(5)} msg ${String(s.toolCalls).padStart(5)} calls ` + `${tokens.padStart(6)}M tok $${s.costUsd.toFixed(2).padStart(7)}` + (s.topTool ? ` top: ${s.topTool}` : ""));
|
|
51053
|
+
lines.push(` ${s.sessionId.slice(0, 18).padEnd(18)} ` + `${s.source.slice(0, 8).padEnd(8)} ` + `${(s.startedAt ?? "\u2026").slice(0, 10).padStart(10)} ` + `${String(s.messages).padStart(5)} msg ${String(s.toolCalls).padStart(5)} calls ` + `${tokens.padStart(6)}M tok $${s.costUsd.toFixed(2).padStart(7)}` + (s.topTool ? ` top: ${s.topTool}` : ""));
|
|
51044
51054
|
}
|
|
51045
51055
|
return lines;
|
|
51046
51056
|
}
|
|
@@ -53764,6 +53774,9 @@ class TaskRunLinkDao {
|
|
|
53764
53774
|
await this.db.run(`INSERT INTO task_run_links (id, wbs, run_id, kind, created_at)
|
|
53765
53775
|
VALUES (?, ?, ?, ?, ?)`, [input.id, input.wbs, input.run_id, input.kind, input.created_at]);
|
|
53766
53776
|
}
|
|
53777
|
+
async updateRunId(id, runId) {
|
|
53778
|
+
await this.db.run(`UPDATE task_run_links SET run_id = ?1 WHERE id = ?2`, runId, id);
|
|
53779
|
+
}
|
|
53767
53780
|
async listByWbs(wbs, limit) {
|
|
53768
53781
|
try {
|
|
53769
53782
|
return await this.db.queryAll(`SELECT id, wbs, run_id, kind, created_at
|
|
@@ -62114,7 +62127,8 @@ async function applyCliMigrations(adapter, migrations = CLI_MIGRATIONS) {
|
|
|
62114
62127
|
const argsRawSkip = migration.id === "0012_spur_cli_history_tool_call_args_raw" && !await tableExists(adapter, "history_tool_call");
|
|
62115
62128
|
const callIdSkip = migration.id === "0015_spur_cli_history_tool_call_call_id" && !await tableExists(adapter, "history_tool_call");
|
|
62116
62129
|
const nameOccurredIndexSkip = migration.id === "0014_spur_cli_system_events_name_occurred_idx" && !await tableExists(adapter, "system_events");
|
|
62117
|
-
|
|
62130
|
+
const runsStatusDoneSkip = migration.id === "0017_spur_cli_runs_status_completed_to_done" && (!await tableExists(adapter, "runs") || !await columnExists(adapter, "runs", "status"));
|
|
62131
|
+
if (shouldApplySql && !sequenceIndexSkip && !argsRawSkip && !runsStatusDoneSkip && !nameOccurredIndexSkip && !callIdSkip && !tsNullableSkip) {
|
|
62118
62132
|
for (const statement of splitSqlStatements(migration.sql)) {
|
|
62119
62133
|
await adapter.exec(statement);
|
|
62120
62134
|
}
|
|
@@ -62369,6 +62383,10 @@ ${HISTORY_RUN_SESSION_SCHEMA_SQL}
|
|
|
62369
62383
|
{
|
|
62370
62384
|
id: "0016_spur_cli_history_message_ts_nullable",
|
|
62371
62385
|
sql: HISTORY_MESSAGE_TS_NULLABLE_SCHEMA_SQL
|
|
62386
|
+
},
|
|
62387
|
+
{
|
|
62388
|
+
id: "0017_spur_cli_runs_status_completed_to_done",
|
|
62389
|
+
sql: "UPDATE runs SET status = 'done' WHERE status = 'completed'"
|
|
62372
62390
|
}
|
|
62373
62391
|
];
|
|
62374
62392
|
});
|
|
@@ -64266,8 +64284,8 @@ var init_markdown_document = __esm(() => {
|
|
|
64266
64284
|
"Q&A",
|
|
64267
64285
|
"Design",
|
|
64268
64286
|
"Plan",
|
|
64269
|
-
"Solution",
|
|
64270
64287
|
"Root Cause",
|
|
64288
|
+
"Solution",
|
|
64271
64289
|
"Testing",
|
|
64272
64290
|
"Review",
|
|
64273
64291
|
"References",
|
|
@@ -64599,6 +64617,62 @@ var init_task_skeleton = __esm(() => {
|
|
|
64599
64617
|
CANONICAL_INDEX = new Map(TASK_CANONICAL_SECTIONS.map((s2, i2) => [s2, i2]));
|
|
64600
64618
|
});
|
|
64601
64619
|
|
|
64620
|
+
// ../../packages/domain/src/retention.ts
|
|
64621
|
+
import { readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync6 } from "fs";
|
|
64622
|
+
import { join as join7 } from "path";
|
|
64623
|
+
async function runRetention(db2, cwd, now = new Date) {
|
|
64624
|
+
const result = { ruleEvalRuns: 0, queueJobs: 0, ledgerRows: 0, backupFiles: 0 };
|
|
64625
|
+
result.ruleEvalRuns = await purgeRuleEvalRuns(db2, now);
|
|
64626
|
+
result.queueJobs = await purgeQueueJobs(db2, now);
|
|
64627
|
+
result.ledgerRows = await purgeLedger(db2, now);
|
|
64628
|
+
result.backupFiles = pruneBackups(cwd, now);
|
|
64629
|
+
return result;
|
|
64630
|
+
}
|
|
64631
|
+
async function purgeRuleEvalRuns(db2, now) {
|
|
64632
|
+
return purgeRows(db2, `DELETE FROM rule_eval_runs WHERE created_at < ?`, new Date(now.getTime() - RULE_EVAL_RUN_RETENTION_DAYS * DAY_MS).toISOString());
|
|
64633
|
+
}
|
|
64634
|
+
async function purgeQueueJobs(db2, now) {
|
|
64635
|
+
return purgeRows(db2, `DELETE FROM queue_jobs WHERE status IN ('completed', 'failed') AND updated_at < ?`, now.getTime() - QUEUE_JOB_RETENTION_DAYS * DAY_MS);
|
|
64636
|
+
}
|
|
64637
|
+
async function purgeLedger(db2, now) {
|
|
64638
|
+
return purgeRows(db2, `DELETE FROM history_import_ledger WHERE imported_at < ?`, new Date(now.getTime() - LEDGER_RETENTION_DAYS * DAY_MS).toISOString());
|
|
64639
|
+
}
|
|
64640
|
+
function pruneBackups(cwd, now) {
|
|
64641
|
+
const dir = join7(cwd, ".spur", "backups");
|
|
64642
|
+
let entries;
|
|
64643
|
+
try {
|
|
64644
|
+
entries = readdirSync5(dir);
|
|
64645
|
+
} catch {
|
|
64646
|
+
return 0;
|
|
64647
|
+
}
|
|
64648
|
+
const cutoffMs = now.getTime() - BACKUP_RETENTION_DAYS * DAY_MS;
|
|
64649
|
+
let reclaimed = 0;
|
|
64650
|
+
for (const name of entries) {
|
|
64651
|
+
const path9 = join7(dir, name);
|
|
64652
|
+
try {
|
|
64653
|
+
const stat = statSync6(path9);
|
|
64654
|
+
if (!stat.isFile() || stat.mtimeMs >= cutoffMs)
|
|
64655
|
+
continue;
|
|
64656
|
+
rmSync3(path9, { force: true });
|
|
64657
|
+
reclaimed += 1;
|
|
64658
|
+
} catch {}
|
|
64659
|
+
}
|
|
64660
|
+
return reclaimed;
|
|
64661
|
+
}
|
|
64662
|
+
async function purgeRows(db2, sql4, cutoff) {
|
|
64663
|
+
try {
|
|
64664
|
+
await db2.run(sql4, cutoff);
|
|
64665
|
+
const row = await db2.queryFirst("SELECT changes() AS n");
|
|
64666
|
+
return row?.n ?? 0;
|
|
64667
|
+
} catch {
|
|
64668
|
+
return 0;
|
|
64669
|
+
}
|
|
64670
|
+
}
|
|
64671
|
+
var RULE_EVAL_RUN_RETENTION_DAYS = 90, QUEUE_JOB_RETENTION_DAYS = 30, LEDGER_RETENTION_DAYS = 180, BACKUP_RETENTION_DAYS = 30, DAY_MS;
|
|
64672
|
+
var init_retention = __esm(() => {
|
|
64673
|
+
DAY_MS = 24 * 60 * 60 * 1000;
|
|
64674
|
+
});
|
|
64675
|
+
|
|
64602
64676
|
// ../../packages/domain/src/stage-registry/validator.ts
|
|
64603
64677
|
function generateRunId() {
|
|
64604
64678
|
const crypto3 = globalThis.crypto;
|
|
@@ -64881,6 +64955,7 @@ __export(exports_src, {
|
|
|
64881
64955
|
selectorDigest: () => selectorDigest,
|
|
64882
64956
|
sectionMatrixSchema: () => sectionMatrixSchema,
|
|
64883
64957
|
scaffoldFeatureScenarios: () => scaffoldFeatureScenarios,
|
|
64958
|
+
runRetention: () => runRetention,
|
|
64884
64959
|
roleTokenSummary: () => roleTokenSummary,
|
|
64885
64960
|
resolveReportMode: () => resolveReportMode,
|
|
64886
64961
|
resolvePricing: () => resolvePricing,
|
|
@@ -64963,6 +65038,7 @@ __export(exports_src, {
|
|
|
64963
65038
|
createId: () => createId,
|
|
64964
65039
|
createDefaultRegistry: () => createDefaultRegistry,
|
|
64965
65040
|
createBoundaryContext: () => createBoundaryContext,
|
|
65041
|
+
countToolCallsSince: () => countToolCallsSince,
|
|
64966
65042
|
countCheckpointsBySource: () => countCheckpointsBySource,
|
|
64967
65043
|
computeSnapshotHash: () => computeSnapshotHash,
|
|
64968
65044
|
computeRecordCost: () => computeRecordCost,
|
|
@@ -65086,6 +65162,7 @@ var init_src2 = __esm(() => {
|
|
|
65086
65162
|
init_rebuild_events();
|
|
65087
65163
|
init_schema6();
|
|
65088
65164
|
init_task_skeleton();
|
|
65165
|
+
init_retention();
|
|
65089
65166
|
init_stage_registry();
|
|
65090
65167
|
});
|
|
65091
65168
|
|
|
@@ -65601,7 +65678,7 @@ var isAgentExecution = (e) => ("executionId" in e), isActionOutput = (e) => ("st
|
|
|
65601
65678
|
|
|
65602
65679
|
// ../../packages/app/src/observability/workflow-run-log-sink.ts
|
|
65603
65680
|
import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, writeSync as writeSync2 } from "fs";
|
|
65604
|
-
import { join as
|
|
65681
|
+
import { join as join8 } from "path";
|
|
65605
65682
|
|
|
65606
65683
|
class WorkflowRunLogSink {
|
|
65607
65684
|
filePath;
|
|
@@ -65617,7 +65694,7 @@ class WorkflowRunLogSink {
|
|
|
65617
65694
|
bus;
|
|
65618
65695
|
handlers;
|
|
65619
65696
|
constructor(options) {
|
|
65620
|
-
this.filePath =
|
|
65697
|
+
this.filePath = join8(options.dir, `${options.runId}.log`);
|
|
65621
65698
|
this.maxBytes = options.maxBytes ?? DEFAULT_RUN_LOG_MAX_BYTES;
|
|
65622
65699
|
this.maxLines = options.maxLines;
|
|
65623
65700
|
this.planPreview = options.planPreview;
|
|
@@ -66128,7 +66205,24 @@ class AgentService {
|
|
|
66128
66205
|
async doctor(args, deps) {
|
|
66129
66206
|
const executors = this.ctx.agentConfig?.executors;
|
|
66130
66207
|
const doctorRunner = deps?.doctorRunner ?? new DoctorRunner({ env: this.ctx.env, executors });
|
|
66131
|
-
|
|
66208
|
+
if (args.agent === undefined) {
|
|
66209
|
+
const results2 = await doctorRunner.runAll();
|
|
66210
|
+
return this.renderDoctor(results2, executors, args.json, args.agent);
|
|
66211
|
+
}
|
|
66212
|
+
const roleDef = this.ctx.roles?.get(args.agent);
|
|
66213
|
+
if (roleDef !== undefined) {
|
|
66214
|
+
const resolved = await this.resolveRole(args.agent, roleDef.tier, doctorRunner);
|
|
66215
|
+
if (!resolved.ok) {
|
|
66216
|
+
this.ctx.output.error(resolved.message);
|
|
66217
|
+
return resolved.exitCode;
|
|
66218
|
+
}
|
|
66219
|
+
const results2 = [await doctorRunner.runOne(resolved.executor ?? resolved.agent)];
|
|
66220
|
+
return this.renderDoctor(results2, executors, args.json, resolved.executor ?? resolved.agent);
|
|
66221
|
+
}
|
|
66222
|
+
const results = [await doctorRunner.runOne(args.agent)];
|
|
66223
|
+
return this.renderDoctor(results, executors, args.json, args.agent);
|
|
66224
|
+
}
|
|
66225
|
+
renderDoctor(results, executors, json3, agent) {
|
|
66132
66226
|
const modelByExecutor = new Map((executors ?? []).filter((e) => e.model !== undefined).map((e) => [e.name, e.model]));
|
|
66133
66227
|
for (const result of results) {
|
|
66134
66228
|
if (result.modelStatus && (result.modelStatus.status === "quota_exhausted" || result.modelStatus.status === "unavailable")) {
|
|
@@ -66136,14 +66230,14 @@ class AgentService {
|
|
|
66136
66230
|
this.ctx.output.error(`Warning: executor ${result.agent} (model ${model}) reports ${result.modelStatus.status}. Consider \`--agent <alt>\` or check token quota.`);
|
|
66137
66231
|
}
|
|
66138
66232
|
}
|
|
66139
|
-
if (
|
|
66233
|
+
if (json3) {
|
|
66140
66234
|
const executorByName = new Map((executors ?? []).map((e) => [e.name, e]));
|
|
66141
66235
|
const rows = results.map((result) => {
|
|
66142
66236
|
const executor = executorByName.get(result.agent) ?? { name: result.agent, agent: result.agent };
|
|
66143
66237
|
return { ...result, capabilityTier: getExecutorTier(executor) };
|
|
66144
66238
|
});
|
|
66145
66239
|
this.ctx.output.write(toJson({ agents: rows }));
|
|
66146
|
-
} else if (
|
|
66240
|
+
} else if (agent !== undefined) {
|
|
66147
66241
|
this.ctx.output.write(renderDoctorDetail(results[0] ?? null));
|
|
66148
66242
|
} else {
|
|
66149
66243
|
this.ctx.output.write(renderDoctorTable(results));
|
|
@@ -67092,7 +67186,6 @@ function renderDoctorTable(results) {
|
|
|
67092
67186
|
state: usable ? "usable" : "missing",
|
|
67093
67187
|
agent: result.agent,
|
|
67094
67188
|
tier: String(result.tier),
|
|
67095
|
-
auth: usable ? renderAuth(result.authenticated) : dash,
|
|
67096
67189
|
version: result.version ?? dash,
|
|
67097
67190
|
model: renderModelStatus(result.modelStatus)
|
|
67098
67191
|
};
|
|
@@ -67102,7 +67195,6 @@ function renderDoctorTable(results) {
|
|
|
67102
67195
|
state: "STATUS",
|
|
67103
67196
|
agent: "AGENT",
|
|
67104
67197
|
tier: "TIER",
|
|
67105
|
-
auth: "AUTH",
|
|
67106
67198
|
version: "VERSION",
|
|
67107
67199
|
model: "MODEL"
|
|
67108
67200
|
};
|
|
@@ -67111,9 +67203,8 @@ function renderDoctorTable(results) {
|
|
|
67111
67203
|
const wState = width("state");
|
|
67112
67204
|
const wAgent = width("agent");
|
|
67113
67205
|
const wTier = width("tier");
|
|
67114
|
-
const wAuth = width("auth");
|
|
67115
67206
|
const wVersion = width("version");
|
|
67116
|
-
const line = (row) => `${row.glyph} ${row.state.padEnd(wState)} ${row.agent.padEnd(wAgent)} ${row.tier.padEnd(wTier)} ${row.
|
|
67207
|
+
const line = (row) => `${row.glyph} ${row.state.padEnd(wState)} ${row.agent.padEnd(wAgent)} ${row.tier.padEnd(wTier)} ${row.version.padEnd(wVersion)} ${row.model}`.trimEnd();
|
|
67117
67208
|
const usableCount = rows.filter((row) => row.state === "usable").length;
|
|
67118
67209
|
const missingTier1 = results.filter((result) => !result.usable && result.tier === 1).length;
|
|
67119
67210
|
const footer = missingTier1 > 0 ? `${usableCount} usable, ${missingTier1} missing (tier-1)` : `${usableCount} usable, ${rows.length - usableCount} missing`;
|
|
@@ -67239,7 +67330,7 @@ var init_agent_service = __esm(() => {
|
|
|
67239
67330
|
});
|
|
67240
67331
|
|
|
67241
67332
|
// ../../packages/app/src/services/anchor-qualifier.ts
|
|
67242
|
-
import { basename as basename2, join as
|
|
67333
|
+
import { basename as basename2, join as join9 } from "path";
|
|
67243
67334
|
async function resolveRepoRoot(projectRoot) {
|
|
67244
67335
|
if (projectRoot)
|
|
67245
67336
|
return projectRoot;
|
|
@@ -67365,7 +67456,7 @@ async function qualifyAnchors(fs3, opts) {
|
|
|
67365
67456
|
} catch {
|
|
67366
67457
|
continue;
|
|
67367
67458
|
}
|
|
67368
|
-
const mdFiles = entries.filter((name) => name.endsWith(".md") && name !== "kanban.md").map((name) =>
|
|
67459
|
+
const mdFiles = entries.filter((name) => name.endsWith(".md") && name !== "kanban.md").map((name) => join9(dir, name)).sort();
|
|
67369
67460
|
for (const filePath of mdFiles) {
|
|
67370
67461
|
let raw;
|
|
67371
67462
|
try {
|
|
@@ -67690,6 +67781,249 @@ var init_finding_codes2 = __esm(() => {
|
|
|
67690
67781
|
init_src();
|
|
67691
67782
|
});
|
|
67692
67783
|
|
|
67784
|
+
// ../../packages/app/src/services/structural-repair.ts
|
|
67785
|
+
function canonicalOrder(domain2) {
|
|
67786
|
+
return domain2 === "task" ? TASK_CANONICAL_SECTIONS : FEATURE_CANONICAL_SECTIONS;
|
|
67787
|
+
}
|
|
67788
|
+
function splitFrontmatter(content) {
|
|
67789
|
+
const m = FM_RE.exec(content);
|
|
67790
|
+
if (m && m.index === 0) {
|
|
67791
|
+
return { frontmatter: m[0], body: content.slice(m[0].length) };
|
|
67792
|
+
}
|
|
67793
|
+
return { frontmatter: "", body: content };
|
|
67794
|
+
}
|
|
67795
|
+
function scanHeadings(body, level) {
|
|
67796
|
+
const headings = [];
|
|
67797
|
+
let inCodeBlock = false;
|
|
67798
|
+
let lineStart = 0;
|
|
67799
|
+
for (let i2 = 0;i2 <= body.length; i2++) {
|
|
67800
|
+
const atEnd = i2 === body.length;
|
|
67801
|
+
if (!atEnd && body[i2] !== `
|
|
67802
|
+
`)
|
|
67803
|
+
continue;
|
|
67804
|
+
const line = body.slice(lineStart, i2);
|
|
67805
|
+
if (line.startsWith("```")) {
|
|
67806
|
+
inCodeBlock = !inCodeBlock;
|
|
67807
|
+
} else {
|
|
67808
|
+
const m = /^(#{1,6}) (.+)$/.exec(line);
|
|
67809
|
+
if (m !== null && !inCodeBlock) {
|
|
67810
|
+
const hashes = (m[1] ?? "").length;
|
|
67811
|
+
headings.push({
|
|
67812
|
+
start: lineStart,
|
|
67813
|
+
line,
|
|
67814
|
+
hashes,
|
|
67815
|
+
name: (m[2] ?? "").trim(),
|
|
67816
|
+
atLevel: hashes === level
|
|
67817
|
+
});
|
|
67818
|
+
}
|
|
67819
|
+
}
|
|
67820
|
+
lineStart = i2 + 1;
|
|
67821
|
+
}
|
|
67822
|
+
return headings;
|
|
67823
|
+
}
|
|
67824
|
+
function requirementsMissingCheckbox(body) {
|
|
67825
|
+
const out = [];
|
|
67826
|
+
for (const [i2, line] of body.split(`
|
|
67827
|
+
`).entries()) {
|
|
67828
|
+
if (/\[[ xX]\]/.test(line))
|
|
67829
|
+
continue;
|
|
67830
|
+
const m = /^(\s*)([-*])?\s*R\d+\.?(\s.*)?$/.exec(line);
|
|
67831
|
+
if (m !== null)
|
|
67832
|
+
out.push({ index: i2, bullet: m[2] ?? "" });
|
|
67833
|
+
}
|
|
67834
|
+
return out;
|
|
67835
|
+
}
|
|
67836
|
+
function correctedHeadings(headings, level, canonical) {
|
|
67837
|
+
return headings.map((h2) => h2.atLevel || !canonical.has(h2.name) ? h2 : { start: h2.start, line: h2.line, hashes: level, name: h2.name, atLevel: true });
|
|
67838
|
+
}
|
|
67839
|
+
function structuralFindings(content, domain2) {
|
|
67840
|
+
const findings = [];
|
|
67841
|
+
const level = LEVEL[domain2];
|
|
67842
|
+
const order = canonicalOrder(domain2);
|
|
67843
|
+
const { body } = splitFrontmatter(content);
|
|
67844
|
+
const headings = scanHeadings(body, level);
|
|
67845
|
+
const canonical = new Set(order);
|
|
67846
|
+
for (const h2 of headings) {
|
|
67847
|
+
if (h2.atLevel || !canonical.has(h2.name))
|
|
67848
|
+
continue;
|
|
67849
|
+
findings.push({
|
|
67850
|
+
layer: "L2",
|
|
67851
|
+
code: FINDING_CODES.L2_HEADING_LEVEL,
|
|
67852
|
+
severity: "warning",
|
|
67853
|
+
section: h2.name,
|
|
67854
|
+
message: `Section "${h2.name}" uses ${h2.hashes} heading level; expected ${level} (${"#".repeat(level)} ${h2.name})`
|
|
67855
|
+
});
|
|
67856
|
+
}
|
|
67857
|
+
const corrected = correctedHeadings(headings, level, canonical);
|
|
67858
|
+
const atLevel = corrected.filter((h2) => h2.atLevel);
|
|
67859
|
+
const presentCanonical = atLevel.filter((h2) => canonical.has(h2.name));
|
|
67860
|
+
const allCanonical = atLevel.length === presentCanonical.length;
|
|
67861
|
+
const ranked = presentCanonical.map((h2) => order.indexOf(h2.name));
|
|
67862
|
+
const sorted = [...ranked].sort((a2, b) => a2 - b);
|
|
67863
|
+
const outOfOrder = ranked.some((r, i2) => r !== (sorted[i2] ?? -1));
|
|
67864
|
+
if (allCanonical && outOfOrder && presentCanonical.length > 1) {
|
|
67865
|
+
findings.push({
|
|
67866
|
+
layer: "L2",
|
|
67867
|
+
code: FINDING_CODES.L2_SECTION_ORDER,
|
|
67868
|
+
severity: "warning",
|
|
67869
|
+
section: "",
|
|
67870
|
+
message: `Sections are out of canonical order: ${presentCanonical.map((h2) => h2.name).join(" \u2192 ")} (expected ${order.join(" \u2192 ")})`
|
|
67871
|
+
});
|
|
67872
|
+
}
|
|
67873
|
+
const reqHeading = atLevel.find((h2) => h2.name === "Requirements");
|
|
67874
|
+
if (reqHeading !== undefined) {
|
|
67875
|
+
const bodyStart = reqHeading.start + reqHeading.line.length + 1;
|
|
67876
|
+
const next = atLevel.find((h2) => h2.start > reqHeading.start);
|
|
67877
|
+
const bodyEnd = next !== undefined ? next.start : body.length;
|
|
67878
|
+
const missing = requirementsMissingCheckbox(body.slice(bodyStart, bodyEnd));
|
|
67879
|
+
if (missing.length > 0) {
|
|
67880
|
+
findings.push({
|
|
67881
|
+
layer: "L3",
|
|
67882
|
+
code: FINDING_CODES.L3_REQUIREMENTS_CHECKBOX,
|
|
67883
|
+
severity: "warning",
|
|
67884
|
+
section: "Requirements",
|
|
67885
|
+
message: `${missing.length} R-item(s) missing the checkbox marker \u2014 write as "- [ ] R1. \u2026"`
|
|
67886
|
+
});
|
|
67887
|
+
}
|
|
67888
|
+
}
|
|
67889
|
+
return findings;
|
|
67890
|
+
}
|
|
67891
|
+
function applyStructuralRepairs(content, domain2, entry) {
|
|
67892
|
+
const repairs = [];
|
|
67893
|
+
const level = LEVEL[domain2];
|
|
67894
|
+
const order = canonicalOrder(domain2);
|
|
67895
|
+
const canonical = new Set(order);
|
|
67896
|
+
const { frontmatter, body } = splitFrontmatter(content);
|
|
67897
|
+
const headings = scanHeadings(body, level);
|
|
67898
|
+
const levelEdits = new Map;
|
|
67899
|
+
for (const h2 of headings) {
|
|
67900
|
+
if (h2.atLevel || !canonical.has(h2.name))
|
|
67901
|
+
continue;
|
|
67902
|
+
const fixed = `${"#".repeat(level)} ${h2.name}`;
|
|
67903
|
+
levelEdits.set(h2.start, fixed);
|
|
67904
|
+
repairs.push({
|
|
67905
|
+
kind: "heading-level",
|
|
67906
|
+
section: h2.name,
|
|
67907
|
+
detail: `"${h2.line.trim()}" \u2192 "${fixed.trim()}"`
|
|
67908
|
+
});
|
|
67909
|
+
}
|
|
67910
|
+
const corrected = correctedHeadings(headings, level, canonical);
|
|
67911
|
+
const atLevel = corrected.filter((h2) => h2.atLevel);
|
|
67912
|
+
const leadStart = atLevel.length > 0 ? atLevel[0]?.start ?? 0 : body.length;
|
|
67913
|
+
const lead = body.slice(0, leadStart);
|
|
67914
|
+
const blocks = [];
|
|
67915
|
+
for (let i2 = 0;i2 < atLevel.length; i2++) {
|
|
67916
|
+
const h2 = atLevel[i2];
|
|
67917
|
+
if (h2 === undefined)
|
|
67918
|
+
continue;
|
|
67919
|
+
const next = atLevel[i2 + 1];
|
|
67920
|
+
const end = next !== undefined ? next.start : body.length;
|
|
67921
|
+
const headingLine = levelEdits.get(h2.start) ?? h2.line;
|
|
67922
|
+
const bodyStart = h2.start + h2.line.length + 1;
|
|
67923
|
+
blocks.push({
|
|
67924
|
+
name: h2.name,
|
|
67925
|
+
heading: headingLine,
|
|
67926
|
+
body: body.slice(bodyStart, end),
|
|
67927
|
+
canonicalRank: order.indexOf(h2.name)
|
|
67928
|
+
});
|
|
67929
|
+
}
|
|
67930
|
+
const present = new Set(blocks.map((b) => b.name));
|
|
67931
|
+
const missingNames = (entry?.required ?? []).filter((n2) => !present.has(n2) && canonical.has(n2)).sort((a2, b) => order.indexOf(a2) - order.indexOf(b));
|
|
67932
|
+
for (const name of missingNames) {
|
|
67933
|
+
repairs.push({
|
|
67934
|
+
kind: "missing-section",
|
|
67935
|
+
section: name,
|
|
67936
|
+
detail: `inserted empty "${"#".repeat(level)} ${name}"`
|
|
67937
|
+
});
|
|
67938
|
+
}
|
|
67939
|
+
const presentCanonical = blocks.filter((b) => b.canonicalRank >= 0);
|
|
67940
|
+
const allCanonical = blocks.length === presentCanonical.length;
|
|
67941
|
+
const ranked = presentCanonical.map((b) => b.canonicalRank);
|
|
67942
|
+
const sortedRanks = [...ranked].sort((a2, b) => a2 - b);
|
|
67943
|
+
const outOfOrder = ranked.some((r, i2) => r !== (sortedRanks[i2] ?? -1));
|
|
67944
|
+
const reorder = allCanonical && outOfOrder && presentCanonical.length > 1;
|
|
67945
|
+
if (reorder) {
|
|
67946
|
+
repairs.push({
|
|
67947
|
+
kind: "section-order",
|
|
67948
|
+
section: "",
|
|
67949
|
+
detail: `reordered to ${[...presentCanonical].sort((a2, b) => a2.canonicalRank - b.canonicalRank).map((b) => b.name).join(" \u2192 ")}`
|
|
67950
|
+
});
|
|
67951
|
+
}
|
|
67952
|
+
const reqBlock = blocks.find((b) => b.name === "Requirements");
|
|
67953
|
+
let reqRepairs = [];
|
|
67954
|
+
if (reqBlock !== undefined) {
|
|
67955
|
+
reqRepairs = requirementsMissingCheckbox(reqBlock.body);
|
|
67956
|
+
if (reqRepairs.length > 0) {
|
|
67957
|
+
repairs.push({
|
|
67958
|
+
kind: "requirement-checkbox",
|
|
67959
|
+
section: "Requirements",
|
|
67960
|
+
detail: `${reqRepairs.length} R-item(s) gained the "[ ] " checkbox marker`
|
|
67961
|
+
});
|
|
67962
|
+
}
|
|
67963
|
+
}
|
|
67964
|
+
if (repairs.length === 0) {
|
|
67965
|
+
return { content, changed: false, repairs: [] };
|
|
67966
|
+
}
|
|
67967
|
+
const orderedBlocks = reorder ? [...presentCanonical].sort((a2, b) => a2.canonicalRank - b.canonicalRank) : blocks;
|
|
67968
|
+
const emitted = new Set;
|
|
67969
|
+
const rendered = [];
|
|
67970
|
+
for (const b of orderedBlocks) {
|
|
67971
|
+
emitted.add(b.name);
|
|
67972
|
+
let blockBody = b.body;
|
|
67973
|
+
if (b.name === "Requirements" && reqRepairs.length > 0) {
|
|
67974
|
+
const lines = blockBody.split(`
|
|
67975
|
+
`);
|
|
67976
|
+
for (const rr of reqRepairs) {
|
|
67977
|
+
const orig = lines[rr.index];
|
|
67978
|
+
if (orig === undefined)
|
|
67979
|
+
continue;
|
|
67980
|
+
const leadWs = /^(\s*)/.exec(orig)?.[1] ?? "";
|
|
67981
|
+
const rest = orig.slice(leadWs.length).replace(/^[-*]\s*/, "");
|
|
67982
|
+
lines[rr.index] = `${leadWs}${rr.bullet === "" ? "- [ ]" : `${rr.bullet} [ ]`} ${rest}`;
|
|
67983
|
+
}
|
|
67984
|
+
blockBody = lines.join(`
|
|
67985
|
+
`);
|
|
67986
|
+
}
|
|
67987
|
+
rendered.push(`${b.heading}
|
|
67988
|
+
${blockBody}`);
|
|
67989
|
+
}
|
|
67990
|
+
for (const b of blocks) {
|
|
67991
|
+
if (!emitted.has(b.name)) {
|
|
67992
|
+
rendered.push(`${b.heading}
|
|
67993
|
+
${b.body}`);
|
|
67994
|
+
}
|
|
67995
|
+
}
|
|
67996
|
+
if (missingNames.length > 0) {
|
|
67997
|
+
for (const name of missingNames) {
|
|
67998
|
+
const rank = order.indexOf(name);
|
|
67999
|
+
const at = rendered.findIndex((blk) => {
|
|
68000
|
+
const headingName = /^#{1,6} (.+)$/.exec(blk.split(`
|
|
68001
|
+
`)[0] ?? "")?.[1]?.trim() ?? "";
|
|
68002
|
+
return canonical.has(headingName) && order.indexOf(headingName) > rank;
|
|
68003
|
+
});
|
|
68004
|
+
const heading = `${"#".repeat(level)} ${name}
|
|
68005
|
+
`;
|
|
68006
|
+
const prev = at === -1 ? rendered[rendered.length - 1] : rendered[at - 1];
|
|
68007
|
+
const sep = prev !== undefined && !prev.endsWith(`
|
|
68008
|
+
|
|
68009
|
+
`) ? `
|
|
68010
|
+
` : "";
|
|
68011
|
+
rendered.splice(at === -1 ? rendered.length : at, 0, `${sep}${heading}
|
|
68012
|
+
`);
|
|
68013
|
+
}
|
|
68014
|
+
}
|
|
68015
|
+
const result = frontmatter + lead + rendered.join("");
|
|
68016
|
+
const changed = result !== content;
|
|
68017
|
+
return { content: result, changed, repairs };
|
|
68018
|
+
}
|
|
68019
|
+
var LEVEL, FM_RE;
|
|
68020
|
+
var init_structural_repair = __esm(() => {
|
|
68021
|
+
init_src2();
|
|
68022
|
+
init_planning_check_base();
|
|
68023
|
+
LEVEL = { task: 3, feature: 2 };
|
|
68024
|
+
FM_RE = /^---\r?\n[\s\S]*?\r?\n---\r?\n?/;
|
|
68025
|
+
});
|
|
68026
|
+
|
|
67693
68027
|
// ../../packages/app/src/services/planning-check-base.ts
|
|
67694
68028
|
function key(e) {
|
|
67695
68029
|
return `${e.kind}:${e.id}:${e.code}`;
|
|
@@ -67741,7 +68075,7 @@ class PlanningCheckService {
|
|
|
67741
68075
|
}
|
|
67742
68076
|
return doc2;
|
|
67743
68077
|
}
|
|
67744
|
-
runL2(doc2, entry, findings) {
|
|
68078
|
+
runL2(doc2, entry, findings, raw) {
|
|
67745
68079
|
if (!entry)
|
|
67746
68080
|
return;
|
|
67747
68081
|
const sectionNames = doc2.sectionNames;
|
|
@@ -67786,6 +68120,7 @@ class PlanningCheckService {
|
|
|
67786
68120
|
});
|
|
67787
68121
|
}
|
|
67788
68122
|
}
|
|
68123
|
+
findings.push(...structuralFindings(raw, this.docKind));
|
|
67789
68124
|
}
|
|
67790
68125
|
summarizeWithStatus(status, findings, strict, overrides, accepted, id) {
|
|
67791
68126
|
const effectiveFindings = [];
|
|
@@ -67830,6 +68165,7 @@ class PlanningCheckService {
|
|
|
67830
68165
|
var init_planning_check_base = __esm(() => {
|
|
67831
68166
|
init_src2();
|
|
67832
68167
|
init_finding_codes2();
|
|
68168
|
+
init_structural_repair();
|
|
67833
68169
|
});
|
|
67834
68170
|
|
|
67835
68171
|
// ../../packages/app/src/services/feature-check.ts
|
|
@@ -67841,7 +68177,7 @@ __export(exports_feature_check, {
|
|
|
67841
68177
|
FeatureCheckService: () => FeatureCheckService,
|
|
67842
68178
|
DEFAULT_FEATURE_MATRIX: () => DEFAULT_FEATURE_MATRIX
|
|
67843
68179
|
});
|
|
67844
|
-
import { dirname as dirname8, join as
|
|
68180
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
67845
68181
|
function isGroupFeature(fm) {
|
|
67846
68182
|
const tags = fm.tags;
|
|
67847
68183
|
return Array.isArray(tags) && tags.includes("group") ? "group" : "standard";
|
|
@@ -67939,9 +68275,9 @@ function decodeVerdictRows(source, sectionName, required2) {
|
|
|
67939
68275
|
function defaultVerdictRunDir(tasksDir) {
|
|
67940
68276
|
const norm = tasksDir.replace(/\\/g, "/");
|
|
67941
68277
|
if (/\/docs\/tasks\d*$/.test(norm) || /\/docs\/tasks$/.test(norm)) {
|
|
67942
|
-
return
|
|
68278
|
+
return join10(dirname8(dirname8(tasksDir)), ".spur", "run");
|
|
67943
68279
|
}
|
|
67944
|
-
return
|
|
68280
|
+
return join10(dirname8(tasksDir), ".spur", "run");
|
|
67945
68281
|
}
|
|
67946
68282
|
function rowMatchesScenario(id, sc) {
|
|
67947
68283
|
const stripped = id.replace(/^\[[^\]]*\]\s*/, "").replace(/^Scenario:\s*/i, "").replace(/^\[[^\]]*\]\s*/, "").trim();
|
|
@@ -67963,6 +68299,7 @@ var init_feature_check = __esm(() => {
|
|
|
67963
68299
|
init_src2();
|
|
67964
68300
|
init_done_transition_guard();
|
|
67965
68301
|
init_planning_check_base();
|
|
68302
|
+
init_structural_repair();
|
|
67966
68303
|
DEFAULT_FEATURE_MATRIX = {
|
|
67967
68304
|
variants: {
|
|
67968
68305
|
standard: {
|
|
@@ -68037,7 +68374,22 @@ var init_feature_check = __esm(() => {
|
|
|
68037
68374
|
}
|
|
68038
68375
|
async check(filePath, featureId2, options) {
|
|
68039
68376
|
const strict = options?.strict === true;
|
|
68040
|
-
const
|
|
68377
|
+
const rawSource = await this.fs.readFile(filePath);
|
|
68378
|
+
let raw = rawSource;
|
|
68379
|
+
let repairs = [];
|
|
68380
|
+
if (options?.fix === true) {
|
|
68381
|
+
const probe = MarkdownDocument.parse(rawSource, "feature");
|
|
68382
|
+
const probeFm = probe.frontmatterData ?? {};
|
|
68383
|
+
const probeStatus = probeFm.status ?? "backlog";
|
|
68384
|
+
const probeEffective = options?.asStatus ?? probeStatus;
|
|
68385
|
+
const fixEntry = this.resolveMatrixEntry(isGroupFeature(probeFm), probeEffective);
|
|
68386
|
+
const fixed = applyStructuralRepairs(rawSource, "feature", fixEntry);
|
|
68387
|
+
if (fixed.changed) {
|
|
68388
|
+
await this.fs.writeFile(filePath, fixed.content);
|
|
68389
|
+
repairs = fixed.repairs;
|
|
68390
|
+
raw = fixed.content;
|
|
68391
|
+
}
|
|
68392
|
+
}
|
|
68041
68393
|
const findings = [];
|
|
68042
68394
|
const doc2 = this.runL1(raw, featureId2, findings);
|
|
68043
68395
|
if (doc2 === null) {
|
|
@@ -68046,18 +68398,22 @@ var init_feature_check = __esm(() => {
|
|
|
68046
68398
|
const fm = doc2.frontmatterData ?? {};
|
|
68047
68399
|
const status = fm.status ?? "backlog";
|
|
68048
68400
|
const entry = this.resolveMatrixEntry(isGroupFeature(fm), status);
|
|
68049
|
-
this.runL2(doc2, entry, findings);
|
|
68401
|
+
this.runL2(doc2, entry, findings, raw);
|
|
68050
68402
|
this.runL3(doc2, findings, fm);
|
|
68051
68403
|
if (options?.featuresDir) {
|
|
68052
68404
|
await this.checkOneActiveGoal(fm, featureId2, options.featuresDir, findings, options.asStatus);
|
|
68053
68405
|
await this.checkChildrenLimit(featureId2, options.featuresDir, findings);
|
|
68054
68406
|
}
|
|
68055
|
-
const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ?
|
|
68407
|
+
const dogfoodDir = options?.dogfoodDir ?? (options?.featuresDir ? join10(dirname8(options.featuresDir), "dogfood") : undefined);
|
|
68056
68408
|
const primaryTasksDir = options?.tasksDir ?? options?.tasksDirs?.[0];
|
|
68057
68409
|
const runDir = options?.runDir ?? (primaryTasksDir ? defaultVerdictRunDir(primaryTasksDir) : undefined);
|
|
68058
68410
|
const taskScanDirs = options?.tasksDirs && options.tasksDirs.length > 0 ? options.tasksDirs : options?.tasksDir ? [options.tasksDir] : [];
|
|
68059
68411
|
await this.runL4(doc2, featureId2, status, taskScanDirs, dogfoodDir, runDir, findings);
|
|
68060
|
-
return {
|
|
68412
|
+
return {
|
|
68413
|
+
id: featureId2,
|
|
68414
|
+
...this.summarizeWithStatus(status, findings, strict, options?.severityOverrides),
|
|
68415
|
+
repairs
|
|
68416
|
+
};
|
|
68061
68417
|
}
|
|
68062
68418
|
runL3(doc2, findings, fm) {
|
|
68063
68419
|
const rawAc = doc2.getSection("Acceptance Criteria");
|
|
@@ -68494,7 +68850,7 @@ var init_task_locator = __esm(() => {
|
|
|
68494
68850
|
});
|
|
68495
68851
|
|
|
68496
68852
|
// ../../packages/app/src/services/task-check.ts
|
|
68497
|
-
import { basename as basename3, dirname as dirname9, join as
|
|
68853
|
+
import { basename as basename3, dirname as dirname9, join as join11 } from "path";
|
|
68498
68854
|
function isPlaceholderBody(body) {
|
|
68499
68855
|
const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
|
|
68500
68856
|
return stripped.length === 0;
|
|
@@ -68696,6 +69052,7 @@ var EXTERNAL_EVIDENCE_RE, TaskCheckService;
|
|
|
68696
69052
|
var init_task_check = __esm(() => {
|
|
68697
69053
|
init_src2();
|
|
68698
69054
|
init_planning_check_base();
|
|
69055
|
+
init_structural_repair();
|
|
68699
69056
|
init_task_locator();
|
|
68700
69057
|
init_verify_verdict();
|
|
68701
69058
|
EXTERNAL_EVIDENCE_RE = /`([^`\n]+?)`\s+(?:line|lines?)\s+(\d+)(?:-(\d+))?/g;
|
|
@@ -68719,7 +69076,23 @@ var init_task_check = __esm(() => {
|
|
|
68719
69076
|
}
|
|
68720
69077
|
async check(filePath, wbs, options) {
|
|
68721
69078
|
const strict = options?.strict === true;
|
|
68722
|
-
const
|
|
69079
|
+
const rawSource = await this.fs.readFile(filePath);
|
|
69080
|
+
let raw = rawSource;
|
|
69081
|
+
let repairs = [];
|
|
69082
|
+
if (options?.fix === true) {
|
|
69083
|
+
const probe = MarkdownDocument.parse(rawSource, "task");
|
|
69084
|
+
const probeFm = probe.frontmatterData ?? {};
|
|
69085
|
+
const probeStatus = probeFm.status ?? "backlog";
|
|
69086
|
+
const probeEffective = options?.asStatus ?? probeStatus;
|
|
69087
|
+
const probeVariant = probeFm.template ?? DEFAULT_TASK_VARIANT;
|
|
69088
|
+
const fixEntry = this.resolveMatrixEntry(probeVariant, probeEffective);
|
|
69089
|
+
const fixed = applyStructuralRepairs(rawSource, "task", fixEntry);
|
|
69090
|
+
if (fixed.changed) {
|
|
69091
|
+
await this.fs.writeFile(filePath, fixed.content);
|
|
69092
|
+
repairs = fixed.repairs;
|
|
69093
|
+
raw = fixed.content;
|
|
69094
|
+
}
|
|
69095
|
+
}
|
|
68723
69096
|
const findings = [];
|
|
68724
69097
|
const doc2 = this.runL1(raw, wbs, findings);
|
|
68725
69098
|
if (doc2 === null) {
|
|
@@ -68733,10 +69106,10 @@ var init_task_check = __esm(() => {
|
|
|
68733
69106
|
const effectiveStatus = options?.asStatus ?? status;
|
|
68734
69107
|
const variant = fm.template ?? DEFAULT_TASK_VARIANT;
|
|
68735
69108
|
const entry = this.resolveMatrixEntry(variant, effectiveStatus);
|
|
68736
|
-
this.runL2(doc2, entry, findings);
|
|
69109
|
+
this.runL2(doc2, entry, findings, raw);
|
|
68737
69110
|
this.runL3(doc2, entry, effectiveStatus, findings);
|
|
68738
69111
|
const tasksDir = dirname9(filePath);
|
|
68739
|
-
const featuresDir =
|
|
69112
|
+
const featuresDir = join11(dirname9(tasksDir), "features");
|
|
68740
69113
|
await this.runL4(doc2, fm, effectiveStatus, findings, featuresDir, tasksDir, wbs);
|
|
68741
69114
|
await this.runL4Rollup(doc2, wbs, effectiveStatus, findings, tasksDir);
|
|
68742
69115
|
if (effectiveStatus !== "done" && effectiveStatus !== "cancelled") {
|
|
@@ -68744,7 +69117,8 @@ var init_task_check = __esm(() => {
|
|
|
68744
69117
|
}
|
|
68745
69118
|
return {
|
|
68746
69119
|
wbs,
|
|
68747
|
-
...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs)
|
|
69120
|
+
...this.summarizeWithStatus(effectiveStatus, findings, strict, options?.severityOverrides, options?.accepted, wbs),
|
|
69121
|
+
repairs
|
|
68748
69122
|
};
|
|
68749
69123
|
}
|
|
68750
69124
|
runL3(doc2, entry, status, findings) {
|
|
@@ -69171,7 +69545,7 @@ var init_task_check = __esm(() => {
|
|
|
69171
69545
|
const body = doc2.getSection(section);
|
|
69172
69546
|
if (body === null)
|
|
69173
69547
|
continue;
|
|
69174
|
-
if (
|
|
69548
|
+
if (/(?<![\w-])(HITL|human[- ]in[- ]the-loop|approval|approved|merge event|merged|content-gate|GATED|capstone)(?![\w-])/i.test(body)) {
|
|
69175
69549
|
findings.push({
|
|
69176
69550
|
layer: "L4",
|
|
69177
69551
|
code: FINDING_CODES.L4_GATE_LANGUAGE,
|
|
@@ -69227,7 +69601,7 @@ var init_task_check = __esm(() => {
|
|
|
69227
69601
|
for (const cite of citations) {
|
|
69228
69602
|
if (reported >= 5)
|
|
69229
69603
|
break;
|
|
69230
|
-
const abs =
|
|
69604
|
+
const abs = join11(projectRoot, cite.path);
|
|
69231
69605
|
let exists2 = false;
|
|
69232
69606
|
try {
|
|
69233
69607
|
exists2 = await this.fs.exists(abs);
|
|
@@ -69309,7 +69683,7 @@ var init_task_check = __esm(() => {
|
|
|
69309
69683
|
continue;
|
|
69310
69684
|
}
|
|
69311
69685
|
try {
|
|
69312
|
-
const abs =
|
|
69686
|
+
const abs = join11(dir, name);
|
|
69313
69687
|
const stat = await this.fs.stat(abs);
|
|
69314
69688
|
if (stat !== null && stat !== undefined && stat.isDirectory?.())
|
|
69315
69689
|
stack.push(abs);
|
|
@@ -69404,7 +69778,7 @@ var init_task_check = __esm(() => {
|
|
|
69404
69778
|
}
|
|
69405
69779
|
async checkVerdictArtifact(wbs, tasksDir, findings) {
|
|
69406
69780
|
const projectRoot = resolveProjectRootFromTasksDir(tasksDir);
|
|
69407
|
-
const runDir =
|
|
69781
|
+
const runDir = join11(projectRoot, ".spur", "run");
|
|
69408
69782
|
const verdictPath = `${runDir}/${wbs}-verdict.json`;
|
|
69409
69783
|
const outcome = await readVerifyVerdict(this.fs, verdictPath, wbs);
|
|
69410
69784
|
if (outcome.kind === "missing")
|
|
@@ -69449,7 +69823,7 @@ var init_task_check = __esm(() => {
|
|
|
69449
69823
|
});
|
|
69450
69824
|
|
|
69451
69825
|
// ../../packages/app/src/services/corpus-check.ts
|
|
69452
|
-
import { basename as basename4, dirname as dirname10, join as
|
|
69826
|
+
import { basename as basename4, dirname as dirname10, join as join12, relative, resolve as resolve3 } from "path";
|
|
69453
69827
|
function baselineSeverity(e) {
|
|
69454
69828
|
return e.severity ?? "error";
|
|
69455
69829
|
}
|
|
@@ -69457,7 +69831,7 @@ function resolveProjectRoot(cwd) {
|
|
|
69457
69831
|
const fs3 = createNodeFileSystem3(cwd);
|
|
69458
69832
|
let current = resolve3(cwd);
|
|
69459
69833
|
while (true) {
|
|
69460
|
-
if (fs3.exists(
|
|
69834
|
+
if (fs3.exists(join12(current, ".spur", "config.yaml")) || fs3.exists(join12(current, "config", "corpus-baseline.json"))) {
|
|
69461
69835
|
return current;
|
|
69462
69836
|
}
|
|
69463
69837
|
const parent = dirname10(current);
|
|
@@ -69468,10 +69842,10 @@ function resolveProjectRoot(cwd) {
|
|
|
69468
69842
|
}
|
|
69469
69843
|
async function loadTaskMatrix(projectRoot) {
|
|
69470
69844
|
const fs3 = createNodeFileSystem3(projectRoot);
|
|
69471
|
-
const candidates = [
|
|
69845
|
+
const candidates = [join12(projectRoot, ".spur", "tasks", "section-matrix.yaml")];
|
|
69472
69846
|
const bundledRoot = bundledConfigRoot();
|
|
69473
69847
|
if (bundledRoot !== null)
|
|
69474
|
-
candidates.push(
|
|
69848
|
+
candidates.push(join12(bundledRoot, "tasks", "section-matrix.yaml"));
|
|
69475
69849
|
let matrixPath;
|
|
69476
69850
|
for (const candidate of candidates) {
|
|
69477
69851
|
if (await fs3.exists(candidate)) {
|
|
@@ -69514,7 +69888,7 @@ async function structuralSweep(projectRoot) {
|
|
|
69514
69888
|
const wbs = fileName.match(/^(\d{4})_.+\.md$/)?.[1];
|
|
69515
69889
|
if (wbs === undefined)
|
|
69516
69890
|
continue;
|
|
69517
|
-
const result = await taskService.check(
|
|
69891
|
+
const result = await taskService.check(join12(tasksDir, fileName), wbs, {
|
|
69518
69892
|
severityOverrides: planning.severityOverrides
|
|
69519
69893
|
});
|
|
69520
69894
|
for (const finding of result.findings) {
|
|
@@ -69534,7 +69908,7 @@ async function structuralSweep(projectRoot) {
|
|
|
69534
69908
|
const id = fileName.match(/^([A-Z][1-9]*)_.+\.md$/)?.[1];
|
|
69535
69909
|
if (id === undefined)
|
|
69536
69910
|
continue;
|
|
69537
|
-
const result = await featureService.check(
|
|
69911
|
+
const result = await featureService.check(join12(featuresDir, fileName), id, {
|
|
69538
69912
|
featuresDir,
|
|
69539
69913
|
tasksDir: activeTasksDir,
|
|
69540
69914
|
tasksDirs: taskDirs,
|
|
@@ -69564,7 +69938,7 @@ async function duplicateIds(cwd, taskDirs, featuresDir) {
|
|
|
69564
69938
|
return [];
|
|
69565
69939
|
}
|
|
69566
69940
|
const pattern = kind === "task" ? /^(\d{4})_/ : /^([A-Z][0-9]*)_/;
|
|
69567
|
-
return names.map((n2) => ({ m: n2.match(pattern), n: n2 })).filter((x) => x.m !== null).map((x) => ({ id: x.m[1], file: relative(cwd,
|
|
69941
|
+
return names.map((n2) => ({ m: n2.match(pattern), n: n2 })).filter((x) => x.m !== null).map((x) => ({ id: x.m[1], file: relative(cwd, join12(dir, x.n)), kind }));
|
|
69568
69942
|
};
|
|
69569
69943
|
const all = [
|
|
69570
69944
|
...(await Promise.all(taskDirs.map((dir) => scan(dir, "task")))).flat(),
|
|
@@ -69603,14 +69977,14 @@ function diskReader(cwd) {
|
|
|
69603
69977
|
return {
|
|
69604
69978
|
list: async (dir) => {
|
|
69605
69979
|
try {
|
|
69606
|
-
return (await fs3.readDir(
|
|
69980
|
+
return (await fs3.readDir(join12(cwd, dir))).map((n2) => `${dir}/${n2}`);
|
|
69607
69981
|
} catch {
|
|
69608
69982
|
return [];
|
|
69609
69983
|
}
|
|
69610
69984
|
},
|
|
69611
69985
|
read: async (path9) => {
|
|
69612
69986
|
try {
|
|
69613
|
-
return await fs3.readFile(
|
|
69987
|
+
return await fs3.readFile(join12(cwd, path9));
|
|
69614
69988
|
} catch {
|
|
69615
69989
|
return null;
|
|
69616
69990
|
}
|
|
@@ -69802,7 +70176,7 @@ function reconcileBaseline(observed, baseline) {
|
|
|
69802
70176
|
}
|
|
69803
70177
|
async function runCorpusCheck(cwd, since) {
|
|
69804
70178
|
const projectRoot = resolveProjectRoot(cwd);
|
|
69805
|
-
const baselineFile =
|
|
70179
|
+
const baselineFile = join12(projectRoot, "config", "corpus-baseline.json");
|
|
69806
70180
|
let baseline = { entries: [] };
|
|
69807
70181
|
if (await Bun.file(baselineFile).exists()) {
|
|
69808
70182
|
baseline = await Bun.file(baselineFile).json();
|
|
@@ -69822,7 +70196,7 @@ async function runCorpusCheck(cwd, since) {
|
|
|
69822
70196
|
}
|
|
69823
70197
|
async function loadAcceptedFindings(cwd) {
|
|
69824
70198
|
const projectRoot = resolveProjectRoot(cwd);
|
|
69825
|
-
const baselineFile =
|
|
70199
|
+
const baselineFile = join12(projectRoot, "config", "corpus-baseline.json");
|
|
69826
70200
|
const accepted = new Map;
|
|
69827
70201
|
try {
|
|
69828
70202
|
if (await Bun.file(baselineFile).exists()) {
|
|
@@ -69852,7 +70226,7 @@ var init_corpus_check = __esm(() => {
|
|
|
69852
70226
|
});
|
|
69853
70227
|
|
|
69854
70228
|
// ../../packages/app/src/services/corpus-migrator.ts
|
|
69855
|
-
import { join as
|
|
70229
|
+
import { join as join13, relative as relative2 } from "path";
|
|
69856
70230
|
function applyM1(data, flags) {
|
|
69857
70231
|
const raw = data.status;
|
|
69858
70232
|
if (typeof raw !== "string")
|
|
@@ -70152,7 +70526,7 @@ class CorpusMigrator {
|
|
|
70152
70526
|
}
|
|
70153
70527
|
async discoverFiles() {
|
|
70154
70528
|
const entries = await this.fs.readDir(this.corpusDir);
|
|
70155
|
-
return entries.filter((name) => name.endsWith(".md")).filter((name) => name !== "kanban.md").map((name) =>
|
|
70529
|
+
return entries.filter((name) => name.endsWith(".md")).filter((name) => name !== "kanban.md").map((name) => join13(this.corpusDir, name)).sort();
|
|
70156
70530
|
}
|
|
70157
70531
|
async migrateFile(filePath, dryRun, gitDates = new Map) {
|
|
70158
70532
|
const flags = [];
|
|
@@ -73000,15 +73374,15 @@ var init_feature_service = __esm(() => {
|
|
|
73000
73374
|
import {
|
|
73001
73375
|
existsSync as existsSync5,
|
|
73002
73376
|
mkdirSync as mkdirSync6,
|
|
73003
|
-
readdirSync as
|
|
73377
|
+
readdirSync as readdirSync6,
|
|
73004
73378
|
readFileSync as readFileSync8,
|
|
73005
73379
|
readlinkSync,
|
|
73006
|
-
rmSync as
|
|
73380
|
+
rmSync as rmSync4,
|
|
73007
73381
|
symlinkSync,
|
|
73008
73382
|
unlinkSync as unlinkSync2,
|
|
73009
73383
|
writeFileSync as writeFileSync4
|
|
73010
73384
|
} from "fs";
|
|
73011
|
-
import { dirname as dirname11, isAbsolute as isAbsolute3, join as
|
|
73385
|
+
import { dirname as dirname11, isAbsolute as isAbsolute3, join as join14, resolve as resolve4 } from "path";
|
|
73012
73386
|
async function buildRefreshCoverage(db2, selector, fanOut) {
|
|
73013
73387
|
const statusBySource = new Map(fanOut.entries.map((e) => [e.source, e.status]));
|
|
73014
73388
|
const refreshed = FULL_FIDELITY_SOURCES.filter((s3) => statusBySource.has(s3) && statusBySource.get(s3) !== "failed");
|
|
@@ -73157,8 +73531,9 @@ class HistoryService {
|
|
|
73157
73531
|
const timeoutMs = opts.sourceTimeout ?? DEFAULT_SOURCE_TIMEOUT_MS;
|
|
73158
73532
|
const entries = [];
|
|
73159
73533
|
const warnings = [];
|
|
73534
|
+
const runStartedAt = new Date().toISOString();
|
|
73160
73535
|
for (const source of sources) {
|
|
73161
|
-
const { coverageEntry, sourceWarnings } = await this.importOneIsolated(source, opts, timeoutMs);
|
|
73536
|
+
const { coverageEntry, sourceWarnings } = await this.importOneIsolated(source, opts, timeoutMs, runStartedAt);
|
|
73162
73537
|
entries.push(coverageEntry);
|
|
73163
73538
|
warnings.push(...sourceWarnings);
|
|
73164
73539
|
}
|
|
@@ -73193,10 +73568,11 @@ class HistoryService {
|
|
|
73193
73568
|
writeFileSync4(reportPath, renderMarkdown(artifact, opts.mode));
|
|
73194
73569
|
}
|
|
73195
73570
|
const pruned = pruneReports(cwd, REPORT_RETENTION_DAYS);
|
|
73571
|
+
const retained = await runRetention(db2, cwd);
|
|
73196
73572
|
const coverage = await buildRefreshCoverage(db2, selector, fanOut);
|
|
73197
|
-
return { fanOut, artifact, pruned, coverage, ...reportPath !== undefined ? { reportPath } : {} };
|
|
73573
|
+
return { fanOut, artifact, pruned, retained, coverage, ...reportPath !== undefined ? { reportPath } : {} };
|
|
73198
73574
|
}
|
|
73199
|
-
async importOneIsolated(source, opts, timeoutMs) {
|
|
73575
|
+
async importOneIsolated(source, opts, timeoutMs, runStartedAt) {
|
|
73200
73576
|
const db2 = await this.ctx.getDb();
|
|
73201
73577
|
const sourceWarnings = [];
|
|
73202
73578
|
const checkpointCount = await countCheckpointsBySource(db2, source);
|
|
@@ -73235,12 +73611,13 @@ class HistoryService {
|
|
|
73235
73611
|
detail: `source '${source}' previously had checkpoint rows but discovered 0 files`
|
|
73236
73612
|
});
|
|
73237
73613
|
}
|
|
73614
|
+
const toolCalls = opts.dryRun ? 0 : await countToolCallsSince(db2, source, runStartedAt);
|
|
73238
73615
|
const coverageEntry = {
|
|
73239
73616
|
source,
|
|
73240
73617
|
status,
|
|
73241
73618
|
files: result.scannedFiles,
|
|
73242
73619
|
messages: result.importedRecords,
|
|
73243
|
-
toolCalls
|
|
73620
|
+
toolCalls,
|
|
73244
73621
|
unknownRecords: result.unknownRecords,
|
|
73245
73622
|
lastImportedAt: null,
|
|
73246
73623
|
parseErrors: result.parseErrors.length,
|
|
@@ -73292,7 +73669,7 @@ function emptyTotals2() {
|
|
|
73292
73669
|
function foldMessage(bucket, row) {
|
|
73293
73670
|
bucket.messages += row.messages;
|
|
73294
73671
|
bucket.records += row.messages;
|
|
73295
|
-
bucket.inputTokens += row.inputTokens ?? 0;
|
|
73672
|
+
bucket.inputTokens += (row.inputTokens ?? 0) + (row.cacheReadTokens ?? 0) + (row.cacheWriteTokens ?? 0);
|
|
73296
73673
|
bucket.outputTokens += row.outputTokens ?? 0;
|
|
73297
73674
|
bucket.cacheReadTokens += row.cacheReadTokens ?? 0;
|
|
73298
73675
|
bucket.cacheWriteTokens += row.cacheWriteTokens ?? 0;
|
|
@@ -73440,9 +73817,9 @@ function resolveArtifactPaths(artifact, opts) {
|
|
|
73440
73817
|
return { artifactPath: out, sidecarPath: `${out.replace(/\.json$/, "")}.errors.jsonl` };
|
|
73441
73818
|
}
|
|
73442
73819
|
const base2 = resolve4(opts.cwd, ".spur", "reports", "history");
|
|
73443
|
-
const dateDir =
|
|
73820
|
+
const dateDir = join14(base2, artifact.generatedAt.slice(0, 10));
|
|
73444
73821
|
const name = `analyze-${selectorDigest(artifact.selector)}`;
|
|
73445
|
-
return { artifactPath:
|
|
73822
|
+
return { artifactPath: join14(dateDir, `${name}.json`), sidecarPath: join14(dateDir, `${name}.errors.jsonl`) };
|
|
73446
73823
|
}
|
|
73447
73824
|
function boundCoverage(coverage) {
|
|
73448
73825
|
const overflow = [];
|
|
@@ -73465,7 +73842,7 @@ function boundCoverage(coverage) {
|
|
|
73465
73842
|
}
|
|
73466
73843
|
function updateLatestPointer(opts, artifactPath) {
|
|
73467
73844
|
const base2 = resolve4(opts.cwd, ".spur", "reports", "history");
|
|
73468
|
-
const latest =
|
|
73845
|
+
const latest = join14(base2, "latest.json");
|
|
73469
73846
|
mkdirSync6(base2, { recursive: true });
|
|
73470
73847
|
if (existsSync5(latest))
|
|
73471
73848
|
unlinkSync2(latest);
|
|
@@ -73531,10 +73908,10 @@ function formatIssue(issue2) {
|
|
|
73531
73908
|
return `${issue2.sourceFile}:${issue2.sourceLine}: ${issue2.reason}`;
|
|
73532
73909
|
}
|
|
73533
73910
|
function pruneReports(cwd, retentionDays = REPORT_RETENTION_DAYS, now2 = new Date) {
|
|
73534
|
-
const dir =
|
|
73911
|
+
const dir = join14(cwd, ".spur", "reports", "history");
|
|
73535
73912
|
let entries;
|
|
73536
73913
|
try {
|
|
73537
|
-
entries =
|
|
73914
|
+
entries = readdirSync6(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
73538
73915
|
} catch {
|
|
73539
73916
|
return [];
|
|
73540
73917
|
}
|
|
@@ -73549,7 +73926,7 @@ function pruneReports(cwd, retentionDays = REPORT_RETENTION_DAYS, now2 = new Dat
|
|
|
73549
73926
|
if (entryDate >= cutoff)
|
|
73550
73927
|
continue;
|
|
73551
73928
|
try {
|
|
73552
|
-
|
|
73929
|
+
rmSync4(join14(dir, name), { recursive: true, force: true });
|
|
73553
73930
|
pruned.push(name);
|
|
73554
73931
|
} catch {}
|
|
73555
73932
|
}
|
|
@@ -73880,6 +74257,16 @@ async function ensurePipelineRunLink(db2, wbs, options = {}) {
|
|
|
73880
74257
|
const links = await dao2.listByWbs(wbs, 20);
|
|
73881
74258
|
const existing = links.find((l) => l.kind === "pipeline");
|
|
73882
74259
|
if (existing) {
|
|
74260
|
+
if (options.runId !== undefined && options.runId !== existing.run_id) {
|
|
74261
|
+
await dao2.updateRunId(existing.id, options.runId);
|
|
74262
|
+
return {
|
|
74263
|
+
created: false,
|
|
74264
|
+
id: existing.id,
|
|
74265
|
+
wbs: existing.wbs,
|
|
74266
|
+
runId: options.runId,
|
|
74267
|
+
kind: "pipeline"
|
|
74268
|
+
};
|
|
74269
|
+
}
|
|
73883
74270
|
return {
|
|
73884
74271
|
created: false,
|
|
73885
74272
|
id: existing.id,
|
|
@@ -74376,17 +74763,17 @@ var init_process_inventory_service = __esm(() => {
|
|
|
74376
74763
|
});
|
|
74377
74764
|
|
|
74378
74765
|
// ../../packages/app/src/services/project-registry.ts
|
|
74379
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as
|
|
74766
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
74380
74767
|
import { connect, createServer } from "net";
|
|
74381
74768
|
import { homedir as homedir5 } from "os";
|
|
74382
|
-
import { dirname as dirname12, join as
|
|
74769
|
+
import { dirname as dirname12, join as join15, resolve as resolve5 } from "path";
|
|
74383
74770
|
function normalizeProjectPath(pathInput) {
|
|
74384
74771
|
const trimmed = pathInput.trim();
|
|
74385
74772
|
let expanded = trimmed;
|
|
74386
74773
|
if (trimmed === "~") {
|
|
74387
74774
|
expanded = homedir5();
|
|
74388
74775
|
} else if (trimmed.startsWith("~/")) {
|
|
74389
|
-
expanded =
|
|
74776
|
+
expanded = join15(homedir5(), trimmed.slice(2));
|
|
74390
74777
|
} else {
|
|
74391
74778
|
expanded = resolve5(trimmed);
|
|
74392
74779
|
}
|
|
@@ -74509,7 +74896,7 @@ class ProjectRegistry {
|
|
|
74509
74896
|
}
|
|
74510
74897
|
if (!acquired) {
|
|
74511
74898
|
try {
|
|
74512
|
-
|
|
74899
|
+
rmSync5(this.lockDir, { recursive: true, force: true });
|
|
74513
74900
|
mkdirSync7(this.lockDir);
|
|
74514
74901
|
acquired = true;
|
|
74515
74902
|
} catch {
|
|
@@ -74521,7 +74908,7 @@ class ProjectRegistry {
|
|
|
74521
74908
|
} finally {
|
|
74522
74909
|
if (acquired) {
|
|
74523
74910
|
try {
|
|
74524
|
-
|
|
74911
|
+
rmSync5(this.lockDir, { recursive: true, force: true });
|
|
74525
74912
|
} catch {}
|
|
74526
74913
|
}
|
|
74527
74914
|
}
|
|
@@ -74826,7 +75213,7 @@ var init_project_start = __esm(() => {
|
|
|
74826
75213
|
|
|
74827
75214
|
// ../../packages/app/src/services/rule-service.ts
|
|
74828
75215
|
import { homedir as homedir6 } from "os";
|
|
74829
|
-
import { delimiter, join as
|
|
75216
|
+
import { delimiter, join as join16, relative as relative3, resolve as resolve7 } from "path";
|
|
74830
75217
|
|
|
74831
75218
|
class RuleService {
|
|
74832
75219
|
context;
|
|
@@ -75021,13 +75408,13 @@ ${ruleIds.join(`
|
|
|
75021
75408
|
const hasGlobalOverride = globalOverride !== undefined && globalOverride.length > 0;
|
|
75022
75409
|
layers.push({
|
|
75023
75410
|
id: "global",
|
|
75024
|
-
path: hasGlobalOverride ? resolve7(cwd, globalOverride) :
|
|
75411
|
+
path: hasGlobalOverride ? resolve7(cwd, globalOverride) : join16(homedir6(), GLOBAL_RULES_DIR),
|
|
75025
75412
|
priority: 10
|
|
75026
75413
|
});
|
|
75027
75414
|
if (opts.includeBundled && !hasGlobalOverride) {
|
|
75028
75415
|
const bundledConfig = bundledConfigRoot();
|
|
75029
75416
|
if (bundledConfig !== null) {
|
|
75030
|
-
layers.push({ id: "bundled-config", path:
|
|
75417
|
+
layers.push({ id: "bundled-config", path: join16(bundledConfig, "rules"), priority: 15 });
|
|
75031
75418
|
}
|
|
75032
75419
|
const bundled = await bundledRulesRoot();
|
|
75033
75420
|
if (bundled !== null)
|
|
@@ -75240,7 +75627,7 @@ ${ruleIds.join(`
|
|
|
75240
75627
|
const { fs: fs3 } = this.context;
|
|
75241
75628
|
for (const root of await this.ruleRoots()) {
|
|
75242
75629
|
for (const ext of ["yaml", "yml", "json"]) {
|
|
75243
|
-
if (await fs3.exists(
|
|
75630
|
+
if (await fs3.exists(join16(root, `${name}.${ext}`)))
|
|
75244
75631
|
return true;
|
|
75245
75632
|
}
|
|
75246
75633
|
}
|
|
@@ -75276,7 +75663,7 @@ ${ruleIds.join(`
|
|
|
75276
75663
|
for (const layer of await this.existingRuleSourceLayers()) {
|
|
75277
75664
|
const entries = await this.context.fs.readDir(layer.path);
|
|
75278
75665
|
const named = entries.map((entry) => ({ entry, name: entry.match(/^([\w-]+)\.(?:ya?ml|json)$/i)?.[1] ?? "" })).filter(({ name }) => name.length > 0);
|
|
75279
|
-
const stats = await Promise.all(named.map(({ entry }) => this.context.fs.stat(
|
|
75666
|
+
const stats = await Promise.all(named.map(({ entry }) => this.context.fs.stat(join16(layer.path, entry))));
|
|
75280
75667
|
named.forEach(({ name }, i2) => {
|
|
75281
75668
|
if (stats[i2]?.isFile())
|
|
75282
75669
|
candidates.add(name);
|
|
@@ -75298,7 +75685,7 @@ ${ruleIds.join(`
|
|
|
75298
75685
|
for (const layer of layers) {
|
|
75299
75686
|
for (const file2 of await this.listRuleFilesInLayer(layer.path)) {
|
|
75300
75687
|
if (!merged.has(file2))
|
|
75301
|
-
merged.set(file2, { absolutePath:
|
|
75688
|
+
merged.set(file2, { absolutePath: join16(layer.path, file2), source: layer.id });
|
|
75302
75689
|
}
|
|
75303
75690
|
}
|
|
75304
75691
|
const files = [];
|
|
@@ -75366,7 +75753,7 @@ ${ruleIds.join(`
|
|
|
75366
75753
|
for (const entry of entries.sort()) {
|
|
75367
75754
|
if (entry === "presets")
|
|
75368
75755
|
continue;
|
|
75369
|
-
const stat = await this.context.fs.stat(
|
|
75756
|
+
const stat = await this.context.fs.stat(join16(root, entry));
|
|
75370
75757
|
if (stat?.isDirectory())
|
|
75371
75758
|
dirs.push(entry);
|
|
75372
75759
|
}
|
|
@@ -75374,12 +75761,12 @@ ${ruleIds.join(`
|
|
|
75374
75761
|
}
|
|
75375
75762
|
async listRuleFiles(root, relativeDir) {
|
|
75376
75763
|
const { fs: fs3 } = this.context;
|
|
75377
|
-
const dir =
|
|
75764
|
+
const dir = join16(root, relativeDir);
|
|
75378
75765
|
const entries = await fs3.readDir(dir);
|
|
75379
75766
|
const files = [];
|
|
75380
75767
|
for (const entry of entries.sort()) {
|
|
75381
|
-
const relativePath2 = relativeDir.length === 0 ? entry :
|
|
75382
|
-
const absolutePath =
|
|
75768
|
+
const relativePath2 = relativeDir.length === 0 ? entry : join16(relativeDir, entry);
|
|
75769
|
+
const absolutePath = join16(root, relativePath2);
|
|
75383
75770
|
const stat = await fs3.stat(absolutePath);
|
|
75384
75771
|
if (stat?.isDirectory()) {
|
|
75385
75772
|
files.push(...await this.listRuleFiles(root, relativePath2));
|
|
@@ -75513,8 +75900,8 @@ var init_rule_service = __esm(() => {
|
|
|
75513
75900
|
init_dist10();
|
|
75514
75901
|
init_agent_execution();
|
|
75515
75902
|
init_system_event_envelope();
|
|
75516
|
-
LOCAL_RULES_DIR =
|
|
75517
|
-
GLOBAL_RULES_DIR =
|
|
75903
|
+
LOCAL_RULES_DIR = join16(".spur", "rules");
|
|
75904
|
+
GLOBAL_RULES_DIR = join16(".config", "spur", "rules");
|
|
75518
75905
|
SEVERITY_RANK2 = {
|
|
75519
75906
|
info: 0,
|
|
75520
75907
|
warning: 1,
|
|
@@ -76132,12 +76519,12 @@ class SystemEventEmitter {
|
|
|
76132
76519
|
secretValues;
|
|
76133
76520
|
projectContext;
|
|
76134
76521
|
quotas;
|
|
76135
|
-
constructor(dao2, logger3,
|
|
76522
|
+
constructor(dao2, logger3, retention2 = {}, secretValues = [], projectContext = systemEventProjectContext("")) {
|
|
76136
76523
|
this.dao = dao2;
|
|
76137
76524
|
this.logger = logger3;
|
|
76138
76525
|
this.secretValues = secretValues;
|
|
76139
76526
|
this.projectContext = projectContext;
|
|
76140
|
-
this.quotas = resolveRetentionQuotas(
|
|
76527
|
+
this.quotas = resolveRetentionQuotas(retention2);
|
|
76141
76528
|
}
|
|
76142
76529
|
async emit(event) {
|
|
76143
76530
|
const entry = systemEventCatalogEntry(event.event);
|
|
@@ -76519,7 +76906,7 @@ var init_task_size_precheck = __esm(() => {
|
|
|
76519
76906
|
});
|
|
76520
76907
|
|
|
76521
76908
|
// ../../packages/app/src/services/task-service.ts
|
|
76522
|
-
import { dirname as dirname14, isAbsolute as isAbsolute4, join as
|
|
76909
|
+
import { dirname as dirname14, isAbsolute as isAbsolute4, join as join17, relative as relative4 } from "path";
|
|
76523
76910
|
function renderRosterTable(rows) {
|
|
76524
76911
|
const header = `| WBS | Sub-task | Status |
|
|
76525
76912
|
| --- | -------- | ------ |`;
|
|
@@ -76943,7 +77330,7 @@ class TaskService {
|
|
|
76943
77330
|
if (!featureId2 || featureId2.length === 0)
|
|
76944
77331
|
return [];
|
|
76945
77332
|
const tasksDir = dirname14(taskFilePath);
|
|
76946
|
-
const featuresDir =
|
|
77333
|
+
const featuresDir = join17(tasksDir, "..", "features");
|
|
76947
77334
|
const featurePath = await (async () => {
|
|
76948
77335
|
try {
|
|
76949
77336
|
for (const name of await this.ctx.fs.readDir(featuresDir)) {
|
|
@@ -76986,9 +77373,11 @@ class TaskService {
|
|
|
76986
77373
|
reviewWritten: false,
|
|
76987
77374
|
solutionBackfilled: false
|
|
76988
77375
|
};
|
|
76989
|
-
|
|
76990
|
-
|
|
76991
|
-
|
|
77376
|
+
if (!(verdict.verdict === "UNKNOWN" && !sectionIsBare(doc2, "Testing"))) {
|
|
77377
|
+
const testingBody = renderTesting(verdict);
|
|
77378
|
+
await this.writeService.updateSection(ref, "Testing", testingBody);
|
|
77379
|
+
result.testingWritten = true;
|
|
77380
|
+
}
|
|
76992
77381
|
if (sectionIsBare(doc2, "Review")) {
|
|
76993
77382
|
const reviewBody = renderReview(verdict);
|
|
76994
77383
|
await this.writeService.updateSection(ref, "Review", reviewBody);
|
|
@@ -77691,7 +78080,7 @@ var init_task_verdict = __esm(() => {
|
|
|
77691
78080
|
});
|
|
77692
78081
|
|
|
77693
78082
|
// ../../packages/app/src/services/team-service.ts
|
|
77694
|
-
import { join as
|
|
78083
|
+
import { join as join18, resolve as resolve8 } from "path";
|
|
77695
78084
|
|
|
77696
78085
|
class TeamService {
|
|
77697
78086
|
ctx;
|
|
@@ -77699,7 +78088,7 @@ class TeamService {
|
|
|
77699
78088
|
orchestratorPromise;
|
|
77700
78089
|
constructor(ctx) {
|
|
77701
78090
|
this.ctx = ctx;
|
|
77702
|
-
this.configDir =
|
|
78091
|
+
this.configDir = join18(ctx.cwd, ".spur", "agents");
|
|
77703
78092
|
}
|
|
77704
78093
|
async sendMessage(fromId, toId, body, replyTo) {
|
|
77705
78094
|
validateAgentId(toId);
|
|
@@ -78167,7 +78556,7 @@ class TeamService {
|
|
|
78167
78556
|
async resolveTaskFile(taskId) {
|
|
78168
78557
|
const fs3 = this.ctx.fs;
|
|
78169
78558
|
const { foldersConfig } = await resolvePlanningFolders(fs3);
|
|
78170
|
-
const dirs = [...new Set([foldersConfig.active_folder, ...Object.keys(foldersConfig.folders)])].map((dir) =>
|
|
78559
|
+
const dirs = [...new Set([foldersConfig.active_folder, ...Object.keys(foldersConfig.folders)])].map((dir) => join18(this.ctx.cwd, dir));
|
|
78171
78560
|
return await TaskLocator.forDirs(fs3, dirs).findPathByWbs(taskId);
|
|
78172
78561
|
}
|
|
78173
78562
|
}
|
|
@@ -78205,7 +78594,7 @@ var init_team_service = __esm(() => {
|
|
|
78205
78594
|
|
|
78206
78595
|
// ../../packages/app/src/services/token-ledger-service.ts
|
|
78207
78596
|
import { closeSync as closeSync3, existsSync as existsSync8, fstatSync, openSync as openSync3, readSync } from "fs";
|
|
78208
|
-
import { join as
|
|
78597
|
+
import { join as join19 } from "path";
|
|
78209
78598
|
function isSparseToolActivity(events2) {
|
|
78210
78599
|
return events2.length === 0 || !events2.some((e) => TOOL_ACTIVITY_TYPES.has(e.type));
|
|
78211
78600
|
}
|
|
@@ -78306,7 +78695,7 @@ class TokenLedgerService {
|
|
|
78306
78695
|
ledgerPath;
|
|
78307
78696
|
chunkSize;
|
|
78308
78697
|
constructor(options) {
|
|
78309
|
-
this.ledgerPath = options.ledgerPath ??
|
|
78698
|
+
this.ledgerPath = options.ledgerPath ?? join19(options.cwd, TOKEN_LEDGER_RELATIVE_PATH);
|
|
78310
78699
|
this.chunkSize = options.chunkSize ?? DEFAULT_CHUNK;
|
|
78311
78700
|
}
|
|
78312
78701
|
get path() {
|
|
@@ -78347,7 +78736,7 @@ class TokenLedgerService {
|
|
|
78347
78736
|
var TOOL_ACTIVITY_TYPES, TOKEN_LEDGER_DEFAULT_LIMIT = 200, TOKEN_LEDGER_MAX_LIMIT = 1000, TOKEN_LEDGER_RELATIVE_PATH, DEFAULT_CHUNK;
|
|
78348
78737
|
var init_token_ledger_service = __esm(() => {
|
|
78349
78738
|
TOOL_ACTIVITY_TYPES = new Set(["read", "write", "bash", "grep", "glob"]);
|
|
78350
|
-
TOKEN_LEDGER_RELATIVE_PATH =
|
|
78739
|
+
TOKEN_LEDGER_RELATIVE_PATH = join19(".spur", "context", "token-ledger.jsonl");
|
|
78351
78740
|
DEFAULT_CHUNK = 64 * 1024;
|
|
78352
78741
|
});
|
|
78353
78742
|
|
|
@@ -78717,7 +79106,7 @@ var init_steering = __esm(() => {
|
|
|
78717
79106
|
|
|
78718
79107
|
// ../../packages/app/src/workflow/actions/agent-run.ts
|
|
78719
79108
|
import { tmpdir } from "os";
|
|
78720
|
-
import { dirname as dirname15, isAbsolute as isAbsolute5, join as
|
|
79109
|
+
import { dirname as dirname15, isAbsolute as isAbsolute5, join as join20 } from "path";
|
|
78721
79110
|
|
|
78722
79111
|
class AgentRunActionRunner {
|
|
78723
79112
|
observabilityBus;
|
|
@@ -78754,11 +79143,11 @@ class AgentRunActionRunner {
|
|
|
78754
79143
|
let sessionDir = asOptionalString(context4.vars.__agentSessionDir);
|
|
78755
79144
|
if (affinityOn) {
|
|
78756
79145
|
if (!sessionDir || prevAgent && prevAgent !== targetAgentDir) {
|
|
78757
|
-
sessionDir =
|
|
79146
|
+
sessionDir = join20(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
|
|
78758
79147
|
}
|
|
78759
79148
|
} else {
|
|
78760
79149
|
if (!sessionDir) {
|
|
78761
|
-
sessionDir =
|
|
79150
|
+
sessionDir = join20(cwd, ".spur", "run", context4.runId, "agent-sessions", targetAgentDir);
|
|
78762
79151
|
}
|
|
78763
79152
|
}
|
|
78764
79153
|
const storedSessionId = asOptionalString(context4.vars.__agentSessionId);
|
|
@@ -78868,13 +79257,13 @@ class AgentRunActionRunner {
|
|
|
78868
79257
|
const ok = exitCode === 0;
|
|
78869
79258
|
const invocation = traced.invocation;
|
|
78870
79259
|
if (capture && answerFile !== undefined) {
|
|
78871
|
-
const target = isAbsolute5(answerFile) ? answerFile :
|
|
79260
|
+
const target = isAbsolute5(answerFile) ? answerFile : join20(cwd, answerFile);
|
|
78872
79261
|
const fs3 = createNodeFileSystem3(cwd);
|
|
78873
79262
|
await fs3.ensureDir(dirname15(target));
|
|
78874
79263
|
await fs3.writeFile(target, answer);
|
|
78875
79264
|
}
|
|
78876
79265
|
if (ok && expectFile !== undefined) {
|
|
78877
|
-
const target = isAbsolute5(expectFile) ? expectFile :
|
|
79266
|
+
const target = isAbsolute5(expectFile) ? expectFile : join20(cwd, expectFile);
|
|
78878
79267
|
const fs3 = createNodeFileSystem3(cwd);
|
|
78879
79268
|
if (!await fs3.exists(target)) {
|
|
78880
79269
|
return {
|
|
@@ -78913,7 +79302,7 @@ class AgentRunActionRunner {
|
|
|
78913
79302
|
const resolvedAgent = invocation?.agent ?? targetAgentDir;
|
|
78914
79303
|
let resolvedSessionDir = sessionDir;
|
|
78915
79304
|
if (ok && affinityOn && resolvedAgent !== targetAgentDir && !context4.vars.__agentSessionDir) {
|
|
78916
|
-
resolvedSessionDir =
|
|
79305
|
+
resolvedSessionDir = join20(cwd, ".spur", "run", context4.runId, "agent-sessions", resolvedAgent);
|
|
78917
79306
|
}
|
|
78918
79307
|
let discoveredSessionId = storedSessionId;
|
|
78919
79308
|
if (ok && affinityOn && resolvedSessionDir && !discoveredSessionId) {
|
|
@@ -78921,7 +79310,7 @@ class AgentRunActionRunner {
|
|
|
78921
79310
|
}
|
|
78922
79311
|
if (ok && affinityOn && resolvedSessionDir) {
|
|
78923
79312
|
try {
|
|
78924
|
-
const sidecarPath =
|
|
79313
|
+
const sidecarPath = join20(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
|
|
78925
79314
|
const fs3 = createNodeFileSystem3(cwd);
|
|
78926
79315
|
await fs3.ensureDir(dirname15(sidecarPath));
|
|
78927
79316
|
await fs3.writeFile(sidecarPath, JSON.stringify({
|
|
@@ -78964,7 +79353,7 @@ async function discoverSessionId(sessionDir) {
|
|
|
78964
79353
|
let newestFile;
|
|
78965
79354
|
let newestMtime = 0;
|
|
78966
79355
|
for (const entry of jsonEntries) {
|
|
78967
|
-
const fullPath =
|
|
79356
|
+
const fullPath = join20(sessionDir, entry);
|
|
78968
79357
|
const st = await fs3.stat(fullPath);
|
|
78969
79358
|
if (st?.isFile() && st.mtimeMs > newestMtime) {
|
|
78970
79359
|
newestMtime = st.mtimeMs;
|
|
@@ -79060,7 +79449,7 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
|
|
|
79060
79449
|
const headerLine = model !== undefined ? `${agentLabel} (model: ${model})` : agentLabel;
|
|
79061
79450
|
const inv = traced.invocation;
|
|
79062
79451
|
const argvLine = inv ? `${inv.command} ${inv.argv.join(" ")}` : "(invocation not captured)";
|
|
79063
|
-
const latchedSessionPath =
|
|
79452
|
+
const latchedSessionPath = join20(cwd, ".spur", "run", `${context4.runId}-agent-session.json`);
|
|
79064
79453
|
const resumeContext = [
|
|
79065
79454
|
"",
|
|
79066
79455
|
"## resume context",
|
|
@@ -79076,9 +79465,9 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
|
|
|
79076
79465
|
try {
|
|
79077
79466
|
const fs4 = createNodeFileSystem3(cwd);
|
|
79078
79467
|
const locator = TaskLocator.forDirs(fs4, [
|
|
79079
|
-
|
|
79080
|
-
|
|
79081
|
-
|
|
79468
|
+
join20(cwd, "docs", "tasks3"),
|
|
79469
|
+
join20(cwd, "docs", "tasks2"),
|
|
79470
|
+
join20(cwd, "docs", "tasks")
|
|
79082
79471
|
]);
|
|
79083
79472
|
const hit = await locator.findByWbs(wbs);
|
|
79084
79473
|
if (hit) {
|
|
@@ -79134,7 +79523,7 @@ async function writePartialWorkArtifact(context4, agentLabel, model, traced, cwd
|
|
|
79134
79523
|
completedSection
|
|
79135
79524
|
].join(`
|
|
79136
79525
|
`);
|
|
79137
|
-
const target =
|
|
79526
|
+
const target = join20(cwd, ".spur", "run", `${context4.runId}-${context4.stateOrNodeId}-partial.md`);
|
|
79138
79527
|
const fs3 = createNodeFileSystem3(cwd);
|
|
79139
79528
|
await fs3.ensureDir(dirname15(target));
|
|
79140
79529
|
await fs3.writeFile(target, body);
|
|
@@ -79156,7 +79545,7 @@ async function gitDiffStat(cwd) {
|
|
|
79156
79545
|
}
|
|
79157
79546
|
}
|
|
79158
79547
|
async function createGitWorkingTreeSnapshot(cwd, excludeGlobs = ["docs/tasks3/*", "docs/features/*"]) {
|
|
79159
|
-
const indexFile =
|
|
79548
|
+
const indexFile = join20(tmpdir(), `spur-implement-scope-${crypto.randomUUID()}.index`);
|
|
79160
79549
|
const fs3 = createNodeFileSystem3(cwd);
|
|
79161
79550
|
let keepIndex = false;
|
|
79162
79551
|
try {
|
|
@@ -79287,9 +79676,9 @@ async function findOutOfScopeChanges(cwd, wbs, changed) {
|
|
|
79287
79676
|
try {
|
|
79288
79677
|
const fs3 = createNodeFileSystem3(cwd);
|
|
79289
79678
|
const locator = TaskLocator.forDirs(fs3, [
|
|
79290
|
-
|
|
79291
|
-
|
|
79292
|
-
|
|
79679
|
+
join20(cwd, "docs", "tasks3"),
|
|
79680
|
+
join20(cwd, "docs", "tasks2"),
|
|
79681
|
+
join20(cwd, "docs", "tasks")
|
|
79293
79682
|
]);
|
|
79294
79683
|
const hit = await locator.findByWbs(wbs);
|
|
79295
79684
|
if (!hit)
|
|
@@ -79516,9 +79905,10 @@ function parseDoctorJson(stdout) {
|
|
|
79516
79905
|
const first = parsed.agents?.[0];
|
|
79517
79906
|
const auth = typeof first?.authenticated === "string" ? first.authenticated : "unknown";
|
|
79518
79907
|
const detail = typeof first?.modelStatus?.detail === "string" ? first.modelStatus.detail : "";
|
|
79519
|
-
|
|
79908
|
+
const resolvedAgent = typeof first?.agent === "string" && first.agent.length > 0 ? first.agent : "";
|
|
79909
|
+
return { auth, detail, resolvedAgent };
|
|
79520
79910
|
} catch {
|
|
79521
|
-
return { auth: "unknown", detail: "" };
|
|
79911
|
+
return { auth: "unknown", detail: "", resolvedAgent: "" };
|
|
79522
79912
|
}
|
|
79523
79913
|
}
|
|
79524
79914
|
|
|
@@ -79604,18 +79994,20 @@ ${res.stderr}`.trim();
|
|
|
79604
79994
|
status = "FAIL";
|
|
79605
79995
|
continue;
|
|
79606
79996
|
}
|
|
79607
|
-
const { auth, detail } = parseDoctorJson(res.stdout);
|
|
79608
|
-
const
|
|
79609
|
-
const
|
|
79997
|
+
const { auth, detail, resolvedAgent } = parseDoctorJson(res.stdout);
|
|
79998
|
+
const classifyAgainst = resolvedAgent !== "" ? resolvedAgent : exe;
|
|
79999
|
+
const probe = classifyDoctorProbe(detail, classifyAgainst);
|
|
80000
|
+
const resolvedSuffix = resolvedAgent !== "" && resolvedAgent !== exe ? ` (resolved ${resolvedAgent})` : "";
|
|
80001
|
+
const line = `precheck: ${exe}${resolvedSuffix} auth=${auth} probe=${probe} ${detail}`.replace(/\s+$/, "");
|
|
79610
80002
|
lines.push(line);
|
|
79611
80003
|
emit(line);
|
|
79612
80004
|
if (auth === "unauthenticated") {
|
|
79613
|
-
if (RELAY_FAMILY.test(
|
|
79614
|
-
const soft = `precheck: SOFT - executor ${
|
|
80005
|
+
if (RELAY_FAMILY.test(classifyAgainst) && (probe === "env-miss" || probe === "unknown")) {
|
|
80006
|
+
const soft = `precheck: SOFT - executor ${classifyAgainst} auth probe cannot see agent-owned credentials`;
|
|
79615
80007
|
lines.push(soft);
|
|
79616
80008
|
emit(soft);
|
|
79617
80009
|
} else {
|
|
79618
|
-
const hard = `precheck: FAIL - executor ${
|
|
80010
|
+
const hard = `precheck: FAIL - executor ${classifyAgainst} is unauthenticated; fix agent.default or pass --vars '{"agent":"<authenticated-executor>"}' (${spurBin} agent doctor ${classifyAgainst} --json); ${detail}`;
|
|
79619
80011
|
lines.push(hard);
|
|
79620
80012
|
emit(hard);
|
|
79621
80013
|
status = "FAIL";
|
|
@@ -80270,9 +80662,9 @@ var init_composition_baseline = __esm(() => {
|
|
|
80270
80662
|
// ../../packages/app/src/workflow/proof-input-fingerprint.ts
|
|
80271
80663
|
import crypto4 from "crypto";
|
|
80272
80664
|
import { tmpdir as tmpdir2 } from "os";
|
|
80273
|
-
import { dirname as dirname16, join as
|
|
80665
|
+
import { dirname as dirname16, join as join21 } from "path";
|
|
80274
80666
|
async function createGitAlternateTree(cwd, excludeGlobs = DEFAULT_EXCLUDE_GLOBS, executor = new NodeProcessExecutor3, fs3 = createNodeFileSystem3()) {
|
|
80275
|
-
const indexFile =
|
|
80667
|
+
const indexFile = join21(tmpdir2(), `spur-proof-fingerprint-${crypto4.randomUUID()}.index`);
|
|
80276
80668
|
try {
|
|
80277
80669
|
await fs3.ensureDir(dirname16(indexFile));
|
|
80278
80670
|
const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
@@ -80818,8 +81210,9 @@ var init_builtins2 = __esm(() => {
|
|
|
80818
81210
|
});
|
|
80819
81211
|
|
|
80820
81212
|
// ../../packages/app/src/services/workflow-service.ts
|
|
81213
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
80821
81214
|
import { homedir as homedir7 } from "os";
|
|
80822
|
-
import { dirname as dirname17, join as
|
|
81215
|
+
import { basename as basename7, dirname as dirname17, join as join22, resolve as resolve13 } from "path";
|
|
80823
81216
|
function signalSubprocess(pid) {
|
|
80824
81217
|
if (!Number.isInteger(pid) || pid <= 1)
|
|
80825
81218
|
return false;
|
|
@@ -80955,8 +81348,9 @@ class WorkflowAppService {
|
|
|
80955
81348
|
if (roleErrors.length > 0) {
|
|
80956
81349
|
return { ok: false, valid: false, file: file2, errors: roleErrors };
|
|
80957
81350
|
}
|
|
81351
|
+
const composition = collectCompositionAdvisory(workflow, absolute);
|
|
80958
81352
|
await this.loadWorkflowExtensions(createDefaultWorkflowEngineHost(), workflow, absolute);
|
|
80959
|
-
return { ok: true, valid: true, workflow };
|
|
81353
|
+
return { ok: true, valid: true, workflow, composition };
|
|
80960
81354
|
} catch (error51) {
|
|
80961
81355
|
return {
|
|
80962
81356
|
ok: false,
|
|
@@ -81073,7 +81467,7 @@ class WorkflowAppService {
|
|
|
81073
81467
|
};
|
|
81074
81468
|
}
|
|
81075
81469
|
async cleanRunLogs(retentionDays = 30, dryRun = false) {
|
|
81076
|
-
const runDir =
|
|
81470
|
+
const runDir = join22(this.ctx.cwd, ".spur", "run");
|
|
81077
81471
|
const fs3 = createNodeFileSystem3();
|
|
81078
81472
|
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
81079
81473
|
const reclaimed = [];
|
|
@@ -81085,7 +81479,7 @@ class WorkflowAppService {
|
|
|
81085
81479
|
return { retentionDays, dryRun, reclaimed, failures };
|
|
81086
81480
|
}
|
|
81087
81481
|
for (const name of entries) {
|
|
81088
|
-
const path9 =
|
|
81482
|
+
const path9 = join22(runDir, name);
|
|
81089
81483
|
const stat = await fs3.stat(path9);
|
|
81090
81484
|
if (stat === null || !stat.isFile() || stat.mtimeMs >= cutoffMs)
|
|
81091
81485
|
continue;
|
|
@@ -81169,7 +81563,7 @@ class WorkflowAppService {
|
|
|
81169
81563
|
}
|
|
81170
81564
|
async list(workflowPaths = [".spur/workflows/"]) {
|
|
81171
81565
|
const projectRoot = this.ctx.cwd;
|
|
81172
|
-
const globalRoot =
|
|
81566
|
+
const globalRoot = join22(homedir7(), ".config", "spur");
|
|
81173
81567
|
const layers = [];
|
|
81174
81568
|
const entries = [];
|
|
81175
81569
|
const scannedPaths = new Set;
|
|
@@ -81443,6 +81837,100 @@ function collectAgentRunRoleViolations(def) {
|
|
|
81443
81837
|
}
|
|
81444
81838
|
return violations;
|
|
81445
81839
|
}
|
|
81840
|
+
function collectCompositionAdvisory(def, workflowFile) {
|
|
81841
|
+
const findings = [];
|
|
81842
|
+
let suppressed = 0;
|
|
81843
|
+
const shellLines = (command) => command.split(/\n|;/).map((u2) => u2.trim()).filter((u2) => u2.length > 0 && !u2.startsWith("#")).length;
|
|
81844
|
+
const agentRunSeverity = (prompt) => {
|
|
81845
|
+
if (prompt.length < 200)
|
|
81846
|
+
return "low";
|
|
81847
|
+
if (prompt.length <= 1000)
|
|
81848
|
+
return "medium";
|
|
81849
|
+
return "high";
|
|
81850
|
+
};
|
|
81851
|
+
const baseline = loadCompositionBaselineFor(workflowFile);
|
|
81852
|
+
const workflowName = basename7(workflowFile, ".yaml");
|
|
81853
|
+
const adjudicated = (actionKey) => {
|
|
81854
|
+
if (!baseline)
|
|
81855
|
+
return false;
|
|
81856
|
+
const entry = baseline.workflows[workflowName];
|
|
81857
|
+
if (!entry)
|
|
81858
|
+
return false;
|
|
81859
|
+
const action = entry.actions[actionKey];
|
|
81860
|
+
return action?.disposition !== undefined;
|
|
81861
|
+
};
|
|
81862
|
+
const visitAction = (stateId, action, idx, phase) => {
|
|
81863
|
+
const actionKey = `${stateId}:${phase}:${idx}`;
|
|
81864
|
+
if (action.kind === "shell") {
|
|
81865
|
+
const cmd = action.options?.command;
|
|
81866
|
+
if (typeof cmd !== "string" || cmd.length === 0)
|
|
81867
|
+
return;
|
|
81868
|
+
const measured = shellLines(cmd);
|
|
81869
|
+
if (measured < 6)
|
|
81870
|
+
return;
|
|
81871
|
+
if (adjudicated(actionKey)) {
|
|
81872
|
+
suppressed += 1;
|
|
81873
|
+
return;
|
|
81874
|
+
}
|
|
81875
|
+
findings.push({
|
|
81876
|
+
workflow: workflowName,
|
|
81877
|
+
state: stateId,
|
|
81878
|
+
actionKey,
|
|
81879
|
+
measure: { kind: "shell-lines", measured, threshold: 6 },
|
|
81880
|
+
recommendation: `shell action at ${actionKey} measures ${measured} lines (>5 frozen threshold, ADR-069) \u2014 extract to a script or record a disposition in config/workflow-composition-baseline.json`
|
|
81881
|
+
});
|
|
81882
|
+
} else if (action.kind === "agent.run") {
|
|
81883
|
+
const input = action.options?.input;
|
|
81884
|
+
if (typeof input !== "string" || input.length === 0)
|
|
81885
|
+
return;
|
|
81886
|
+
if (input.trimStart().startsWith("/"))
|
|
81887
|
+
return;
|
|
81888
|
+
if (adjudicated(actionKey)) {
|
|
81889
|
+
suppressed += 1;
|
|
81890
|
+
} else {
|
|
81891
|
+
const severity = agentRunSeverity(input);
|
|
81892
|
+
findings.push({
|
|
81893
|
+
workflow: workflowName,
|
|
81894
|
+
state: stateId,
|
|
81895
|
+
actionKey,
|
|
81896
|
+
measure: { kind: "agent-run-chars", measured: input.length, severity },
|
|
81897
|
+
recommendation: `agent.run prompt at ${actionKey} is ${input.length} chars, not slash-pinned (severity ${severity}) \u2014 pin to a slash command or a script with a bounded prompt`
|
|
81898
|
+
});
|
|
81899
|
+
}
|
|
81900
|
+
}
|
|
81901
|
+
};
|
|
81902
|
+
if (def.kind === "transition-flow" || def.kind === undefined) {
|
|
81903
|
+
const flowDef = def;
|
|
81904
|
+
for (const node of flowDef.nodes ?? []) {
|
|
81905
|
+
if (node.action)
|
|
81906
|
+
visitAction(node.id, node.action, 0, "onEnter");
|
|
81907
|
+
}
|
|
81908
|
+
} else {
|
|
81909
|
+
const smDef = def;
|
|
81910
|
+
for (const state of smDef.states ?? []) {
|
|
81911
|
+
for (const [i2, action] of (state.onEnter ?? []).entries())
|
|
81912
|
+
visitAction(state.id, action, i2, "onEnter");
|
|
81913
|
+
for (const [i2, action] of (state.onExit ?? []).entries())
|
|
81914
|
+
visitAction(state.id, action, i2, "onExit");
|
|
81915
|
+
}
|
|
81916
|
+
}
|
|
81917
|
+
return { findings, suppressed };
|
|
81918
|
+
}
|
|
81919
|
+
function loadCompositionBaselineFor(workflowFile) {
|
|
81920
|
+
let dir = dirname17(resolve13(workflowFile));
|
|
81921
|
+
for (let i2 = 0;i2 < 10; i2++) {
|
|
81922
|
+
const candidate = join22(dir, "config/workflow-composition-baseline.json");
|
|
81923
|
+
if (existsSync10(candidate)) {
|
|
81924
|
+
try {
|
|
81925
|
+
return JSON.parse(readFileSync10(candidate, "utf8"));
|
|
81926
|
+
} catch {
|
|
81927
|
+
return;
|
|
81928
|
+
}
|
|
81929
|
+
}
|
|
81930
|
+
dir = dirname17(dir);
|
|
81931
|
+
}
|
|
81932
|
+
return;
|
|
81933
|
+
}
|
|
81446
81934
|
async function fileExists(path9) {
|
|
81447
81935
|
const fs3 = createNodeFileSystem3();
|
|
81448
81936
|
return await fs3.exists(path9);
|
|
@@ -81495,8 +81983,8 @@ async function resolveOutputLogConfig(cwd) {
|
|
|
81495
81983
|
}
|
|
81496
81984
|
}
|
|
81497
81985
|
async function outputArtifactForRun(cwd, runId) {
|
|
81498
|
-
const relative5 =
|
|
81499
|
-
return await fileExists(
|
|
81986
|
+
const relative5 = join22(".spur", "run", `${runId}.log`);
|
|
81987
|
+
return await fileExists(join22(cwd, relative5)) ? relative5 : undefined;
|
|
81500
81988
|
}
|
|
81501
81989
|
async function scanWorkflowFiles(rootPath, source) {
|
|
81502
81990
|
const entries = [];
|
|
@@ -81671,7 +82159,7 @@ function projectActionTraceResult(resultJson, secretValues = []) {
|
|
|
81671
82159
|
async function partialArtifactForAction(cwd, runId, node, ok) {
|
|
81672
82160
|
if (ok !== false || !TRACE_IDENTIFIER.test(runId) || !TRACE_IDENTIFIER.test(node))
|
|
81673
82161
|
return;
|
|
81674
|
-
const relativePath2 =
|
|
82162
|
+
const relativePath2 = join22(".spur", "run", `${runId}-${node}-partial.md`);
|
|
81675
82163
|
return await fileExists(resolve13(cwd, relativePath2)) ? relativePath2 : undefined;
|
|
81676
82164
|
}
|
|
81677
82165
|
var TASK_PIPELINE_WORKFLOW = "task-pipeline", EMBEDDED_SCHEMA_PREFIX = "\x00embedded-spur", SPUR_SCHEMA_MANIFEST = "@gobing-ai/spur/package.json", PIPELINE_LINK_KIND = "pipeline", TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
|
|
@@ -81903,7 +82391,8 @@ class LifecycleAdapter {
|
|
|
81903
82391
|
const db2 = await this.opts.getDb();
|
|
81904
82392
|
const host = createDefaultWorkflowEngineHost();
|
|
81905
82393
|
host.registerGuard(new EnvShellGuardRunner(new NodeProcessExecutor3), "builtin");
|
|
81906
|
-
const
|
|
82394
|
+
const persistence2 = new DbWorkflowPersistenceAdapter(db2);
|
|
82395
|
+
const svc = new WorkflowService(host, persistence2);
|
|
81907
82396
|
const workflow = this.bindGuardVar(await this.loadWorkflow(), ref.id);
|
|
81908
82397
|
const externalKey = `${profile.entityPrefix}:${ref.id}`;
|
|
81909
82398
|
const now2 = new Date().toISOString();
|
|
@@ -81963,6 +82452,8 @@ class LifecycleAdapter {
|
|
|
81963
82452
|
await svc.reseedRun(workflow, runId, currentStatus);
|
|
81964
82453
|
const result = await svc.requestTransition(workflow, runId, to, { workdir: this.opts.cwd });
|
|
81965
82454
|
if (result.allowed) {
|
|
82455
|
+
const terminalStatus = to === "cancelled" ? "failed" : to === "done" ? "done" : null;
|
|
82456
|
+
await persistence2.finalizeRun(runId, terminalStatus ?? "running", new Date().toISOString());
|
|
81966
82457
|
return { allowed: true, from: result.fromState, to: result.toState };
|
|
81967
82458
|
}
|
|
81968
82459
|
return {
|
|
@@ -82385,7 +82876,7 @@ var init_progress_follow = __esm(() => {
|
|
|
82385
82876
|
});
|
|
82386
82877
|
|
|
82387
82878
|
// ../../packages/app/src/workflow/trace-writer.ts
|
|
82388
|
-
import { dirname as dirname18, join as
|
|
82879
|
+
import { dirname as dirname18, join as join23 } from "path";
|
|
82389
82880
|
|
|
82390
82881
|
class WorkflowTraceWriter {
|
|
82391
82882
|
path;
|
|
@@ -82393,7 +82884,7 @@ class WorkflowTraceWriter {
|
|
|
82393
82884
|
pending = Promise.resolve();
|
|
82394
82885
|
constructor(cwd, runId) {
|
|
82395
82886
|
const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
82396
|
-
this.path =
|
|
82887
|
+
this.path = join23(cwd, ".spur", "runs", "workflow", `${safeRunId}.jsonl`);
|
|
82397
82888
|
}
|
|
82398
82889
|
attach(bus) {
|
|
82399
82890
|
bus.on("workflow.run.started", (event) => this.enqueue("workflow.run.started", event));
|
|
@@ -90010,25 +90501,114 @@ async function attachSystemEventLedger(bus, context4, options = {}) {
|
|
|
90010
90501
|
};
|
|
90011
90502
|
}
|
|
90012
90503
|
|
|
90504
|
+
// src/commands/shared-options.ts
|
|
90505
|
+
var SHARED_OPTIONS = {
|
|
90506
|
+
json: ["--json", "Output machine-readable JSON"],
|
|
90507
|
+
jsonSupported: ["--json", "Output machine-readable JSON where supported"],
|
|
90508
|
+
section: ["--section <name>", "Section name to replace"],
|
|
90509
|
+
fromFile: ["--from-file <path>", "File to read section body from (requires --section)"],
|
|
90510
|
+
statusFilter: ["--status <s>", "Filter by status"],
|
|
90511
|
+
last: ["--last <n>", "Limit results (default 20)"],
|
|
90512
|
+
noSchema: ["--no-schema", "Skip schema validation"],
|
|
90513
|
+
since: ["--since <iso-date>", "Filter runs started on or after this date"],
|
|
90514
|
+
timeout: ["--timeout <ms>", "Caller deadline in milliseconds"],
|
|
90515
|
+
folderTasks: ["--folder <path>", "Custom tasks folder"],
|
|
90516
|
+
folderFeatures: ["--folder <path>", "Custom features folder"],
|
|
90517
|
+
agentIdMessage: ["--agent <id>", "Agent id"],
|
|
90518
|
+
agentIdWatch: ["--agent <id>", "Agent id to watch"],
|
|
90519
|
+
agentIdLegacyRecipient: ["--agent <id>", "Agent spec id / message recipient (legacy \u2014 prefer --spec)"],
|
|
90520
|
+
cwdServe: ["--cwd <path>", "Working directory"],
|
|
90521
|
+
cwdAgent: ["--cwd <path>", "Working directory for agent execution"],
|
|
90522
|
+
modeAgent: ["--mode <mode>", "Agent output mode: text|json"],
|
|
90523
|
+
modeHistory: ["--mode <mode>", "full|incremental|force-file"],
|
|
90524
|
+
nameAgent: ["--name <name>", "Agent name"],
|
|
90525
|
+
nameProjectDisplay: ["--name <name>", "Display name for the project"],
|
|
90526
|
+
nameProjectInit: ["--name <name>", "Project name (default: current directory name)"],
|
|
90527
|
+
pollWorkflow: ["--poll <ms>", "Follow polling interval in milliseconds"],
|
|
90528
|
+
pollAgent: ["--poll <ms>", "Idle poll interval in milliseconds"],
|
|
90529
|
+
portProjects: ["--port <n>", "Explicit port to bind"],
|
|
90530
|
+
portServe: ["--port <n>", "Server port (env: PORT, default: 3000)"],
|
|
90531
|
+
priorityFilter: ["--priority <p>", "Filter by priority"],
|
|
90532
|
+
prioritySet: ["--priority <p>", "Set the priority frontmatter field (P0\u2013P3)"],
|
|
90533
|
+
runHistory: ["--run <runId>", "Narrow to a single workflow run id"],
|
|
90534
|
+
runAgentPin: ["--run <runId>", "Pin a specific run id (default: spec latest run)"],
|
|
90535
|
+
runIdTask: ["--run-id <id>", "Explicit run_id (auto-generated when omitted)"],
|
|
90536
|
+
runIdWorkflow: ["--run-id <id>", "Persisted run id for workflow run"],
|
|
90537
|
+
sourceLink: ["--source <source>", "Link source identifier (e.g. next-auto)"],
|
|
90538
|
+
sourceHistory: [
|
|
90539
|
+
"--source <source>",
|
|
90540
|
+
"pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all"
|
|
90541
|
+
],
|
|
90542
|
+
statusDoneFailed: ["--status <status>", "Filter by status: done, failed"],
|
|
90543
|
+
statusDoneFailedRunning: ["--status <status>", "Filter by status: done, failed, running"],
|
|
90544
|
+
untilAgent: ["--until <state>", "Lifecycle state to wait for (repeatable OR)"],
|
|
90545
|
+
untilMessage: ["--until <state>", "Wait target for --wait: injected|invoke-exit"],
|
|
90546
|
+
verboseWorkflow: ["--verbose", "Include transitions and correlation diagnostics in human progress"],
|
|
90547
|
+
verboseRule: ["--verbose", "Stream per-rule progress to stderr"],
|
|
90548
|
+
jsonArtifact: ["--json", "Emit the artifact as JSON instead of the human summary"],
|
|
90549
|
+
jsonDaily: ["--json", "Emit the daily result as JSON"],
|
|
90550
|
+
jsonParsedArtifact: ["--json", "Emit the parsed artifact as JSON instead of the human report"],
|
|
90551
|
+
jsonProjectsArray: ["--json", "Output JSON array of projects"],
|
|
90552
|
+
jsonProjectsResponse: ["--json", "Output JSON response"],
|
|
90553
|
+
jsonMessageStream: ["--json", "Output one JSON object per new message (machine-consumable)"],
|
|
90554
|
+
jsonServePortUrl: ["--json", "Output { port, url, pid } and exit"],
|
|
90555
|
+
fromFileOutcomeRows: ["--from-file <path>", "Path to JSON array of {wbs,outcome[,reason]} rows"],
|
|
90556
|
+
asFeature0418: [
|
|
90557
|
+
"--as <status>",
|
|
90558
|
+
"Evaluate the one-active-goal rule as if the feature were in <status> (0418: lifecycle FSM guards pass the transition target)"
|
|
90559
|
+
],
|
|
90560
|
+
asTaskF92: [
|
|
90561
|
+
"--as <status>",
|
|
90562
|
+
"Evaluate the task AS if it were in <status> (F92 R2): the lifecycle guards pass the transition target so testing\u2192done checks the done row. Validate against canonical task statuses. Omitted \u2192 current-status diagnostics."
|
|
90563
|
+
],
|
|
90564
|
+
dryRunFeatureMap: ["--dry-run", "Show the old\u2192new ID map + affected tasks without writing"],
|
|
90565
|
+
dryRunFeatureSync: ["--dry-run", "Report proposed status sync transitions without applying"],
|
|
90566
|
+
dryRunHistoryScan: ["--dry-run", "Scan without persisting imported records"],
|
|
90567
|
+
dryRunRuleFix: ["--dry-run", "Preview fixes without writing (use with --fix-mode auto)"],
|
|
90568
|
+
dryRunTaskReport: ["--dry-run", "Produce the full report without writing files"],
|
|
90569
|
+
dryRunWorkflowValidate: ["--dry-run", "Validate and walk transitions without executing actions"],
|
|
90570
|
+
dryRunWorkflowClean: ["--dry-run", "List what would be cleaned without writing (applies to both scopes)"],
|
|
90571
|
+
featureTrace: ["--feature <id>", "Feature ID for traceability and Goal\u2192Background derivation"],
|
|
90572
|
+
featureFrontmatter: ["--feature <id>", "Set the feature_id frontmatter field (traceability edge)"],
|
|
90573
|
+
featureFilterEdge: ["--feature <id>", "Filter by linked feature ID (feature_id edge)"],
|
|
90574
|
+
featureTasksRewrite: [
|
|
90575
|
+
"--feature <id>",
|
|
90576
|
+
"Restrict the ## Tasks rewrite to one feature (INDEX.md still regenerated)"
|
|
90577
|
+
],
|
|
90578
|
+
fileHistoryJsonl: ["--file <path>", "Import one JSONL file (single-source only)"],
|
|
90579
|
+
fileRuleAdhoc: ["--file <path>", "Ad-hoc rule file"],
|
|
90580
|
+
fileRuleAdhocPath: ["--file <path>", "Ad-hoc rule file path"],
|
|
90581
|
+
fileTaskBatch: ["--file <path>", "Path to the batch JSON file validated against task-batch.schema.json"],
|
|
90582
|
+
fileTaskTest: ["--file <path>", "Custom target test file path"],
|
|
90583
|
+
forceAgentDelete: ["--force", "Required for delete"],
|
|
90584
|
+
forceFeatureReopen: ["--force", "Force applying reopen proposals without confirmation"],
|
|
90585
|
+
forceInitRecreate: ["--force", "Recreate files that already exist"],
|
|
90586
|
+
forceWorkflowClean: ["--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)"],
|
|
90587
|
+
strictFeature: ["--strict", "Elevate warnings to failures"],
|
|
90588
|
+
strictTaskAll: ["--strict", "Elevate ALL warnings to failures"],
|
|
90589
|
+
strictTaskPath: ["--strict", "Match only the exact corpus path (no basename-WBS fallback)"]
|
|
90590
|
+
};
|
|
90591
|
+
var SHARED_OPTION_FLAGS = new Set(Object.values(SHARED_OPTIONS).map(([flags]) => flags));
|
|
90592
|
+
|
|
90013
90593
|
// src/commands/agent.ts
|
|
90014
90594
|
function registerAgentCommand(program2, context4) {
|
|
90015
90595
|
const agent = program2.command("agent").summary("run and inspect supported coding agents");
|
|
90016
|
-
agent.command("list").description("List detected coding agents, or team agent specs with --specs.").option(
|
|
90596
|
+
agent.command("list").description("List detected coding agents, or team agent specs with --specs.").option(...SHARED_OPTIONS.json).option("--specs", "List team specs instead of detected agents").action(async (options) => {
|
|
90017
90597
|
const svc = new AgentService({ cwd: context4.cwd, env: context4.env, output: context4.output });
|
|
90018
90598
|
const code = await runAgentList(svc, context4, { json: options.json, specs: options.specs });
|
|
90019
90599
|
context4.setExitCode(code);
|
|
90020
90600
|
});
|
|
90021
|
-
agent.command("doctor").description("Check agent readiness.").option(
|
|
90601
|
+
agent.command("doctor").description("Check agent readiness.").option(...SHARED_OPTIONS.json).argument("[agent]", "Agent to check").action(async (agentName, options) => {
|
|
90022
90602
|
const svc = context4.agentService();
|
|
90023
90603
|
const code = await svc.doctor({ json: options.json === true, agent: agentName }, undefined);
|
|
90024
90604
|
context4.setExitCode(code);
|
|
90025
90605
|
});
|
|
90026
|
-
agent.command("run").description("Execute a prompt or slash command via a coding agent.").option("--agent <name>", "Role, executor, agent binary, auto, or inline (host-session-only; errors on headless surfaces)").option("--spec <id>", "Team agent spec id (occupant addressing; pairs with --drain)").option("--continue", "Resume the previous agent session").option("--model <name>", "Agent model argument").option(
|
|
90606
|
+
agent.command("run").description("Execute a prompt or slash command via a coding agent.").option("--agent <name>", "Role, executor, agent binary, auto, or inline (host-session-only; errors on headless surfaces)").option("--spec <id>", "Team agent spec id (occupant addressing; pairs with --drain)").option("--continue", "Resume the previous agent session").option("--model <name>", "Agent model argument").option(...SHARED_OPTIONS.modeAgent).option(...SHARED_OPTIONS.cwdAgent).option(...SHARED_OPTIONS.jsonSupported).option("--drain", "Prepend pending inbox messages for --spec <id>").argument("<prompt>", "The prompt or slash command to execute").action(async (prompt, options) => {
|
|
90027
90607
|
const flags = commanderOptionsToFlags(options);
|
|
90028
90608
|
const code = await runAgentRun(prompt, context4, flags);
|
|
90029
90609
|
context4.setExitCode(code);
|
|
90030
90610
|
});
|
|
90031
|
-
agent.command("loop").description("Run the persistent self-draining loop for a team member (used by the supervisor).").option("--spec <id>", "Agent spec id / message recipient").option(
|
|
90611
|
+
agent.command("loop").description("Run the persistent self-draining loop for a team member (used by the supervisor).").option("--spec <id>", "Agent spec id / message recipient").option(...SHARED_OPTIONS.agentIdLegacyRecipient).option(...SHARED_OPTIONS.pollAgent, String(DEFAULT_LOOP_POLL_MS)).action(async (options) => {
|
|
90032
90612
|
const controller = new AbortController;
|
|
90033
90613
|
const onSignal = () => controller.abort();
|
|
90034
90614
|
process.on("SIGINT", onSignal);
|
|
@@ -90042,11 +90622,11 @@ function registerAgentCommand(program2, context4) {
|
|
|
90042
90622
|
process.off("SIGTERM", onSignal);
|
|
90043
90623
|
}
|
|
90044
90624
|
});
|
|
90045
|
-
agent.command("wait").description("Wait for a pinned occupant run to reach a lifecycle state (G4 wave 2).").argument("<specId>", "Agent spec id whose occupant to wait on").option(
|
|
90625
|
+
agent.command("wait").description("Wait for a pinned occupant run to reach a lifecycle state (G4 wave 2).").argument("<specId>", "Agent spec id whose occupant to wait on").option(...SHARED_OPTIONS.runAgentPin).option(...SHARED_OPTIONS.untilAgent, collectUntil, []).option(...SHARED_OPTIONS.timeout, parseTimeout).option(...SHARED_OPTIONS.json).action(async (specId, options) => {
|
|
90046
90626
|
const code = await runAgentWait(context4, specId, options);
|
|
90047
90627
|
context4.setExitCode(code);
|
|
90048
90628
|
});
|
|
90049
|
-
agent.command("create").description("Write a team agent spec to .spur/agents/<id>.yaml.").option("--type <agent-type>", "Agent spec type for create").option("--tags <a,b>", "Team identity tags").option("--system-prompt <text>", "Team identity system prompt").option(
|
|
90629
|
+
agent.command("create").description("Write a team agent spec to .spur/agents/<id>.yaml.").option("--type <agent-type>", "Agent spec type for create").option("--tags <a,b>", "Team identity tags").option("--system-prompt <text>", "Team identity system prompt").option(...SHARED_OPTIONS.nameAgent).option("--workspace <path>", "Workspace path").option("--purpose <text>", "Team identity purpose").option("--auto-start", "Auto-start flag").option("--model <name>", "Agent model argument").option("--autonomy <level>", "Autonomy level").option("--no-identity-preamble", "Disable identity preamble").option(...SHARED_OPTIONS.json).argument("<id>", "Agent spec id").action(async (id, options) => {
|
|
90050
90630
|
const flags = commanderOptionsToFlags(options);
|
|
90051
90631
|
const code = await runAgentCreate(id, context4, flags);
|
|
90052
90632
|
context4.setExitCode(code);
|
|
@@ -90055,7 +90635,7 @@ function registerAgentCommand(program2, context4) {
|
|
|
90055
90635
|
const code = await runAgentEdit(id, context4);
|
|
90056
90636
|
context4.setExitCode(code);
|
|
90057
90637
|
});
|
|
90058
|
-
agent.command("delete").description("Remove an agent spec.").option(
|
|
90638
|
+
agent.command("delete").description("Remove an agent spec.").option(...SHARED_OPTIONS.forceAgentDelete).argument("<id>", "Agent spec id").action(async (id, options) => {
|
|
90059
90639
|
const flags = commanderOptionsToFlags(options);
|
|
90060
90640
|
const code = await runAgentDelete(id, context4, flags);
|
|
90061
90641
|
context4.setExitCode(code);
|
|
@@ -90457,6 +91037,533 @@ function waitFail(context4, options, code, message) {
|
|
|
90457
91037
|
return 1;
|
|
90458
91038
|
}
|
|
90459
91039
|
|
|
91040
|
+
// src/release-ops.ts
|
|
91041
|
+
init_dist5();
|
|
91042
|
+
import { existsSync as existsSync11, readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
|
|
91043
|
+
import { join as join24 } from "path";
|
|
91044
|
+
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
91045
|
+
function releaseTag(config4, version3) {
|
|
91046
|
+
return `${config4.packageName}${config4.tagVersionSeparator}${version3}`;
|
|
91047
|
+
}
|
|
91048
|
+
async function run(repoRoot, cmd) {
|
|
91049
|
+
const executor = new NodeProcessExecutor3({ defaultTimeout: 30000, defaultMaxOutput: 512000 });
|
|
91050
|
+
const result = await executor.run({
|
|
91051
|
+
command: cmd[0] ?? "",
|
|
91052
|
+
args: cmd.slice(1),
|
|
91053
|
+
cwd: repoRoot,
|
|
91054
|
+
forceBuffered: true
|
|
91055
|
+
});
|
|
91056
|
+
return {
|
|
91057
|
+
ok: result.exitCode === 0,
|
|
91058
|
+
stdout: result.stdout.toString().trim(),
|
|
91059
|
+
stderr: result.stderr.toString().trim()
|
|
91060
|
+
};
|
|
91061
|
+
}
|
|
91062
|
+
async function git2(repoRoot, args) {
|
|
91063
|
+
const result = await run(repoRoot, ["git", ...args]);
|
|
91064
|
+
if (!result.ok) {
|
|
91065
|
+
throw new Error(`git ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
|
|
91066
|
+
}
|
|
91067
|
+
return result.stdout;
|
|
91068
|
+
}
|
|
91069
|
+
async function npmViewVersion(repoRoot, name, version3) {
|
|
91070
|
+
const result = await run(repoRoot, ["npm", "view", `${name}@${version3}`, "version"]);
|
|
91071
|
+
return result.ok && result.stdout === version3;
|
|
91072
|
+
}
|
|
91073
|
+
async function readJson(path9) {
|
|
91074
|
+
const file2 = Bun.file(path9);
|
|
91075
|
+
if (!await file2.exists())
|
|
91076
|
+
return null;
|
|
91077
|
+
try {
|
|
91078
|
+
const parsed = await file2.json();
|
|
91079
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
91080
|
+
return null;
|
|
91081
|
+
return parsed;
|
|
91082
|
+
} catch {
|
|
91083
|
+
return null;
|
|
91084
|
+
}
|
|
91085
|
+
}
|
|
91086
|
+
function rootWorkspaceGlobs(repoRoot) {
|
|
91087
|
+
let root;
|
|
91088
|
+
try {
|
|
91089
|
+
root = JSON.parse(readFileSync11(join24(repoRoot, "package.json"), "utf8"));
|
|
91090
|
+
} catch {
|
|
91091
|
+
return [];
|
|
91092
|
+
}
|
|
91093
|
+
if (root === null || typeof root !== "object")
|
|
91094
|
+
return [];
|
|
91095
|
+
const raw = root.workspaces;
|
|
91096
|
+
const rawPackages = raw.packages;
|
|
91097
|
+
const list = Array.isArray(raw) ? raw : Array.isArray(rawPackages) ? rawPackages : [];
|
|
91098
|
+
return list.filter((g) => typeof g === "string");
|
|
91099
|
+
}
|
|
91100
|
+
function workspaceDirs(repoRoot) {
|
|
91101
|
+
const dirs = [];
|
|
91102
|
+
for (const glob of rootWorkspaceGlobs(repoRoot)) {
|
|
91103
|
+
if (!glob.endsWith("/*"))
|
|
91104
|
+
continue;
|
|
91105
|
+
const parent = glob.slice(0, -2);
|
|
91106
|
+
const parentAbs = join24(repoRoot, parent);
|
|
91107
|
+
if (!existsSync11(parentAbs))
|
|
91108
|
+
continue;
|
|
91109
|
+
for (const entry of readdirSync7(parentAbs, { withFileTypes: true })) {
|
|
91110
|
+
if (entry.isDirectory() && existsSync11(join24(parentAbs, entry.name, "package.json"))) {
|
|
91111
|
+
dirs.push(`${parent}/${entry.name}`);
|
|
91112
|
+
}
|
|
91113
|
+
}
|
|
91114
|
+
}
|
|
91115
|
+
return dirs;
|
|
91116
|
+
}
|
|
91117
|
+
async function releaseContext(repoRoot) {
|
|
91118
|
+
const rootManifest = await readJson(join24(repoRoot, "package.json"));
|
|
91119
|
+
if (rootManifest === null) {
|
|
91120
|
+
throw new Error(`no package.json found at ${repoRoot} \u2014 builder operates on a package workspace root.`);
|
|
91121
|
+
}
|
|
91122
|
+
const rootName = typeof rootManifest.name === "string" ? rootManifest.name : "root";
|
|
91123
|
+
const workspaces2 = [];
|
|
91124
|
+
for (const dir of workspaceDirs(repoRoot)) {
|
|
91125
|
+
const manifest = await readJson(join24(repoRoot, dir, "package.json"));
|
|
91126
|
+
if (manifest === null)
|
|
91127
|
+
continue;
|
|
91128
|
+
workspaces2.push({
|
|
91129
|
+
dir,
|
|
91130
|
+
name: typeof manifest.name === "string" ? manifest.name : dir,
|
|
91131
|
+
version: typeof manifest.version === "string" ? manifest.version : "0.0.0",
|
|
91132
|
+
manifest
|
|
91133
|
+
});
|
|
91134
|
+
}
|
|
91135
|
+
const shortName = (name) => name.split("/").pop() ?? name;
|
|
91136
|
+
const packages = new Map;
|
|
91137
|
+
for (const ws of workspaces2) {
|
|
91138
|
+
const versionSourceFile = existsSync11(join24(repoRoot, ws.dir, "src", "config.ts")) ? `${ws.dir}/src/config.ts` : undefined;
|
|
91139
|
+
packages.set(shortName(ws.name), {
|
|
91140
|
+
packageDir: ws.dir,
|
|
91141
|
+
packageName: ws.name,
|
|
91142
|
+
versionSourceFile,
|
|
91143
|
+
tagVersionSeparator: "-v",
|
|
91144
|
+
publishWorkflow: "publish.yml",
|
|
91145
|
+
releaseCommitType: "chore",
|
|
91146
|
+
releaseCommitScope: "release",
|
|
91147
|
+
releaseCommitSubject: (version3) => `bump ${shortName(ws.name)} to ${version3}`,
|
|
91148
|
+
releaseTagMessage: (tag) => `release: ${tag}`,
|
|
91149
|
+
ghRunListLimit: 5
|
|
91150
|
+
});
|
|
91151
|
+
}
|
|
91152
|
+
const pinnedNames = new Set;
|
|
91153
|
+
for (const ws of workspaces2) {
|
|
91154
|
+
for (const field2 of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
91155
|
+
const deps = ws.manifest[field2];
|
|
91156
|
+
if (deps === null || typeof deps !== "object" || Array.isArray(deps))
|
|
91157
|
+
continue;
|
|
91158
|
+
for (const [dep, spec] of Object.entries(deps)) {
|
|
91159
|
+
if (typeof spec === "string" && spec.startsWith("workspace:") && dep !== ws.name) {
|
|
91160
|
+
pinnedNames.add(dep);
|
|
91161
|
+
}
|
|
91162
|
+
}
|
|
91163
|
+
}
|
|
91164
|
+
}
|
|
91165
|
+
pinnedNames.add(rootName);
|
|
91166
|
+
const allPackages = [...packages.values()].filter((c3) => pinnedNames.has(c3.packageName));
|
|
91167
|
+
return { repoRoot, rootName, packages, allPackages };
|
|
91168
|
+
}
|
|
91169
|
+
async function updateWorkspacePins(ctx, pkgName, oldVersion, newVersion, output2) {
|
|
91170
|
+
const changed = [];
|
|
91171
|
+
for (const dir of workspaceDirs(ctx.repoRoot)) {
|
|
91172
|
+
const relPath = `${dir}/package.json`;
|
|
91173
|
+
const manifest = await readJson(join24(ctx.repoRoot, relPath));
|
|
91174
|
+
if (manifest === null)
|
|
91175
|
+
continue;
|
|
91176
|
+
let dirty = false;
|
|
91177
|
+
const pin = `workspace:${oldVersion}`;
|
|
91178
|
+
const nextPin = `workspace:${newVersion}`;
|
|
91179
|
+
for (const field2 of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
91180
|
+
const deps = manifest[field2];
|
|
91181
|
+
if (deps === null || typeof deps !== "object" || Array.isArray(deps))
|
|
91182
|
+
continue;
|
|
91183
|
+
const map2 = deps;
|
|
91184
|
+
if (map2[pkgName] === pin) {
|
|
91185
|
+
map2[pkgName] = nextPin;
|
|
91186
|
+
dirty = true;
|
|
91187
|
+
}
|
|
91188
|
+
}
|
|
91189
|
+
if (dirty) {
|
|
91190
|
+
await Bun.write(join24(ctx.repoRoot, relPath), `${JSON.stringify(manifest, null, 4)}
|
|
91191
|
+
`);
|
|
91192
|
+
output2.write(` \u21B3 ${relPath}: ${pkgName} workspace pin ${oldVersion} \u2192 ${newVersion}`);
|
|
91193
|
+
changed.push(relPath);
|
|
91194
|
+
}
|
|
91195
|
+
}
|
|
91196
|
+
return changed;
|
|
91197
|
+
}
|
|
91198
|
+
async function updateVersionSourceFile(ctx, filePath, previous, next, output2) {
|
|
91199
|
+
const absPath = join24(ctx.repoRoot, filePath);
|
|
91200
|
+
const content = await Bun.file(absPath).text();
|
|
91201
|
+
const updated = content.replace(/(binaryVersion:\s*['"])\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?(['"])/, `$1${next}$2`);
|
|
91202
|
+
if (updated === content) {
|
|
91203
|
+
output2.write(` \u26A0 ${filePath}: binaryVersion pattern not found (expected ${previous})`);
|
|
91204
|
+
return false;
|
|
91205
|
+
}
|
|
91206
|
+
await Bun.write(absPath, updated);
|
|
91207
|
+
output2.write(` \u21B3 ${filePath}: binaryVersion ${previous} \u2192 ${next}`);
|
|
91208
|
+
return true;
|
|
91209
|
+
}
|
|
91210
|
+
async function syncMarketplaceAndPlugins(ctx, version3, staged, output2) {
|
|
91211
|
+
const marketplacePath = join24(ctx.repoRoot, ".claude-plugin", "marketplace.json");
|
|
91212
|
+
const marketplaceFile = Bun.file(marketplacePath);
|
|
91213
|
+
if (!await marketplaceFile.exists())
|
|
91214
|
+
return;
|
|
91215
|
+
const marketplace = await readJson(marketplacePath);
|
|
91216
|
+
const rawPlugins = marketplace?.plugins;
|
|
91217
|
+
if (marketplace === null || !Array.isArray(rawPlugins))
|
|
91218
|
+
return;
|
|
91219
|
+
const isPlugin = (entry) => entry !== null && typeof entry === "object" && typeof entry.name === "string" && typeof entry.version === "string" && typeof entry.source === "string";
|
|
91220
|
+
const plugins = [];
|
|
91221
|
+
for (const entry of rawPlugins) {
|
|
91222
|
+
if (isPlugin(entry)) {
|
|
91223
|
+
plugins.push({ name: entry.name, version: entry.version, source: entry.source });
|
|
91224
|
+
}
|
|
91225
|
+
}
|
|
91226
|
+
let mpUpdated = false;
|
|
91227
|
+
for (const entry of plugins) {
|
|
91228
|
+
if (entry.version !== version3) {
|
|
91229
|
+
entry.version = version3;
|
|
91230
|
+
mpUpdated = true;
|
|
91231
|
+
}
|
|
91232
|
+
}
|
|
91233
|
+
if (mpUpdated) {
|
|
91234
|
+
marketplace.plugins = plugins;
|
|
91235
|
+
await Bun.write(marketplacePath, `${JSON.stringify(marketplace, null, 4)}
|
|
91236
|
+
`);
|
|
91237
|
+
staged.push(".claude-plugin/marketplace.json");
|
|
91238
|
+
output2.write(`Bumped marketplace plugins to ${version3}`);
|
|
91239
|
+
}
|
|
91240
|
+
for (const entry of plugins) {
|
|
91241
|
+
const pluginJsonPath = join24(ctx.repoRoot, entry.source, "plugin.json");
|
|
91242
|
+
const pluginJson = await readJson(pluginJsonPath);
|
|
91243
|
+
if (pluginJson === null) {
|
|
91244
|
+
output2.write(` \u26A0 plugin.json not found at ${entry.source}/plugin.json \u2014 skipping`);
|
|
91245
|
+
continue;
|
|
91246
|
+
}
|
|
91247
|
+
if (pluginJson.version !== version3) {
|
|
91248
|
+
pluginJson.version = version3;
|
|
91249
|
+
await Bun.write(pluginJsonPath, `${JSON.stringify(pluginJson, null, 4)}
|
|
91250
|
+
`);
|
|
91251
|
+
staged.push(`${entry.source}/plugin.json`);
|
|
91252
|
+
output2.write(`Bumped ${entry.source}/plugin.json to ${version3}`);
|
|
91253
|
+
}
|
|
91254
|
+
}
|
|
91255
|
+
}
|
|
91256
|
+
async function assertCleanTreeOnBranch(ctx) {
|
|
91257
|
+
if (await git2(ctx.repoRoot, ["status", "--porcelain"]) !== "") {
|
|
91258
|
+
throw new Error("working tree is not clean. Commit or stash changes before releasing.");
|
|
91259
|
+
}
|
|
91260
|
+
const branch = await git2(ctx.repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
91261
|
+
if (branch === "HEAD") {
|
|
91262
|
+
throw new Error("detached HEAD \u2014 checkout a branch before releasing.");
|
|
91263
|
+
}
|
|
91264
|
+
return branch;
|
|
91265
|
+
}
|
|
91266
|
+
async function assertTagFree(ctx, config4, tag, version3, output2) {
|
|
91267
|
+
const localTags = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
|
|
91268
|
+
`).filter(Boolean));
|
|
91269
|
+
if (localTags.has(tag)) {
|
|
91270
|
+
throw new Error(`tag already exists locally: ${tag}. Run "spur builder drop-tags ${config4.packageName.split("/").pop()} ${version3}" first.`);
|
|
91271
|
+
}
|
|
91272
|
+
const remote = await run(ctx.repoRoot, ["git", "ls-remote", "--tags", "origin"]);
|
|
91273
|
+
if (remote.ok && remote.stdout.includes(`refs/tags/${tag}`)) {
|
|
91274
|
+
throw new Error(`tag already exists on origin: ${tag}. Run "spur builder drop-tags ${config4.packageName.split("/").pop()} ${version3} --remote" first.`);
|
|
91275
|
+
}
|
|
91276
|
+
if (!remote.ok) {
|
|
91277
|
+
output2.write("warning: could not check origin for tag clashes (remote unreachable); continuing.");
|
|
91278
|
+
}
|
|
91279
|
+
}
|
|
91280
|
+
async function bumpVersion(ctx, config4, version3, options, output2) {
|
|
91281
|
+
if (!SEMVER.test(version3)) {
|
|
91282
|
+
throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.4).`);
|
|
91283
|
+
}
|
|
91284
|
+
const tag = releaseTag(config4, version3);
|
|
91285
|
+
const branch = await assertCleanTreeOnBranch(ctx);
|
|
91286
|
+
await assertTagFree(ctx, config4, tag, version3, output2);
|
|
91287
|
+
if (await npmViewVersion(ctx.repoRoot, config4.packageName, version3)) {
|
|
91288
|
+
throw new Error(`${config4.packageName}@${version3} is already published on npm. Use a new version.`);
|
|
91289
|
+
}
|
|
91290
|
+
const manifestPath = join24(ctx.repoRoot, config4.packageDir, "package.json");
|
|
91291
|
+
const manifest = await readJson(manifestPath);
|
|
91292
|
+
if (manifest === null)
|
|
91293
|
+
throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
|
|
91294
|
+
const previous = typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
91295
|
+
manifest.version = version3;
|
|
91296
|
+
await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 4)}
|
|
91297
|
+
`);
|
|
91298
|
+
output2.write(`${config4.packageName}: ${previous} -> ${version3}`);
|
|
91299
|
+
const staged = [`${config4.packageDir}/package.json`];
|
|
91300
|
+
if (config4.versionSourceFile) {
|
|
91301
|
+
const updated = await updateVersionSourceFile(ctx, config4.versionSourceFile, previous, version3, output2);
|
|
91302
|
+
if (updated)
|
|
91303
|
+
staged.push(config4.versionSourceFile);
|
|
91304
|
+
}
|
|
91305
|
+
staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
|
|
91306
|
+
await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
|
|
91307
|
+
const lockPath = join24(ctx.repoRoot, "bun.lock");
|
|
91308
|
+
if (existsSync11(lockPath) && Bun.file(lockPath).size > 0)
|
|
91309
|
+
staged.push("bun.lock");
|
|
91310
|
+
await git2(ctx.repoRoot, ["add", ...staged]);
|
|
91311
|
+
const message = `${config4.releaseCommitType}(${config4.releaseCommitScope}): ${config4.releaseCommitSubject(version3)}`;
|
|
91312
|
+
await git2(ctx.repoRoot, ["commit", "-m", message]);
|
|
91313
|
+
output2.write(`Committed: ${message}`);
|
|
91314
|
+
await git2(ctx.repoRoot, ["tag", "-a", tag, "-m", config4.releaseTagMessage(tag)]);
|
|
91315
|
+
output2.write(`Tagged: ${tag}`);
|
|
91316
|
+
if (!options.push) {
|
|
91317
|
+
output2.write(`
|
|
91318
|
+
Done (local). Review, then push to release:`);
|
|
91319
|
+
output2.write(` git push origin ${branch}`);
|
|
91320
|
+
output2.write(` git push origin ${tag}`);
|
|
91321
|
+
output2.write("Or re-run with --push next time to do this automatically.");
|
|
91322
|
+
return;
|
|
91323
|
+
}
|
|
91324
|
+
output2.write(`
|
|
91325
|
+
Pushing branch ${branch} (tags excluded)...`);
|
|
91326
|
+
await git2(ctx.repoRoot, ["push", "origin", branch]);
|
|
91327
|
+
output2.write(`Pushing release trigger tag ${tag}...`);
|
|
91328
|
+
await git2(ctx.repoRoot, ["push", "origin", tag]);
|
|
91329
|
+
output2.write(`
|
|
91330
|
+
Released ${version3}. The publish workflow should now be running:`);
|
|
91331
|
+
output2.write(` gh run list --workflow=${config4.publishWorkflow} --limit ${config4.ghRunListLimit}`);
|
|
91332
|
+
}
|
|
91333
|
+
async function bumpAll(ctx, version3, options, output2) {
|
|
91334
|
+
if (!SEMVER.test(version3)) {
|
|
91335
|
+
throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.4).`);
|
|
91336
|
+
}
|
|
91337
|
+
if (ctx.allPackages.length === 0) {
|
|
91338
|
+
throw new Error('no workspace packages are pinned via "workspace:" by another package \u2014 nothing to bump.');
|
|
91339
|
+
}
|
|
91340
|
+
const branch = await assertCleanTreeOnBranch(ctx);
|
|
91341
|
+
const configs = ctx.allPackages;
|
|
91342
|
+
const aggregateTag = `${ctx.rootName}-v${version3}`;
|
|
91343
|
+
const existingLocal = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
|
|
91344
|
+
`).filter(Boolean));
|
|
91345
|
+
const allTags = [aggregateTag, ...configs.map((c3) => releaseTag(c3, version3))];
|
|
91346
|
+
for (const tag of allTags) {
|
|
91347
|
+
if (existingLocal.has(tag))
|
|
91348
|
+
throw new Error(`tag already exists locally: ${tag}`);
|
|
91349
|
+
}
|
|
91350
|
+
const remoteRefs = await git2(ctx.repoRoot, ["ls-remote", "--tags", "origin"]);
|
|
91351
|
+
for (const tag of allTags) {
|
|
91352
|
+
if (remoteRefs.includes(`refs/tags/${tag}`)) {
|
|
91353
|
+
throw new Error(`tag already exists on origin: ${tag}. Run "spur builder drop-tags --all ${version3} --remote" first.`);
|
|
91354
|
+
}
|
|
91355
|
+
}
|
|
91356
|
+
for (const config4 of configs) {
|
|
91357
|
+
if (await npmViewVersion(ctx.repoRoot, config4.packageName, version3)) {
|
|
91358
|
+
throw new Error(`${config4.packageName}@${version3} is already published on npm. Use a new version.`);
|
|
91359
|
+
}
|
|
91360
|
+
}
|
|
91361
|
+
const staged = [];
|
|
91362
|
+
for (const config4 of configs) {
|
|
91363
|
+
const manifestPath = join24(ctx.repoRoot, config4.packageDir, "package.json");
|
|
91364
|
+
const manifest = await readJson(manifestPath);
|
|
91365
|
+
if (manifest === null) {
|
|
91366
|
+
throw new Error(`missing or malformed manifest at ${config4.packageDir}/package.json`);
|
|
91367
|
+
}
|
|
91368
|
+
const previous = typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
91369
|
+
manifest.version = version3;
|
|
91370
|
+
await Bun.write(manifestPath, `${JSON.stringify(manifest, null, 4)}
|
|
91371
|
+
`);
|
|
91372
|
+
output2.write(`${config4.packageName}: ${previous} -> ${version3}`);
|
|
91373
|
+
staged.push(`${config4.packageDir}/package.json`);
|
|
91374
|
+
if (config4.versionSourceFile) {
|
|
91375
|
+
const updated = await updateVersionSourceFile(ctx, config4.versionSourceFile, previous, version3, output2);
|
|
91376
|
+
if (updated)
|
|
91377
|
+
staged.push(config4.versionSourceFile);
|
|
91378
|
+
}
|
|
91379
|
+
staged.push(...await updateWorkspacePins(ctx, config4.packageName, previous, version3, output2));
|
|
91380
|
+
}
|
|
91381
|
+
await syncMarketplaceAndPlugins(ctx, version3, staged, output2);
|
|
91382
|
+
const lockPath = join24(ctx.repoRoot, "bun.lock");
|
|
91383
|
+
if (existsSync11(lockPath) && Bun.file(lockPath).size > 0)
|
|
91384
|
+
staged.push("bun.lock");
|
|
91385
|
+
await git2(ctx.repoRoot, ["add", ...staged]);
|
|
91386
|
+
const shortNames = configs.map((c3) => c3.packageName.split("/").pop() ?? c3.packageName).join(" + ");
|
|
91387
|
+
const message = `chore(release): bump ${shortNames} to ${version3}`;
|
|
91388
|
+
await git2(ctx.repoRoot, ["commit", "-m", message]);
|
|
91389
|
+
output2.write(`Committed: ${message}`);
|
|
91390
|
+
for (const config4 of configs) {
|
|
91391
|
+
const tag = releaseTag(config4, version3);
|
|
91392
|
+
if (tag !== aggregateTag) {
|
|
91393
|
+
await git2(ctx.repoRoot, ["tag", "-a", tag, "-m", config4.releaseTagMessage(tag)]);
|
|
91394
|
+
output2.write(`Tagged (trace): ${tag}`);
|
|
91395
|
+
}
|
|
91396
|
+
}
|
|
91397
|
+
await git2(ctx.repoRoot, ["tag", "-a", aggregateTag, "-m", `${ctx.rootName} ${version3} \u2014 ${shortNames}`]);
|
|
91398
|
+
output2.write(`Tagged (publish): ${aggregateTag}`);
|
|
91399
|
+
if (!options.push) {
|
|
91400
|
+
output2.write(`
|
|
91401
|
+
Done (local). Review, then push to release:`);
|
|
91402
|
+
output2.write(` git push origin ${branch}`);
|
|
91403
|
+
output2.write(` git push origin ${aggregateTag}`);
|
|
91404
|
+
output2.write("Or re-run with --push next time to do this automatically.");
|
|
91405
|
+
return;
|
|
91406
|
+
}
|
|
91407
|
+
output2.write(`
|
|
91408
|
+
Pushing branch ${branch} (tags excluded)...`);
|
|
91409
|
+
await git2(ctx.repoRoot, ["push", "origin", branch]);
|
|
91410
|
+
output2.write(`Pushing release trigger tag ${aggregateTag}...`);
|
|
91411
|
+
await git2(ctx.repoRoot, ["push", "origin", aggregateTag]);
|
|
91412
|
+
output2.write(`
|
|
91413
|
+
Released ${version3}. The publish workflow should now be running:`);
|
|
91414
|
+
output2.write(" gh run list --workflow=publish.yml --limit 3");
|
|
91415
|
+
}
|
|
91416
|
+
async function dropTagsFor(ctx, config4, version3, options, output2) {
|
|
91417
|
+
if (!SEMVER.test(version3)) {
|
|
91418
|
+
throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.2).`);
|
|
91419
|
+
}
|
|
91420
|
+
const tag = releaseTag(config4, version3);
|
|
91421
|
+
const localTags = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
|
|
91422
|
+
`).filter(Boolean));
|
|
91423
|
+
if (localTags.has(tag)) {
|
|
91424
|
+
await git2(ctx.repoRoot, ["tag", "-d", tag]);
|
|
91425
|
+
output2.write(`Deleted local tag ${tag}`);
|
|
91426
|
+
} else {
|
|
91427
|
+
output2.write(`No local tag ${tag}`);
|
|
91428
|
+
}
|
|
91429
|
+
if (options.remote) {
|
|
91430
|
+
const result = await run(ctx.repoRoot, ["git", "push", "origin", `:refs/tags/${tag}`]);
|
|
91431
|
+
output2.write(result.ok ? `Deleted remote tag ${tag}` : `Remote tag ${tag} not present or already removed`);
|
|
91432
|
+
}
|
|
91433
|
+
}
|
|
91434
|
+
async function dropAll(ctx, version3, options, output2) {
|
|
91435
|
+
if (!SEMVER.test(version3)) {
|
|
91436
|
+
throw new Error(`"${version3}" is not a valid semver version (expected e.g. 0.1.2).`);
|
|
91437
|
+
}
|
|
91438
|
+
for (const config4 of ctx.allPackages) {
|
|
91439
|
+
await dropTagsFor(ctx, config4, version3, options, output2);
|
|
91440
|
+
}
|
|
91441
|
+
const aggregateTag = `${ctx.rootName}-v${version3}`;
|
|
91442
|
+
const localTags = new Set((await git2(ctx.repoRoot, ["tag", "-l"])).split(`
|
|
91443
|
+
`).filter(Boolean));
|
|
91444
|
+
if (localTags.has(aggregateTag)) {
|
|
91445
|
+
await git2(ctx.repoRoot, ["tag", "-d", aggregateTag]);
|
|
91446
|
+
output2.write(`Deleted local tag ${aggregateTag}`);
|
|
91447
|
+
}
|
|
91448
|
+
if (options.remote) {
|
|
91449
|
+
const result = await run(ctx.repoRoot, ["git", "push", "origin", `:refs/tags/${aggregateTag}`]);
|
|
91450
|
+
output2.write(result.ok ? `Deleted remote tag ${aggregateTag}` : `Remote tag ${aggregateTag} not present or already removed`);
|
|
91451
|
+
}
|
|
91452
|
+
}
|
|
91453
|
+
function releaseUsage(message) {
|
|
91454
|
+
const usage = [
|
|
91455
|
+
"Usage:",
|
|
91456
|
+
" bump-ver <version> | bump-ver --all <version> [--push] bump all released packages",
|
|
91457
|
+
" bump-ver <package-id> <version> [--push] bump one package, commit, tag",
|
|
91458
|
+
" drop-tags <version> | drop-tags --all <version> [--remote] drop all release tags",
|
|
91459
|
+
" drop-tags <package-id> <version> [--remote] drop one package release tag"
|
|
91460
|
+
].join(`
|
|
91461
|
+
`);
|
|
91462
|
+
return new Error(message === undefined ? usage : `${message}
|
|
91463
|
+
|
|
91464
|
+
${usage}`);
|
|
91465
|
+
}
|
|
91466
|
+
async function bumpVer(args, repoRoot = process.cwd(), output2 = consoleOutput) {
|
|
91467
|
+
const ctx = await releaseContext(repoRoot);
|
|
91468
|
+
const positional = args.filter((arg) => !arg.startsWith("--"));
|
|
91469
|
+
if (args.includes("--all") || positional.length === 1 && SEMVER.test(positional[0] ?? "")) {
|
|
91470
|
+
const allVersion = positional[0];
|
|
91471
|
+
if (!allVersion)
|
|
91472
|
+
throw releaseUsage("bump-ver [--all] <version> [--push]");
|
|
91473
|
+
await bumpAll(ctx, allVersion, { push: args.includes("--push") }, output2);
|
|
91474
|
+
return;
|
|
91475
|
+
}
|
|
91476
|
+
const packageId = positional[0];
|
|
91477
|
+
const version3 = positional[1];
|
|
91478
|
+
if (!packageId || !version3)
|
|
91479
|
+
throw releaseUsage("bump-ver <version> | bump-ver <package-id> <version> [--push]");
|
|
91480
|
+
const config4 = ctx.packages.get(packageId);
|
|
91481
|
+
if (config4 === undefined) {
|
|
91482
|
+
throw releaseUsage(`unknown package "${packageId}". Package IDs: ${[...ctx.packages.keys()].join(", ")}`);
|
|
91483
|
+
}
|
|
91484
|
+
await bumpVersion(ctx, config4, version3, { push: args.includes("--push") }, output2);
|
|
91485
|
+
}
|
|
91486
|
+
async function dropTags(args, repoRoot = process.cwd(), output2 = consoleOutput) {
|
|
91487
|
+
const ctx = await releaseContext(repoRoot);
|
|
91488
|
+
const positional = args.filter((arg) => !arg.startsWith("--"));
|
|
91489
|
+
if (args.includes("--all") || positional.length === 1 && SEMVER.test(positional[0] ?? "")) {
|
|
91490
|
+
const allVersion = positional[0];
|
|
91491
|
+
if (!allVersion)
|
|
91492
|
+
throw releaseUsage("drop-tags [--all] <version> [--remote]");
|
|
91493
|
+
await dropAll(ctx, allVersion, { remote: args.includes("--remote") }, output2);
|
|
91494
|
+
return;
|
|
91495
|
+
}
|
|
91496
|
+
const packageId = positional[0];
|
|
91497
|
+
const version3 = positional[1];
|
|
91498
|
+
if (!packageId || !version3) {
|
|
91499
|
+
throw releaseUsage("drop-tags <version> | drop-tags <package-id> <version> [--remote]");
|
|
91500
|
+
}
|
|
91501
|
+
const config4 = ctx.packages.get(packageId);
|
|
91502
|
+
if (config4 === undefined) {
|
|
91503
|
+
throw releaseUsage(`unknown package "${packageId}". Package IDs: ${[...ctx.packages.keys()].join(", ")}`);
|
|
91504
|
+
}
|
|
91505
|
+
await dropTagsFor(ctx, config4, version3, { remote: args.includes("--remote") }, output2);
|
|
91506
|
+
}
|
|
91507
|
+
|
|
91508
|
+
// src/commands/builder.ts
|
|
91509
|
+
function registerBuilderCommand(program2, context4) {
|
|
91510
|
+
const noun = program2.command("builder").summary("release plumbing: version bumps and release tags");
|
|
91511
|
+
noun.command("bump-ver").summary("bump a package version, commit, tag, optionally push").description([
|
|
91512
|
+
"Bump one workspace package (or every released package with --all),",
|
|
91513
|
+
"rewrite workspace pins, commit, and tag. A bare version bumps all:",
|
|
91514
|
+
" spur builder bump-ver 0.1.4 # every released package",
|
|
91515
|
+
" spur builder bump-ver spur 0.1.4 # one package by id"
|
|
91516
|
+
].join(`
|
|
91517
|
+
`)).option("--all", "bump every released package in one commit with per-package + aggregate tags").option("--push", "push the branch and release tag to origin").option(...SHARED_OPTIONS.json).argument("[target]", "package id, or the version itself when bumping all").argument("[version]", "target semver version").action(async (target, version3, options) => {
|
|
91518
|
+
const args = [target, version3].filter((value) => value !== undefined);
|
|
91519
|
+
if (options.all === true)
|
|
91520
|
+
args.push("--all");
|
|
91521
|
+
if (options.push === true)
|
|
91522
|
+
args.push("--push");
|
|
91523
|
+
try {
|
|
91524
|
+
await bumpVer(args, context4.cwd, context4.output);
|
|
91525
|
+
if (options.json === true) {
|
|
91526
|
+
context4.output.write(toJson2({ ok: true, verb: "bump-ver", target: target ?? "all", version: version3 ?? "" }));
|
|
91527
|
+
}
|
|
91528
|
+
} catch (err) {
|
|
91529
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91530
|
+
if (options.json === true) {
|
|
91531
|
+
context4.output.write(toJson2({ ok: false, verb: "bump-ver", error: message }));
|
|
91532
|
+
context4.setExitCode(1);
|
|
91533
|
+
return;
|
|
91534
|
+
}
|
|
91535
|
+
context4.output.error(message);
|
|
91536
|
+
context4.setExitCode(1);
|
|
91537
|
+
}
|
|
91538
|
+
});
|
|
91539
|
+
noun.command("drop-tags").summary("delete release tags locally and optionally on origin").description([
|
|
91540
|
+
"Delete one package release tag (or every released tag plus the aggregate",
|
|
91541
|
+
"with --all / a bare version). Use --remote to also delete on origin."
|
|
91542
|
+
].join(`
|
|
91543
|
+
`)).option("--all", "drop every released tag plus the aggregate tag").option("--remote", "also delete the tag(s) on origin").option(...SHARED_OPTIONS.json).argument("[target]", "package id, or the version itself when dropping all").argument("[version]", "semver version of the tags to drop").action(async (target, version3, options) => {
|
|
91544
|
+
const args = [target, version3].filter((value) => value !== undefined);
|
|
91545
|
+
if (options.all === true)
|
|
91546
|
+
args.push("--all");
|
|
91547
|
+
if (options.remote === true)
|
|
91548
|
+
args.push("--remote");
|
|
91549
|
+
try {
|
|
91550
|
+
await dropTags(args, context4.cwd, context4.output);
|
|
91551
|
+
if (options.json === true) {
|
|
91552
|
+
context4.output.write(toJson2({ ok: true, verb: "drop-tags", target: target ?? "all", version: version3 ?? "" }));
|
|
91553
|
+
}
|
|
91554
|
+
} catch (err) {
|
|
91555
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
91556
|
+
if (options.json === true) {
|
|
91557
|
+
context4.output.write(toJson2({ ok: false, verb: "drop-tags", error: message }));
|
|
91558
|
+
context4.setExitCode(1);
|
|
91559
|
+
return;
|
|
91560
|
+
}
|
|
91561
|
+
context4.output.error(message);
|
|
91562
|
+
context4.setExitCode(1);
|
|
91563
|
+
}
|
|
91564
|
+
});
|
|
91565
|
+
}
|
|
91566
|
+
|
|
90460
91567
|
// src/commands/feature.ts
|
|
90461
91568
|
await init_src3();
|
|
90462
91569
|
|
|
@@ -90489,14 +91596,14 @@ function makePlanningEmitter(context4) {
|
|
|
90489
91596
|
init_loader();
|
|
90490
91597
|
init_src2();
|
|
90491
91598
|
await init_src3();
|
|
90492
|
-
import { existsSync as
|
|
91599
|
+
import { existsSync as existsSync12 } from "fs";
|
|
90493
91600
|
import { homedir as homedir8 } from "os";
|
|
90494
|
-
import { join as
|
|
91601
|
+
import { join as join25, resolve as resolve16 } from "path";
|
|
90495
91602
|
|
|
90496
91603
|
// src/workflow/resolve-spur-bin.ts
|
|
90497
|
-
import { basename as
|
|
91604
|
+
import { basename as basename8 } from "path";
|
|
90498
91605
|
function resolveSpurBinFrom(launch) {
|
|
90499
|
-
const runtime =
|
|
91606
|
+
const runtime = basename8(launch.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
90500
91607
|
const isJsRuntime = runtime === "bun" || runtime === "node";
|
|
90501
91608
|
if (isJsRuntime && launch.mainModule) {
|
|
90502
91609
|
return `${launch.execPath} ${launch.mainModule}`;
|
|
@@ -90509,23 +91616,23 @@ function resolveSpurBin() {
|
|
|
90509
91616
|
}
|
|
90510
91617
|
|
|
90511
91618
|
// src/workflow/make-lifecycle-adapter.ts
|
|
90512
|
-
var GLOBAL_CONFIG_DIR =
|
|
91619
|
+
var GLOBAL_CONFIG_DIR = join25(".config", "spur");
|
|
90513
91620
|
function globalConfigRoot(context4) {
|
|
90514
91621
|
const override = context4.env.SPUR_GLOBAL_RULES_DIR;
|
|
90515
|
-
return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) :
|
|
91622
|
+
return override !== undefined && override.length > 0 ? resolve16(context4.cwd, override) : join25(homedir8(), GLOBAL_CONFIG_DIR);
|
|
90516
91623
|
}
|
|
90517
91624
|
function resolveWorkflowPath(context4, profile) {
|
|
90518
91625
|
const bundledRoot = bundledConfigRoot();
|
|
90519
91626
|
if (bundledRoot !== null) {
|
|
90520
|
-
const bundledPath =
|
|
90521
|
-
if (
|
|
91627
|
+
const bundledPath = join25(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
|
|
91628
|
+
if (existsSync12(bundledPath))
|
|
90522
91629
|
return bundledPath;
|
|
90523
91630
|
}
|
|
90524
|
-
const projectPath =
|
|
90525
|
-
if (
|
|
91631
|
+
const projectPath = join25(context4.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
|
|
91632
|
+
if (existsSync12(projectPath))
|
|
90526
91633
|
return projectPath;
|
|
90527
|
-
const globalPath =
|
|
90528
|
-
if (
|
|
91634
|
+
const globalPath = join25(globalConfigRoot(context4), "workflows", `${profile.workflowName}.yaml`);
|
|
91635
|
+
if (existsSync12(globalPath))
|
|
90529
91636
|
return globalPath;
|
|
90530
91637
|
return null;
|
|
90531
91638
|
}
|
|
@@ -90547,7 +91654,7 @@ function makeLifecycleAdapter(context4, profile) {
|
|
|
90547
91654
|
// src/commands/feature.ts
|
|
90548
91655
|
function registerFeatureCommand(program2, context4) {
|
|
90549
91656
|
const feature = program2.command("feature").summary("manage features (hierarchical IDs)");
|
|
90550
|
-
feature.command("create").summary("Create a feature; allocates a hierarchical ID (DD-14) under the create-lock.").argument("<name>", "Feature name").option("--parent <id>", "Parent feature ID (child gets the next free digit 1-9)").option(
|
|
91657
|
+
feature.command("create").summary("Create a feature; allocates a hierarchical ID (DD-14) under the create-lock.").argument("<name>", "Feature name").option("--parent <id>", "Parent feature ID (child gets the next free digit 1-9)").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (name, options) => {
|
|
90551
91658
|
const svc = await makeService(context4, options.folder);
|
|
90552
91659
|
try {
|
|
90553
91660
|
const result = await svc.create(name, options.parent);
|
|
@@ -90561,7 +91668,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90561
91668
|
context4.setExitCode(1);
|
|
90562
91669
|
}
|
|
90563
91670
|
});
|
|
90564
|
-
feature.command("show").alias("get").summary("Show a feature by ID.").argument("<id>", "Feature ID").option(
|
|
91671
|
+
feature.command("show").alias("get").summary("Show a feature by ID.").argument("<id>", "Feature ID").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
|
|
90565
91672
|
const svc = await makeService(context4, options.folder);
|
|
90566
91673
|
try {
|
|
90567
91674
|
const result = await svc.show(id);
|
|
@@ -90586,7 +91693,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90586
91693
|
"`--section` costs a failed write. List valid names first:",
|
|
90587
91694
|
"`spur task sections <wbs> list`."
|
|
90588
91695
|
].join(`
|
|
90589
|
-
`)).option("--field <key>", "Frontmatter field to set (e.g. priority)").option("--value <value>", "New value for --field").option(
|
|
91696
|
+
`)).option("--field <key>", "Frontmatter field to set (e.g. priority)").option("--value <value>", "New value for --field").option(...SHARED_OPTIONS.section).option(...SHARED_OPTIONS.fromFile).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, status, options) => {
|
|
90590
91697
|
const svc = await makeService(context4, options.folder);
|
|
90591
91698
|
try {
|
|
90592
91699
|
let result;
|
|
@@ -90642,7 +91749,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90642
91749
|
context4.setExitCode(1);
|
|
90643
91750
|
}
|
|
90644
91751
|
});
|
|
90645
|
-
feature.command("advance").summary("Walk a feature through the legal forward lifecycle path.").argument("<id>", "Feature ID to advance").option("--to <status>", "Target status (default: 'done')").option(
|
|
91752
|
+
feature.command("advance").summary("Walk a feature through the legal forward lifecycle path.").argument("<id>", "Feature ID to advance").option("--to <status>", "Target status (default: 'done')").option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
|
|
90646
91753
|
const svc = await makeService(context4, options.folder);
|
|
90647
91754
|
const target = options.to ?? "done";
|
|
90648
91755
|
const forwardPath = {
|
|
@@ -90701,7 +91808,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90701
91808
|
context4.setExitCode(1);
|
|
90702
91809
|
}
|
|
90703
91810
|
});
|
|
90704
|
-
feature.command("list").summary("List features with optional status/priority filters.").option(
|
|
91811
|
+
feature.command("list").summary("List features with optional status/priority filters.").option(...SHARED_OPTIONS.statusFilter).option(...SHARED_OPTIONS.priorityFilter).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
90705
91812
|
const svc = await makeService(context4, options.folder);
|
|
90706
91813
|
try {
|
|
90707
91814
|
let features = await svc.list();
|
|
@@ -90726,7 +91833,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90726
91833
|
context4.setExitCode(1);
|
|
90727
91834
|
}
|
|
90728
91835
|
});
|
|
90729
|
-
feature.command("move").summary("Move a feature to a new parent \u2014 cascade rename of the subtree (DD-14).").argument("<id>", "Feature ID to move").option("--parent <id>", "New parent feature ID (omit to move to a top-level group)").option(
|
|
91836
|
+
feature.command("move").summary("Move a feature to a new parent \u2014 cascade rename of the subtree (DD-14).").argument("<id>", "Feature ID to move").option("--parent <id>", "New parent feature ID (omit to move to a top-level group)").option(...SHARED_OPTIONS.dryRunFeatureMap).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
|
|
90730
91837
|
const svc = await makeService(context4, options.folder);
|
|
90731
91838
|
try {
|
|
90732
91839
|
const result = await svc.move(id, options.parent ?? null, { dryRun: options.dryRun === true });
|
|
@@ -90754,7 +91861,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90754
91861
|
"Does NOT change feature frontmatter status. For lifecycle alignment use `spur feature sync`.",
|
|
90755
91862
|
"Use after task create/link/done when the ## Tasks table is stale."
|
|
90756
91863
|
].join(`
|
|
90757
|
-
`)).option(
|
|
91864
|
+
`)).option(...SHARED_OPTIONS.featureTasksRewrite).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
90758
91865
|
const svc = await makeService(context4, options.folder);
|
|
90759
91866
|
try {
|
|
90760
91867
|
const result = await svc.refresh({ featureId: options.feature });
|
|
@@ -90770,7 +91877,7 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90770
91877
|
context4.setExitCode(1);
|
|
90771
91878
|
}
|
|
90772
91879
|
});
|
|
90773
|
-
feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option(
|
|
91880
|
+
feature.command("check").summary("Validate feature file(s) through the four-layer check (design \xA73).").argument("[id]", "Feature ID (validates all features in the folder when omitted)").option(...SHARED_OPTIONS.strictFeature).option(...SHARED_OPTIONS.asFeature0418).option(...SHARED_OPTIONS.folderFeatures).option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option(...SHARED_OPTIONS.json).action(async (id, options) => {
|
|
90774
91881
|
const resolved = await resolvePlanningFolders(context4.fs);
|
|
90775
91882
|
const featuresDir = options.folder ?? context4.fs.resolve(resolved.featuresDir);
|
|
90776
91883
|
const tasksDir = context4.fs.resolve(resolved.tasksDir);
|
|
@@ -90798,10 +91905,16 @@ function registerFeatureCommand(program2, context4) {
|
|
|
90798
91905
|
tasksDirs,
|
|
90799
91906
|
runDir: context4.fs.resolve(".spur/run"),
|
|
90800
91907
|
severityOverrides: resolved.severityOverrides,
|
|
90801
|
-
asStatus: options.as
|
|
91908
|
+
asStatus: options.as,
|
|
91909
|
+
fix: options.fix === true
|
|
90802
91910
|
});
|
|
90803
91911
|
results.push(result);
|
|
90804
91912
|
if (!json3) {
|
|
91913
|
+
if (result.repairs !== undefined && result.repairs.length > 0) {
|
|
91914
|
+
for (const r of result.repairs) {
|
|
91915
|
+
context4.output.write(` [FIX] ${r.kind} ${r.section}: ${r.detail}`);
|
|
91916
|
+
}
|
|
91917
|
+
}
|
|
90805
91918
|
context4.output.write(`
|
|
90806
91919
|
${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
90807
91920
|
for (const f of result.findings) {
|
|
@@ -90829,7 +91942,7 @@ ${result.id} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
90829
91942
|
"",
|
|
90830
91943
|
"Does NOT rewrite INDEX.md or ## Tasks tables. For stale rosters use `spur feature refresh`."
|
|
90831
91944
|
].join(`
|
|
90832
|
-
`)).argument("[id]", "Feature ID to sync (optional if --all is passed)").option("--all", "Sync all features with linked tasks").option(
|
|
91945
|
+
`)).argument("[id]", "Feature ID to sync (optional if --all is passed)").option("--all", "Sync all features with linked tasks").option(...SHARED_OPTIONS.dryRunFeatureSync).option(...SHARED_OPTIONS.forceFeatureReopen).option(...SHARED_OPTIONS.folderFeatures).option(...SHARED_OPTIONS.json).action(async (id, options) => {
|
|
90833
91946
|
const svc = await makeService(context4, options.folder);
|
|
90834
91947
|
try {
|
|
90835
91948
|
if (!options.all && !id) {
|
|
@@ -90922,7 +92035,7 @@ import { createRequire } from "module";
|
|
|
90922
92035
|
var CLI_CONFIG = {
|
|
90923
92036
|
binaryName: "spur",
|
|
90924
92037
|
binaryLabel: "spur",
|
|
90925
|
-
binaryVersion: "0.3.
|
|
92038
|
+
binaryVersion: "0.3.57",
|
|
90926
92039
|
configDir: ".spur",
|
|
90927
92040
|
configFile: ".spur/config.yaml",
|
|
90928
92041
|
databaseFile: ".spur/spur.db"
|
|
@@ -90949,7 +92062,7 @@ importer: @gobing-ai/ts-llm-jsonl-importer@${provenance.importer}`;
|
|
|
90949
92062
|
function registerHistoryCommand(program2, context4) {
|
|
90950
92063
|
const noun = program2.command("history").summary("import and analyze coding-agent history");
|
|
90951
92064
|
const makeService2 = () => new HistoryService({ getDb: () => context4.getDb(), agentConfig: context4.agentConfig });
|
|
90952
|
-
noun.command("import").description("Import agent conversation JSONL. `--source all` fans out across all sources with " + "per-source failure isolation (task 0470). A single source is the n=1 case of " + "the same contract \u2014 never two import paths.").option(
|
|
92065
|
+
noun.command("import").description("Import agent conversation JSONL. `--source all` fans out across all sources with " + "per-source failure isolation (task 0470). A single source is the n=1 case of " + "the same contract \u2014 never two import paths.").option(...SHARED_OPTIONS.sourceHistory, "all").option(...SHARED_OPTIONS.fileHistoryJsonl).option("--root <path>", "Scan a history root").option(...SHARED_OPTIONS.modeHistory).option(...SHARED_OPTIONS.dryRunHistoryScan).option("--source-timeout <ms>", "Per-source timeout in milliseconds (default 600000 = 10 min)", "600000").option(...SHARED_OPTIONS.jsonSupported).action(async (options) => {
|
|
90953
92066
|
const source = options.source ?? "all";
|
|
90954
92067
|
if (options.file && source === "all") {
|
|
90955
92068
|
context4.output.write(options.json ? toJson2({ status: "error", message: '--file requires a single --source, not "all".' }) : 'spur history import: --file requires a single --source, not "all".');
|
|
@@ -90988,7 +92101,7 @@ function registerHistoryCommand(program2, context4) {
|
|
|
90988
92101
|
${formatFanOutResult(fanOut)}`);
|
|
90989
92102
|
context4.setExitCode(fanOut.exitCode);
|
|
90990
92103
|
});
|
|
90991
|
-
noun.command("analyze").description("Aggregate imported history with SQL and write a versioned JSON artifact (Q1-Q10 forensic query set).").option("--since <iso>", "Inclusive lower bound on message timestamp").option("--until <iso>", "Inclusive upper bound on message timestamp").option(
|
|
92104
|
+
noun.command("analyze").description("Aggregate imported history with SQL and write a versioned JSON artifact (Q1-Q10 forensic query set).").option("--since <iso>", "Inclusive lower bound on message timestamp").option("--until <iso>", "Inclusive upper bound on message timestamp").option(...SHARED_OPTIONS.sourceHistory, "all").option("--session <id>", "Narrow to a single session id").option(...SHARED_OPTIONS.runHistory).option("--task <wbs>", "Narrow to a single task WBS").option("--top <n>", "Leaderboard depth for byTool/bySession", "20").option("--out <path>", "Write the artifact to this path instead of the dated reports dir").option(...SHARED_OPTIONS.jsonArtifact).action(async (options) => {
|
|
90992
92105
|
const svc = makeService2();
|
|
90993
92106
|
const source = options.source ?? "all";
|
|
90994
92107
|
const selector = {
|
|
@@ -91012,7 +92125,7 @@ ${formatFanOutResult(fanOut)}`);
|
|
|
91012
92125
|
daily: artifact.daily
|
|
91013
92126
|
}));
|
|
91014
92127
|
});
|
|
91015
|
-
noun.command("report").description("Render a previously-generated history artifact as a spend + forensic report. " + "Never opens the database \u2014 pure renderer of the analyze JSON. " + "--task / --top narrow the already-loaded artifact client-side (0564 R3).").argument("[path]", "Artifact JSON path (defaults to the latest.json pointer)").option(
|
|
92128
|
+
noun.command("report").description("Render a previously-generated history artifact as a spend + forensic report. " + "Never opens the database \u2014 pure renderer of the analyze JSON. " + "--task / --top narrow the already-loaded artifact client-side (0564 R3).").argument("[path]", "Artifact JSON path (defaults to the latest.json pointer)").option(...SHARED_OPTIONS.jsonParsedArtifact).option("--mode <name>", "Report mode: default | forensics (registry-resolved; unknown names fail)").option("--task <wbs>", "Narrow to a single task WBS the artifact was analyzed with").option("--top <n>", "Leaderboard depth for byTool/bySession (re-slices the artifact)").action(async (pathArg, options) => {
|
|
91016
92129
|
try {
|
|
91017
92130
|
const top = parseTopOrThrow(options.top);
|
|
91018
92131
|
const { report, artifactPath, resolution, artifact, banner } = runHistoryReport({
|
|
@@ -91047,7 +92160,7 @@ ${formatFanOutResult(fanOut)}`);
|
|
|
91047
92160
|
context4.setExitCode(1);
|
|
91048
92161
|
}
|
|
91049
92162
|
});
|
|
91050
|
-
noun.command("daily").description("Run-once daily pipeline: import-all (fan-out, per-source isolation) \u2192 analyze \u2192 write " + "artifact \u2192 prune reports older than 90 days (task 0470 R6). Import uses checkpoint " + "resume, so a missed night self-heals on the next run with no gap and no double-count.").option("--since <iso>", "Inclusive lower bound on message timestamp for the report (not the import)").option("--until <iso>", "Inclusive upper bound on message timestamp for the report").option("--source-timeout <ms>", "Per-source import timeout in milliseconds (default 600000 = 10 min)", "600000").option("--root <path>", "History root override (default: per-source platform dir)").option(
|
|
92163
|
+
noun.command("daily").description("Run-once daily pipeline: import-all (fan-out, per-source isolation) \u2192 analyze \u2192 write " + "artifact \u2192 prune reports older than 90 days (task 0470 R6). Import uses checkpoint " + "resume, so a missed night self-heals on the next run with no gap and no double-count.").option("--since <iso>", "Inclusive lower bound on message timestamp for the report (not the import)").option("--until <iso>", "Inclusive upper bound on message timestamp for the report").option("--source-timeout <ms>", "Per-source import timeout in milliseconds (default 600000 = 10 min)", "600000").option("--root <path>", "History root override (default: per-source platform dir)").option(...SHARED_OPTIONS.jsonDaily).option("--mode <name>", "Render the artifact as a .md sidecar in this mode after analyze (e.g. forensics)").action(async (options) => {
|
|
91051
92164
|
const svc = makeService2();
|
|
91052
92165
|
const sourceTimeout = Number.parseInt(options.sourceTimeout ?? "600000", 10) || 600000;
|
|
91053
92166
|
const bus = new EventBus;
|
|
@@ -91160,7 +92273,7 @@ function parseTopOrThrow(raw) {
|
|
|
91160
92273
|
function formatFanOutResult(r) {
|
|
91161
92274
|
const lines = ["history import (fan-out)", "sources:"];
|
|
91162
92275
|
for (const e of r.entries) {
|
|
91163
|
-
lines.push(` ${e.source}: ${e.status} (files=${e.files} messages=${e.messages})`);
|
|
92276
|
+
lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
|
|
91164
92277
|
}
|
|
91165
92278
|
if (r.warnings.length > 0) {
|
|
91166
92279
|
lines.push("warnings:");
|
|
@@ -91176,7 +92289,7 @@ function formatDailyResult(r) {
|
|
|
91176
92289
|
const lines = ["history daily"];
|
|
91177
92290
|
lines.push("import:");
|
|
91178
92291
|
for (const e of r.fanOut.entries) {
|
|
91179
|
-
lines.push(` ${e.source}: ${e.status} (files=${e.files} messages=${e.messages})`);
|
|
92292
|
+
lines.push(` ${e.source}: ${e.status} (files=${e.files} scanned, new-messages=${e.messages}, tool-calls=${e.toolCalls})`);
|
|
91180
92293
|
}
|
|
91181
92294
|
if (r.fanOut.warnings.length > 0) {
|
|
91182
92295
|
lines.push("warnings:");
|
|
@@ -91200,7 +92313,7 @@ init_loader();
|
|
|
91200
92313
|
init_src2();
|
|
91201
92314
|
init_dist10();
|
|
91202
92315
|
import { homedir as homedir9 } from "os";
|
|
91203
|
-
import { join as
|
|
92316
|
+
import { join as join26, resolve as resolve17 } from "path";
|
|
91204
92317
|
|
|
91205
92318
|
// src/config/scaffold-manifest.ts
|
|
91206
92319
|
var SCAFFOLD_MANIFEST = [
|
|
@@ -91247,8 +92360,8 @@ var SCAFFOLD_MANIFEST = [
|
|
|
91247
92360
|
];
|
|
91248
92361
|
|
|
91249
92362
|
// src/commands/init.ts
|
|
91250
|
-
var GLOBAL_CONFIG_DIR2 =
|
|
91251
|
-
var GLOBAL_RULES_DIR2 =
|
|
92363
|
+
var GLOBAL_CONFIG_DIR2 = join26(".config", "spur");
|
|
92364
|
+
var GLOBAL_RULES_DIR2 = join26(GLOBAL_CONFIG_DIR2, "rules");
|
|
91252
92365
|
var GLOBAL_CONFIG_EXAMPLE = "config.example.yaml";
|
|
91253
92366
|
var GLOBAL_CONFIG_FILE2 = "config.yaml";
|
|
91254
92367
|
var INDEXED_CONTEXT_MARKER = "## Indexed context";
|
|
@@ -91279,7 +92392,7 @@ If \`.spur/context/\` is absent, proceed normally. Never block work on its absen
|
|
|
91279
92392
|
`;
|
|
91280
92393
|
function globalRulesRoot(context4) {
|
|
91281
92394
|
const override = context4.env.SPUR_GLOBAL_RULES_DIR;
|
|
91282
|
-
return override !== undefined && override.length > 0 ? resolve17(context4.cwd, override) :
|
|
92395
|
+
return override !== undefined && override.length > 0 ? resolve17(context4.cwd, override) : join26(homedir9(), GLOBAL_RULES_DIR2);
|
|
91283
92396
|
}
|
|
91284
92397
|
async function writeIfNew(context4, path9, content, force, result) {
|
|
91285
92398
|
if (!force && await context4.fs.exists(path9)) {
|
|
@@ -91296,11 +92409,11 @@ async function seedGlobalRules(context4) {
|
|
|
91296
92409
|
const target = globalRulesRoot(context4);
|
|
91297
92410
|
let written = 0;
|
|
91298
92411
|
for (const relPath of await listBundledRuleFiles()) {
|
|
91299
|
-
const destination =
|
|
92412
|
+
const destination = join26(target, relPath);
|
|
91300
92413
|
if (await context4.fs.exists(destination))
|
|
91301
92414
|
continue;
|
|
91302
|
-
await context4.fs.ensureDir(
|
|
91303
|
-
await context4.fs.writeFile(destination, await context4.fs.readFile(
|
|
92415
|
+
await context4.fs.ensureDir(join26(target, ...relPath.split("/").slice(0, -1)));
|
|
92416
|
+
await context4.fs.writeFile(destination, await context4.fs.readFile(join26(source, relPath)));
|
|
91304
92417
|
written += 1;
|
|
91305
92418
|
}
|
|
91306
92419
|
return written;
|
|
@@ -91310,20 +92423,20 @@ async function seedGlobalConfig(context4) {
|
|
|
91310
92423
|
if (source === null)
|
|
91311
92424
|
return 0;
|
|
91312
92425
|
const globalOverride = context4.env.SPUR_GLOBAL_RULES_DIR;
|
|
91313
|
-
const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve17(context4.cwd, globalOverride) :
|
|
92426
|
+
const target = globalOverride !== undefined && globalOverride.length > 0 ? resolve17(context4.cwd, globalOverride) : join26(homedir9(), GLOBAL_CONFIG_DIR2);
|
|
91314
92427
|
let written = 0;
|
|
91315
92428
|
for (const relPath of listBundledConfigFiles()) {
|
|
91316
92429
|
if (relPath === GLOBAL_CONFIG_EXAMPLE)
|
|
91317
92430
|
continue;
|
|
91318
|
-
const destination =
|
|
92431
|
+
const destination = join26(target, relPath);
|
|
91319
92432
|
if (await context4.fs.exists(destination))
|
|
91320
92433
|
continue;
|
|
91321
|
-
await context4.fs.ensureDir(
|
|
91322
|
-
await context4.fs.writeFile(destination, await context4.fs.readFile(
|
|
92434
|
+
await context4.fs.ensureDir(join26(target, ...relPath.split("/").slice(0, -1)));
|
|
92435
|
+
await context4.fs.writeFile(destination, await context4.fs.readFile(join26(source, relPath)));
|
|
91323
92436
|
written += 1;
|
|
91324
92437
|
}
|
|
91325
|
-
const examplePath =
|
|
91326
|
-
const globalConfigPath =
|
|
92438
|
+
const examplePath = join26(source, GLOBAL_CONFIG_EXAMPLE);
|
|
92439
|
+
const globalConfigPath = join26(target, GLOBAL_CONFIG_FILE2);
|
|
91327
92440
|
if (await context4.fs.exists(examplePath) && !await context4.fs.exists(globalConfigPath)) {
|
|
91328
92441
|
await context4.fs.ensureDir(target);
|
|
91329
92442
|
await context4.fs.writeFile(globalConfigPath, await context4.fs.readFile(examplePath));
|
|
@@ -91331,13 +92444,13 @@ async function seedGlobalConfig(context4) {
|
|
|
91331
92444
|
}
|
|
91332
92445
|
return written;
|
|
91333
92446
|
}
|
|
91334
|
-
function registerInitCommand(program2, context4) {
|
|
91335
|
-
program2.command("init").summary("scaffold a local Spur project").option(
|
|
91336
|
-
const json3 =
|
|
91337
|
-
const force =
|
|
91338
|
-
const minimal =
|
|
91339
|
-
const projectName =
|
|
91340
|
-
const configPath =
|
|
92447
|
+
function registerInitCommand(program2, context4, options = {}) {
|
|
92448
|
+
program2.command("init", { hidden: options.hidden === true }).summary("scaffold a local Spur project").option(...SHARED_OPTIONS.nameProjectInit).option(...SHARED_OPTIONS.forceInitRecreate).option("--minimal", "Only write the minimal .spur scaffold").option(...SHARED_OPTIONS.json).action(async (options2) => {
|
|
92449
|
+
const json3 = options2.json === true;
|
|
92450
|
+
const force = options2.force === true;
|
|
92451
|
+
const minimal = options2.minimal === true;
|
|
92452
|
+
const projectName = options2.name ?? "default";
|
|
92453
|
+
const configPath = join26(context4.cwd, CLI_CONFIG.configFile);
|
|
91341
92454
|
if (!force && await context4.fs.exists(configPath)) {
|
|
91342
92455
|
const message = `Already initialized: ${CLI_CONFIG.configFile}. Use --force to overwrite.`;
|
|
91343
92456
|
context4.output.write(json3 ? toJson2({ ok: false, reason: "already-initialized", config: CLI_CONFIG.configFile }) : message);
|
|
@@ -91372,13 +92485,13 @@ function registerInitCommand(program2, context4) {
|
|
|
91372
92485
|
].join(`
|
|
91373
92486
|
`)}
|
|
91374
92487
|
`;
|
|
91375
|
-
await context4.fs.ensureDir(
|
|
92488
|
+
await context4.fs.ensureDir(join26(context4.cwd, CLI_CONFIG.configDir));
|
|
91376
92489
|
await context4.fs.writeFile(configPath, configYaml);
|
|
91377
92490
|
result.created.push(configPath);
|
|
91378
|
-
const agentsDir =
|
|
92491
|
+
const agentsDir = join26(context4.cwd, CLI_CONFIG.configDir, "agents");
|
|
91379
92492
|
await context4.fs.ensureDir(agentsDir);
|
|
91380
|
-
await writeIfNew(context4,
|
|
91381
|
-
const gitignorePath =
|
|
92493
|
+
await writeIfNew(context4, join26(agentsDir, ".gitkeep"), "", force, result);
|
|
92494
|
+
const gitignorePath = join26(context4.cwd, ".gitignore");
|
|
91382
92495
|
const contextEntry = ".spur/context/";
|
|
91383
92496
|
if (await context4.fs.exists(gitignorePath)) {
|
|
91384
92497
|
const existing = await context4.fs.readFile(gitignorePath);
|
|
@@ -91400,20 +92513,20 @@ ${contextEntry}
|
|
|
91400
92513
|
for (const relPath of listBundledProjectSeedFiles()) {
|
|
91401
92514
|
if (relPath.startsWith("templates/docs/"))
|
|
91402
92515
|
continue;
|
|
91403
|
-
const sourcePath =
|
|
92516
|
+
const sourcePath = join26(configRoot, relPath);
|
|
91404
92517
|
if (!await context4.fs.exists(sourcePath))
|
|
91405
92518
|
continue;
|
|
91406
|
-
const targetPath =
|
|
91407
|
-
await context4.fs.ensureDir(
|
|
92519
|
+
const targetPath = join26(context4.cwd, CLI_CONFIG.configDir, relPath);
|
|
92520
|
+
await context4.fs.ensureDir(join26(targetPath, ".."));
|
|
91408
92521
|
await writeIfNew(context4, targetPath, await context4.fs.readFile(sourcePath), force, result);
|
|
91409
92522
|
}
|
|
91410
92523
|
for (const entry of SCAFFOLD_MANIFEST) {
|
|
91411
|
-
const sourcePath =
|
|
92524
|
+
const sourcePath = join26(configRoot, entry.source);
|
|
91412
92525
|
if (!await context4.fs.exists(sourcePath))
|
|
91413
92526
|
continue;
|
|
91414
|
-
const baseDir = entry.root === true ? context4.cwd :
|
|
91415
|
-
const targetPath =
|
|
91416
|
-
await context4.fs.ensureDir(
|
|
92527
|
+
const baseDir = entry.root === true ? context4.cwd : join26(context4.cwd, CLI_CONFIG.configDir);
|
|
92528
|
+
const targetPath = join26(baseDir, entry.target);
|
|
92529
|
+
await context4.fs.ensureDir(join26(targetPath, ".."));
|
|
91417
92530
|
const entryForce = entry.preserve === true ? false : force;
|
|
91418
92531
|
let body = await context4.fs.readFile(sourcePath);
|
|
91419
92532
|
if (entry.target === "AGENTS.md") {
|
|
@@ -91426,7 +92539,7 @@ ${contextEntry}
|
|
|
91426
92539
|
}
|
|
91427
92540
|
}
|
|
91428
92541
|
}
|
|
91429
|
-
const agentsMdPath =
|
|
92542
|
+
const agentsMdPath = join26(context4.cwd, "AGENTS.md");
|
|
91430
92543
|
if (await context4.fs.exists(agentsMdPath)) {
|
|
91431
92544
|
const existing = await context4.fs.readFile(agentsMdPath);
|
|
91432
92545
|
if (!existing.includes(INDEXED_CONTEXT_MARKER)) {
|
|
@@ -91471,22 +92584,22 @@ var DEFAULT_FROM = "operator";
|
|
|
91471
92584
|
var DEFAULT_WATCH_INTERVAL_MS = 2000;
|
|
91472
92585
|
function registerMessageCommand(program2, context4) {
|
|
91473
92586
|
const noun = program2.command("message").summary("send and inspect durable inter-agent messages");
|
|
91474
|
-
noun.command("send").description("Enqueue a message for an agent. Use --wait to block until the recipient reaches a state.").argument("<body>", "Message body").requiredOption("--to <id>", "Recipient agent id").option("--from <id>", "Sender id", DEFAULT_FROM).option("--wait", "Block until the recipient occupant reaches --until (default: invoke-exit)").option(
|
|
92587
|
+
noun.command("send").description("Enqueue a message for an agent. Use --wait to block until the recipient reaches a state.").argument("<body>", "Message body").requiredOption("--to <id>", "Recipient agent id").option("--from <id>", "Sender id", DEFAULT_FROM).option("--wait", "Block until the recipient occupant reaches --until (default: invoke-exit)").option(...SHARED_OPTIONS.untilMessage, collectSendUntil, []).option(...SHARED_OPTIONS.timeout, parseTimeout2).option(...SHARED_OPTIONS.json).action(async (body, options) => {
|
|
91475
92588
|
const svc = new TeamService(context4);
|
|
91476
92589
|
const code = await runMessageSend(svc, context4, body, options);
|
|
91477
92590
|
context4.setExitCode(code);
|
|
91478
92591
|
});
|
|
91479
|
-
noun.command("inbox").description("List messages addressed to an agent.").requiredOption(
|
|
92592
|
+
noun.command("inbox").description("List messages addressed to an agent.").requiredOption(...SHARED_OPTIONS.agentIdMessage).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
91480
92593
|
const svc = new TeamService(context4);
|
|
91481
92594
|
const code = await runMessageInbox(svc, context4, options);
|
|
91482
92595
|
context4.setExitCode(code);
|
|
91483
92596
|
});
|
|
91484
|
-
noun.command("reply").description("Thread a reply to a message.").argument("<msg-id>", "Message id to reply to").argument("<body>", "Reply body").option(
|
|
92597
|
+
noun.command("reply").description("Thread a reply to a message.").argument("<msg-id>", "Message id to reply to").argument("<body>", "Reply body").option(...SHARED_OPTIONS.json).action(async (msgId, body, options) => {
|
|
91485
92598
|
const svc = new TeamService(context4);
|
|
91486
92599
|
const code = await runMessageReply(svc, context4, msgId, body, options);
|
|
91487
92600
|
context4.setExitCode(code);
|
|
91488
92601
|
});
|
|
91489
|
-
noun.command("watch").description("Follow an agent inbox \u2014 surface new messages as they arrive (Ctrl-C to exit).").requiredOption(
|
|
92602
|
+
noun.command("watch").description("Follow an agent inbox \u2014 surface new messages as they arrive (Ctrl-C to exit).").requiredOption(...SHARED_OPTIONS.agentIdWatch).option("--interval <ms>", "Poll interval in milliseconds", String(DEFAULT_WATCH_INTERVAL_MS)).option(...SHARED_OPTIONS.jsonMessageStream).action(async (options) => {
|
|
91490
92603
|
const svc = new TeamService(context4);
|
|
91491
92604
|
const intervalMs = parseInterval2(options.interval);
|
|
91492
92605
|
if (intervalMs === null) {
|
|
@@ -91769,31 +92882,31 @@ async function waitForPendingDrain(agentService, teamService, pin, timeoutMs, si
|
|
|
91769
92882
|
|
|
91770
92883
|
// src/commands/migrate.ts
|
|
91771
92884
|
init_src2();
|
|
91772
|
-
import { join as
|
|
91773
|
-
function registerMigrateCommand(program2, context4) {
|
|
91774
|
-
program2.command("migrate").summary("apply CLI-owned schema migrations").option(
|
|
91775
|
-
const migrations = await loadSqlMigrations(
|
|
92885
|
+
import { join as join27 } from "path";
|
|
92886
|
+
function registerMigrateCommand(program2, context4, options = {}) {
|
|
92887
|
+
program2.command("migrate", { hidden: options.hidden === true }).summary("apply CLI-owned schema migrations").option(...SHARED_OPTIONS.json).action(async (options2) => {
|
|
92888
|
+
const migrations = await loadSqlMigrations(join27(context4.cwd, "drizzle")).catch(() => {
|
|
91776
92889
|
return;
|
|
91777
92890
|
});
|
|
91778
92891
|
const applied = await applyCliMigrations(await context4.getDb(), migrations);
|
|
91779
92892
|
const result = { ok: true, applied };
|
|
91780
|
-
context4.output.write(
|
|
92893
|
+
context4.output.write(options2.json === true ? toJson2(result) : `Database migrations complete (${applied} applied)`);
|
|
91781
92894
|
});
|
|
91782
92895
|
}
|
|
91783
92896
|
|
|
91784
92897
|
// src/commands/projects.ts
|
|
91785
92898
|
init_dist5();
|
|
91786
92899
|
await init_src3();
|
|
91787
|
-
import { basename as
|
|
92900
|
+
import { basename as basename9, resolve as resolve18 } from "path";
|
|
91788
92901
|
function registerProjectsCommand(program2, context4) {
|
|
91789
92902
|
const projectsCmd = program2.command("projects").summary("manage the Spur multi-project registry");
|
|
91790
|
-
projectsCmd.command("add").argument("<path>", "Project root directory path").option(
|
|
92903
|
+
projectsCmd.command("add").argument("<path>", "Project root directory path").option(...SHARED_OPTIONS.nameProjectDisplay).option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (pathArg, options) => {
|
|
91791
92904
|
try {
|
|
91792
92905
|
const absolutePath = resolve18(context4.cwd, pathArg);
|
|
91793
92906
|
if (!await context4.fs.exists(absolutePath)) {
|
|
91794
92907
|
throw new Error(`Directory does not exist: ${absolutePath}`);
|
|
91795
92908
|
}
|
|
91796
|
-
const name = options.name ??
|
|
92909
|
+
const name = options.name ?? basename9(absolutePath);
|
|
91797
92910
|
const registry2 = new ProjectRegistry;
|
|
91798
92911
|
const entry = await registry2.upsert({ path: absolutePath, name, port: 0 });
|
|
91799
92912
|
if (options.json) {
|
|
@@ -91811,7 +92924,7 @@ function registerProjectsCommand(program2, context4) {
|
|
|
91811
92924
|
context4.setExitCode(1);
|
|
91812
92925
|
}
|
|
91813
92926
|
});
|
|
91814
|
-
projectsCmd.command("remove").argument("<target>", "Project display name or directory path").option(
|
|
92927
|
+
projectsCmd.command("remove").argument("<target>", "Project display name or directory path").option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (target, options) => {
|
|
91815
92928
|
try {
|
|
91816
92929
|
const registry2 = new ProjectRegistry;
|
|
91817
92930
|
const removed = await registry2.remove(target);
|
|
@@ -91833,7 +92946,7 @@ function registerProjectsCommand(program2, context4) {
|
|
|
91833
92946
|
context4.setExitCode(1);
|
|
91834
92947
|
}
|
|
91835
92948
|
});
|
|
91836
|
-
projectsCmd.command("list").option(
|
|
92949
|
+
projectsCmd.command("list").option(...SHARED_OPTIONS.jsonProjectsArray).action(async (options) => {
|
|
91837
92950
|
try {
|
|
91838
92951
|
const registry2 = new ProjectRegistry;
|
|
91839
92952
|
const rawProjects = await registry2.list();
|
|
@@ -91865,7 +92978,7 @@ function registerProjectsCommand(program2, context4) {
|
|
|
91865
92978
|
context4.setExitCode(1);
|
|
91866
92979
|
}
|
|
91867
92980
|
});
|
|
91868
|
-
projectsCmd.command("start").argument("<target>", "Project display name or path").option(
|
|
92981
|
+
projectsCmd.command("start").argument("<target>", "Project display name or path").option(...SHARED_OPTIONS.portProjects, parseInt).option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (target, options) => {
|
|
91869
92982
|
try {
|
|
91870
92983
|
const registry2 = new ProjectRegistry;
|
|
91871
92984
|
const result = await startRegisteredProject(registry2, target, {
|
|
@@ -91898,7 +93011,7 @@ function registerProjectsCommand(program2, context4) {
|
|
|
91898
93011
|
context4.setExitCode(1);
|
|
91899
93012
|
}
|
|
91900
93013
|
});
|
|
91901
|
-
projectsCmd.command("stop").argument("<target>", "Project display name or path").option(
|
|
93014
|
+
projectsCmd.command("stop").argument("<target>", "Project display name or path").option(...SHARED_OPTIONS.jsonProjectsResponse).action(async (target, options) => {
|
|
91902
93015
|
try {
|
|
91903
93016
|
const registry2 = new ProjectRegistry;
|
|
91904
93017
|
const entry = await registry2.getByName(target) ?? await registry2.getByPath(target);
|
|
@@ -91981,7 +93094,7 @@ function shouldColor(env, stream) {
|
|
|
91981
93094
|
// src/commands/rule.ts
|
|
91982
93095
|
function registerRuleCommand(program2, context4) {
|
|
91983
93096
|
const rule = program2.command("rule").summary("manage constraint rules and presets");
|
|
91984
|
-
rule.command("run").summary("Evaluate constraint rules over the working tree.").option("--preset <name>", "Preset to load (default: recommended-pre-check)", "recommended-pre-check").option(
|
|
93097
|
+
rule.command("run").summary("Evaluate constraint rules over the working tree.").option("--preset <name>", "Preset to load (default: recommended-pre-check)", "recommended-pre-check").option(...SHARED_OPTIONS.fileRuleAdhoc).option("--rule <id>", "Filter run to one rule ID").option("--fail-on <severity>", "Exit 1 threshold: error|warning|info (default: error)", "error").option("--stop-on-first [severity]", "Stop evaluation after first rule with findings at/above severity").option("--fix-mode <mode>", "Fix collection/apply mode: none|suggest|auto (default: none)", "none").option(...SHARED_OPTIONS.dryRunRuleFix).option(...SHARED_OPTIONS.verboseRule).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
91985
93098
|
const service = new RuleService(context4);
|
|
91986
93099
|
const preset = options.preset ?? "recommended-pre-check";
|
|
91987
93100
|
const failOn = parseFailOn(options.failOn ?? "error");
|
|
@@ -92008,7 +93121,7 @@ function registerRuleCommand(program2, context4) {
|
|
|
92008
93121
|
});
|
|
92009
93122
|
context4.setExitCode(result.exitCode);
|
|
92010
93123
|
});
|
|
92011
|
-
rule.command("validate").summary("Validate a rule file or preset without evaluating it.").argument("[file-or-preset]", "File path or preset name to validate").option(
|
|
93124
|
+
rule.command("validate").summary("Validate a rule file or preset without evaluating it.").argument("[file-or-preset]", "File path or preset name to validate").option(...SHARED_OPTIONS.fileRuleAdhocPath).option("--preset <name>", "Preset name").option("--kind <type>", "Source kind: file or preset").option(...SHARED_OPTIONS.noSchema).option(...SHARED_OPTIONS.json).action(async (fileOrPreset, options) => {
|
|
92012
93125
|
const service = new RuleService(context4);
|
|
92013
93126
|
const source = resolveSource({ file: options.file, preset: options.preset }, fileOrPreset ? [fileOrPreset] : []);
|
|
92014
93127
|
if (options.kind && (options.kind === "file" || options.kind === "preset")) {
|
|
@@ -92019,13 +93132,13 @@ function registerRuleCommand(program2, context4) {
|
|
|
92019
93132
|
const result = await service.validate({ source, json: json3, validateSchema });
|
|
92020
93133
|
context4.setExitCode(result.exitCode);
|
|
92021
93134
|
});
|
|
92022
|
-
rule.command("list").summary("List discovered rule files, or list resolved rules for a preset.").option("--preset <name>", "Preset to list rules for").option(
|
|
93135
|
+
rule.command("list").summary("List discovered rule files, or list resolved rules for a preset.").option("--preset <name>", "Preset to list rules for").option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
92023
93136
|
const service = new RuleService(context4);
|
|
92024
93137
|
const preset = options.preset;
|
|
92025
93138
|
const result = await service.list(preset);
|
|
92026
93139
|
context4.output.write(options.json ? JSON.stringify(result, null, 2) : preset === undefined ? formatRuleFileList(result) : formatPresetRuleList(result));
|
|
92027
93140
|
});
|
|
92028
|
-
rule.command("trace").summary("Show persisted rule run history.").argument("[run-id]", "Run ID for per-run detail").option("--preset <name>", "Filter by preset name").option(
|
|
93141
|
+
rule.command("trace").summary("Show persisted rule run history.").argument("[run-id]", "Run ID for per-run detail").option("--preset <name>", "Filter by preset name").option(...SHARED_OPTIONS.statusDoneFailed).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option(...SHARED_OPTIONS.json).action(async (runId, options) => {
|
|
92029
93142
|
const last = parseInt(options.last, 10);
|
|
92030
93143
|
if (!Number.isInteger(last) || last < 1) {
|
|
92031
93144
|
context4.output.error("--last must be a positive integer");
|
|
@@ -92210,7 +93323,7 @@ function formatTraceDetail(detail) {
|
|
|
92210
93323
|
|
|
92211
93324
|
// src/commands/serve.ts
|
|
92212
93325
|
init_src();
|
|
92213
|
-
import { join as
|
|
93326
|
+
import { join as join31 } from "path";
|
|
92214
93327
|
|
|
92215
93328
|
// ../server/src/index.ts
|
|
92216
93329
|
init_src();
|
|
@@ -92220,11 +93333,11 @@ init_src();
|
|
|
92220
93333
|
init_loader();
|
|
92221
93334
|
init_src2();
|
|
92222
93335
|
await init_src3();
|
|
92223
|
-
import { basename as
|
|
93336
|
+
import { basename as basename11, dirname as dirname20, isAbsolute as isAbsolute6, join as join30 } from "path";
|
|
92224
93337
|
init_dist5();
|
|
92225
93338
|
|
|
92226
93339
|
// ../server/src/bootstrap.ts
|
|
92227
|
-
import { join as
|
|
93340
|
+
import { join as join28 } from "path";
|
|
92228
93341
|
|
|
92229
93342
|
// ../../node_modules/.bun/radash@12.1.1/node_modules/radash/dist/esm/async.mjs
|
|
92230
93343
|
var guard = (func, shouldGuard) => {
|
|
@@ -98393,7 +99506,7 @@ var featureModule = {
|
|
|
98393
99506
|
|
|
98394
99507
|
// ../server/src/modules/health/index.ts
|
|
98395
99508
|
await init_src3();
|
|
98396
|
-
import { basename as
|
|
99509
|
+
import { basename as basename10 } from "path";
|
|
98397
99510
|
var startedAt = Date.now();
|
|
98398
99511
|
var healthModule = {
|
|
98399
99512
|
name: "health",
|
|
@@ -98419,7 +99532,7 @@ var healthModule = {
|
|
|
98419
99532
|
return c3.json({ status: "error", db: "unreachable" }, 503);
|
|
98420
99533
|
});
|
|
98421
99534
|
app.get("/api/project", (c3) => {
|
|
98422
|
-
return c3.json({ name: ctx ?
|
|
99535
|
+
return c3.json({ name: ctx ? basename10(ctx.cwd) : null });
|
|
98423
99536
|
});
|
|
98424
99537
|
app.get("/api/projects", async (c3) => {
|
|
98425
99538
|
if (!ctx) {
|
|
@@ -101159,7 +102272,7 @@ function createApp(appRt, opts) {
|
|
|
101159
102272
|
app.use("*", async (c3, next) => {
|
|
101160
102273
|
const pathname = c3.req.path === "/" ? "/index.html" : c3.req.path;
|
|
101161
102274
|
try {
|
|
101162
|
-
const file2 = Bun.file(
|
|
102275
|
+
const file2 = Bun.file(join28(webDistPath, pathname));
|
|
101163
102276
|
if (await file2.exists()) {
|
|
101164
102277
|
const headers = new Headers({ "content-type": file2.type });
|
|
101165
102278
|
return new Response(file2.stream(), { headers });
|
|
@@ -101172,7 +102285,7 @@ function createApp(appRt, opts) {
|
|
|
101172
102285
|
return c3.json({ error: "Not Found" }, 404);
|
|
101173
102286
|
}
|
|
101174
102287
|
try {
|
|
101175
|
-
const indexFile = Bun.file(
|
|
102288
|
+
const indexFile = Bun.file(join28(webDistPath, "index.html"));
|
|
101176
102289
|
if (await indexFile.exists()) {
|
|
101177
102290
|
return new Response(indexFile.stream(), {
|
|
101178
102291
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
@@ -101193,7 +102306,7 @@ init_src();
|
|
|
101193
102306
|
init_src2();
|
|
101194
102307
|
init_dist5();
|
|
101195
102308
|
await init_src3();
|
|
101196
|
-
import { dirname as dirname19, join as
|
|
102309
|
+
import { dirname as dirname19, join as join29 } from "path";
|
|
101197
102310
|
var NOOP_OUTPUT = { write: (_s) => {}, error: (_s) => {} };
|
|
101198
102311
|
|
|
101199
102312
|
class LazyPlanningEventEmitter {
|
|
@@ -101228,7 +102341,7 @@ class NotConfiguredError extends Error {
|
|
|
101228
102341
|
function createServerContext(appRt, options) {
|
|
101229
102342
|
const cwd = options.cwd;
|
|
101230
102343
|
const fs3 = options.fs;
|
|
101231
|
-
const dbUrl = options.dbUrl ??
|
|
102344
|
+
const dbUrl = options.dbUrl ?? join29(cwd, DEFAULT_DATABASE_URL);
|
|
101232
102345
|
const eventsBus = options.eventsBus ?? appRt.events;
|
|
101233
102346
|
const jobQueueEnabled = options.jobQueueEnabled ?? false;
|
|
101234
102347
|
const eventProjectContext = systemEventProjectContext(cwd);
|
|
@@ -101479,7 +102592,7 @@ async function loadServerSectionMatrix() {
|
|
|
101479
102592
|
}
|
|
101480
102593
|
const root = bundledConfigRoot();
|
|
101481
102594
|
if (root !== null) {
|
|
101482
|
-
const matrixPath =
|
|
102595
|
+
const matrixPath = join30(root, "tasks", "section-matrix.yaml");
|
|
101483
102596
|
if (await nodeFs.exists(matrixPath)) {
|
|
101484
102597
|
return await loadStructuredSpurConfig(matrixPath, {
|
|
101485
102598
|
validateJsonSchema: false
|
|
@@ -101488,7 +102601,7 @@ async function loadServerSectionMatrix() {
|
|
|
101488
102601
|
}
|
|
101489
102602
|
throw new Error(`no canonical section-matrix found for task creation (F92 R1); tried:
|
|
101490
102603
|
` + ` - ${localPath}
|
|
101491
|
-
` + (root !== null ? ` - ${
|
|
102604
|
+
` + (root !== null ? ` - ${join30(root, "tasks", "section-matrix.yaml")}
|
|
101492
102605
|
` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
|
|
101493
102606
|
}
|
|
101494
102607
|
var SYSTEM_EVENTS_PRUNE_JOB = "system-events-prune";
|
|
@@ -101623,15 +102736,15 @@ async function handleFeatureActionJob(ctx, env, payload) {
|
|
|
101623
102736
|
await runFeatureActionJob(ctx, env, payload);
|
|
101624
102737
|
}
|
|
101625
102738
|
async function resolveWebDistPath(configuredPath) {
|
|
101626
|
-
const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute6(configuredPath) ? configuredPath :
|
|
101627
|
-
|
|
101628
|
-
|
|
101629
|
-
|
|
101630
|
-
|
|
101631
|
-
|
|
102739
|
+
const candidates = configuredPath && configuredPath.trim() !== "" ? [isAbsolute6(configuredPath) ? configuredPath : join30(process.cwd(), configuredPath)] : [
|
|
102740
|
+
join30(process.cwd(), "dist/web"),
|
|
102741
|
+
join30(import.meta.dir, "web"),
|
|
102742
|
+
join30(dirname20(process.execPath), "web"),
|
|
102743
|
+
join30(dirname20(process.execPath), "../web"),
|
|
102744
|
+
join30(import.meta.dir, "../../../dist/web")
|
|
101632
102745
|
];
|
|
101633
102746
|
for (const candidate of candidates) {
|
|
101634
|
-
if (await Bun.file(
|
|
102747
|
+
if (await Bun.file(join30(candidate, "index.html")).exists()) {
|
|
101635
102748
|
return candidate;
|
|
101636
102749
|
}
|
|
101637
102750
|
}
|
|
@@ -101736,7 +102849,7 @@ async function startServer(options, deps = defaultDeps) {
|
|
|
101736
102849
|
});
|
|
101737
102850
|
const projectRegistry = new ProjectRegistry;
|
|
101738
102851
|
const projectCwd = process.cwd();
|
|
101739
|
-
const projectName =
|
|
102852
|
+
const projectName = basename11(projectCwd);
|
|
101740
102853
|
try {
|
|
101741
102854
|
await projectRegistry.upsert({ path: projectCwd, name: projectName, port: server.port });
|
|
101742
102855
|
} catch (err) {
|
|
@@ -101798,18 +102911,18 @@ if (false) {}
|
|
|
101798
102911
|
|
|
101799
102912
|
// src/commands/serve.ts
|
|
101800
102913
|
function resolveServeDbUrl(cwd, env, configuredUrl) {
|
|
101801
|
-
return env.DATABASE_URL === undefined ?
|
|
102914
|
+
return env.DATABASE_URL === undefined ? join31(cwd, DEFAULT_DATABASE_URL) : configuredUrl;
|
|
101802
102915
|
}
|
|
101803
|
-
function registerServeCommand(program2, context4) {
|
|
101804
|
-
program2.command("serve").summary("start the Spur web server (local fallback)").option(
|
|
102916
|
+
function registerServeCommand(program2, context4, options = {}) {
|
|
102917
|
+
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).action(async (options2) => {
|
|
101805
102918
|
try {
|
|
101806
102919
|
const env = process.env;
|
|
101807
102920
|
const config4 = buildConfigFromEnv(env);
|
|
101808
|
-
const port =
|
|
101809
|
-
const host =
|
|
101810
|
-
const cwd =
|
|
102921
|
+
const port = options2.port ?? config4.server.port;
|
|
102922
|
+
const host = options2.host ?? config4.server.host;
|
|
102923
|
+
const cwd = options2.cwd ?? context4.cwd;
|
|
101811
102924
|
const dbUrl = resolveServeDbUrl(cwd, env, config4.database.url);
|
|
101812
|
-
if (
|
|
102925
|
+
if (options2.json) {
|
|
101813
102926
|
context4.output.write(toJson2({
|
|
101814
102927
|
port,
|
|
101815
102928
|
url: `http://${host}:${port}`,
|
|
@@ -101823,7 +102936,7 @@ function registerServeCommand(program2, context4) {
|
|
|
101823
102936
|
port,
|
|
101824
102937
|
host,
|
|
101825
102938
|
dbUrl,
|
|
101826
|
-
openBrowser:
|
|
102939
|
+
openBrowser: options2.open ?? true,
|
|
101827
102940
|
webDistPath: config4.server.webDistPath
|
|
101828
102941
|
});
|
|
101829
102942
|
} catch (err) {
|
|
@@ -101837,7 +102950,7 @@ function registerServeCommand(program2, context4) {
|
|
|
101837
102950
|
}
|
|
101838
102951
|
|
|
101839
102952
|
// src/commands/status.ts
|
|
101840
|
-
import { join as
|
|
102953
|
+
import { join as join32 } from "path";
|
|
101841
102954
|
|
|
101842
102955
|
// src/errors.ts
|
|
101843
102956
|
class CommandError extends Error {
|
|
@@ -101895,10 +103008,10 @@ async function gitContext(cwd) {
|
|
|
101895
103008
|
}
|
|
101896
103009
|
|
|
101897
103010
|
// src/commands/status.ts
|
|
101898
|
-
function registerStatusCommand(program2, context4) {
|
|
101899
|
-
program2.command("status").summary("show project, Git, and optional path status").option(
|
|
103011
|
+
function registerStatusCommand(program2, context4, options = {}) {
|
|
103012
|
+
program2.command("status", { hidden: options.hidden === true }).summary("show project, Git, and optional path status").option(...SHARED_OPTIONS.json).argument("[path]", "Optional file/dir path to check").action(async (path9, options2) => {
|
|
101900
103013
|
try {
|
|
101901
|
-
const code = await runStatusCore(path9,
|
|
103014
|
+
const code = await runStatusCore(path9, options2, context4);
|
|
101902
103015
|
context4.setExitCode(code);
|
|
101903
103016
|
} catch (err) {
|
|
101904
103017
|
context4.output.error(err instanceof Error ? err.message : String(err));
|
|
@@ -101907,9 +103020,9 @@ function registerStatusCommand(program2, context4) {
|
|
|
101907
103020
|
});
|
|
101908
103021
|
}
|
|
101909
103022
|
async function runStatusCore(path9, options, context4) {
|
|
101910
|
-
const [packageJsonExists, spurConfigExists,
|
|
101911
|
-
context4.fs.exists(
|
|
101912
|
-
context4.fs.exists(
|
|
103023
|
+
const [packageJsonExists, spurConfigExists, git3, agentSpecs] = await Promise.all([
|
|
103024
|
+
context4.fs.exists(join32(context4.cwd, "package.json")),
|
|
103025
|
+
context4.fs.exists(join32(context4.cwd, ".spur", "config.yaml")),
|
|
101913
103026
|
gitContext(context4.cwd),
|
|
101914
103027
|
listAgentSpecIds(context4)
|
|
101915
103028
|
]);
|
|
@@ -101918,7 +103031,7 @@ async function runStatusCore(path9, options, context4) {
|
|
|
101918
103031
|
ok: spurConfigExists,
|
|
101919
103032
|
packageJson: packageJsonExists,
|
|
101920
103033
|
spurConfig: spurConfigExists,
|
|
101921
|
-
git:
|
|
103034
|
+
git: git3,
|
|
101922
103035
|
agentSpecs,
|
|
101923
103036
|
...target === undefined ? {} : { target }
|
|
101924
103037
|
};
|
|
@@ -101929,7 +103042,7 @@ async function runStatusCore(path9, options, context4) {
|
|
|
101929
103042
|
`Project: ${spurConfigExists ? "ok" : "missing .spur/config.yaml"}`,
|
|
101930
103043
|
`Package: ${packageJsonExists ? "ok" : "none"}`,
|
|
101931
103044
|
`Agents: ${agentSpecs.length === 0 ? "none" : agentSpecs.join(", ")}`,
|
|
101932
|
-
`Git: ${
|
|
103045
|
+
`Git: ${git3.root === null ? "none" : `${git3.branch ?? "detached"}${git3.dirty ? " dirty" : " clean"}`}`,
|
|
101933
103046
|
...target === undefined ? [] : [`Path: ${target.path} ${target.size} bytes`]
|
|
101934
103047
|
].join(`
|
|
101935
103048
|
`));
|
|
@@ -101937,14 +103050,14 @@ async function runStatusCore(path9, options, context4) {
|
|
|
101937
103050
|
return status.ok ? 0 : 1;
|
|
101938
103051
|
}
|
|
101939
103052
|
async function listAgentSpecIds(context4) {
|
|
101940
|
-
const dir =
|
|
103053
|
+
const dir = join32(context4.cwd, ".spur", "agents");
|
|
101941
103054
|
if (!await context4.fs.exists(dir))
|
|
101942
103055
|
return [];
|
|
101943
103056
|
const entries = await context4.fs.readDir(dir);
|
|
101944
103057
|
return entries.filter((entry) => entry.endsWith(".yaml") || entry.endsWith(".yml")).map((entry) => entry.replace(/\.ya?ml$/, "")).sort();
|
|
101945
103058
|
}
|
|
101946
103059
|
async function readTargetStatus(context4, targetPath) {
|
|
101947
|
-
const resolved =
|
|
103060
|
+
const resolved = join32(context4.cwd, targetPath);
|
|
101948
103061
|
const stat = await context4.fs.stat(resolved);
|
|
101949
103062
|
if (stat === null)
|
|
101950
103063
|
throw new CommandError(`status failed: path does not exist at ${resolved}`);
|
|
@@ -101956,8 +103069,8 @@ init_loader();
|
|
|
101956
103069
|
init_src2();
|
|
101957
103070
|
init_dist5();
|
|
101958
103071
|
await init_src3();
|
|
101959
|
-
import { existsSync as
|
|
101960
|
-
import { dirname as dirname21, join as
|
|
103072
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
103073
|
+
import { dirname as dirname21, join as join33 } from "path";
|
|
101961
103074
|
// schemas/section-matrix.schema.json
|
|
101962
103075
|
var section_matrix_schema_default = {
|
|
101963
103076
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -102861,7 +103974,7 @@ function renderMigrationReport(report, dryRun, corpusDir) {
|
|
|
102861
103974
|
}
|
|
102862
103975
|
function registerTaskCommand(program2, context4) {
|
|
102863
103976
|
const task = program2.command("task").summary("manage tasks");
|
|
102864
|
-
task.command("create").summary("Create a new task with race-safe WBS allocation.").argument("<title>", "Task title").option(
|
|
103977
|
+
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).action(async (title2, options) => {
|
|
102865
103978
|
if (options.template !== undefined && !TASK_VARIANTS.includes(options.template)) {
|
|
102866
103979
|
context4.output.error(`Unknown template variant "${options.template}". Valid: ${TASK_VARIANTS.join(", ")}`);
|
|
102867
103980
|
context4.setExitCode(2);
|
|
@@ -102925,7 +104038,7 @@ function registerTaskCommand(program2, context4) {
|
|
|
102925
104038
|
}
|
|
102926
104039
|
}
|
|
102927
104040
|
});
|
|
102928
|
-
task.command("show").alias("get").summary("Show a task by WBS.").argument("<wbs>", "Task WBS number").option(
|
|
104041
|
+
task.command("show").alias("get").summary("Show a task by WBS.").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
102929
104042
|
const svc = await makeService2(context4, options.folder);
|
|
102930
104043
|
try {
|
|
102931
104044
|
const result = await svc.show(wbs);
|
|
@@ -102953,7 +104066,7 @@ ${result.content}`);
|
|
|
102953
104066
|
"See the gate checklist (spur-dev/references/gate-checklists.md).",
|
|
102954
104067
|
"Valid section names (no failed write): `spur task sections <wbs> list`."
|
|
102955
104068
|
].join(`
|
|
102956
|
-
`)).option(
|
|
104069
|
+
`)).option(...SHARED_OPTIONS.section).option(...SHARED_OPTIONS.fromFile).option(...SHARED_OPTIONS.featureFrontmatter).option(...SHARED_OPTIONS.prioritySet).option("--ac-numbering <mode>", "Set the ac_numbering frontmatter field (task-local) \u2014 opts the task into the Requirements\u2194AC coverage check").option("--ac-altitude <mode>", "Set the ac_altitude frontmatter field. Valid: `graduating` (default; feature-AC subset rule enforced) or `task-local` (skip the DD-09 subset rule \u2014 task scenarios are intentionally not feature ship criteria). Mirrors the L1 schema enum (packages/domain/src/planning/schema.ts:304).").option("--no-lifecycle", "Suppress lifecycle workflow run creation (use during pipeline runs to avoid orphaned lifecycle runs)").option("--force-done", "Allow transitioning to `done` even when the verify verdict is not PASS; records an override (task 0292). Waives the verdict only \u2014 the FSM path still applies, so from an earlier status walk the hops first: `todo` \u2192 `wip` \u2192 `testing` \u2192 `done` (each hop runs the structural `spur task check`)").option("--reason <text>", "Rationale for a forced-done override (paired with --force-done; persisted as done_reason)").option("--verdict-dir <path>", "Directory holding <wbs>-verdict.json artifacts (default: .spur/run)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, status, options) => {
|
|
102957
104070
|
const svc = await makeService2(context4, options.folder, options.lifecycle === false);
|
|
102958
104071
|
try {
|
|
102959
104072
|
if (options.section !== undefined) {
|
|
@@ -103001,7 +104114,7 @@ ${result.content}`);
|
|
|
103001
104114
|
let forcedDoneVerdict;
|
|
103002
104115
|
if (status === "done") {
|
|
103003
104116
|
const current = await svc.show(wbs);
|
|
103004
|
-
const verdictDir = options.verdictDir ??
|
|
104117
|
+
const verdictDir = options.verdictDir ?? join33(context4.cwd, ".spur", "run");
|
|
103005
104118
|
const loaded = await readVerdictArtifact(context4.fs, verdictDir, wbs);
|
|
103006
104119
|
const guardOutcome = evaluateDoneTransition({
|
|
103007
104120
|
wbs,
|
|
@@ -103083,7 +104196,7 @@ ${result.content}`);
|
|
|
103083
104196
|
|
|
103084
104197
|
` + `Validation: WBS format, existence, self-edge, duplicates, and cycle detection
|
|
103085
104198
|
` + `all run BEFORE any write (atomic \u2014 R2). Exit codes: 0 success, 1 generic error,
|
|
103086
|
-
` + "2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: set | add | remove | clear").argument("[values...]", "WBS values (required for set/add/remove; forbidden for clear)").option(
|
|
104199
|
+
` + "2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: set | add | remove | clear").argument("[values...]", "WBS values (required for set/add/remove; forbidden for clear)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, op, values, options) => {
|
|
103087
104200
|
const allowedOps = ["set", "add", "remove", "clear"];
|
|
103088
104201
|
if (!allowedOps.includes(op)) {
|
|
103089
104202
|
context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
|
|
@@ -103123,7 +104236,7 @@ ${result.content}`);
|
|
|
103123
104236
|
` + `(${UNIVERSAL_SECTIONS.join(", ")}) are always allowed. All writes go through the
|
|
103124
104237
|
` + `existing planning-write-service.updateSection pipeline \u2014 phantom-section guards,
|
|
103125
104238
|
` + `atomic writes, history, and timestamps are inherited. Exit codes: 0 success,
|
|
103126
|
-
` + "1 generic error, 2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: init | add | list").argument("[name]", "Canonical section name (required for add; forbidden for init/list)").option(
|
|
104239
|
+
` + "1 generic error, 2 usage error, 3 validation error.").argument("<wbs>", "Task WBS number to mutate").argument("<op>", "Operation: init | add | list").argument("[name]", "Canonical section name (required for add; forbidden for init/list)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, op, name, options) => {
|
|
103127
104240
|
const allowedOps = ["init", "add", "list"];
|
|
103128
104241
|
if (!allowedOps.includes(op)) {
|
|
103129
104242
|
context4.output.error(`Unknown op "${op}". Allowed: ${allowedOps.join(", ")}.`);
|
|
@@ -103170,7 +104283,7 @@ ${result.content}`);
|
|
|
103170
104283
|
}
|
|
103171
104284
|
}
|
|
103172
104285
|
});
|
|
103173
|
-
task.command("list").summary("List tasks with optional filtering.").option(
|
|
104286
|
+
task.command("list").summary("List tasks with optional filtering.").option(...SHARED_OPTIONS.statusFilter).option("--phase <p>", "Filter by phase (legacy alias for --status)").option("--parent <wbs>", "Filter by parent WBS").option(...SHARED_OPTIONS.featureFilterEdge).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
103174
104287
|
const svc = await makeService2(context4, options.folder);
|
|
103175
104288
|
try {
|
|
103176
104289
|
const tasks = await svc.list({
|
|
@@ -103198,7 +104311,7 @@ ${result.content}`);
|
|
|
103198
104311
|
context4.setExitCode(1);
|
|
103199
104312
|
}
|
|
103200
104313
|
});
|
|
103201
|
-
task.command("refresh").summary("Re-scan the task corpus and report counts (kanban.md retired \u2014 A17 cutover).").option(
|
|
104314
|
+
task.command("refresh").summary("Re-scan the task corpus and report counts (kanban.md retired \u2014 A17 cutover).").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
103202
104315
|
const svc = await makeService2(context4, options.folder);
|
|
103203
104316
|
try {
|
|
103204
104317
|
const result = await svc.refresh();
|
|
@@ -103212,7 +104325,7 @@ ${result.content}`);
|
|
|
103212
104325
|
context4.setExitCode(1);
|
|
103213
104326
|
}
|
|
103214
104327
|
});
|
|
103215
|
-
task.command("migrate").summary("Run the one-time A17 task corpus normalization pass.").option(
|
|
104328
|
+
task.command("migrate").summary("Run the one-time A17 task corpus normalization pass.").option(...SHARED_OPTIONS.dryRunTaskReport).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
103216
104329
|
try {
|
|
103217
104330
|
const foldersConfig = (await resolvePlanningFolders(context4.fs)).foldersConfig;
|
|
103218
104331
|
const activeFolder = context4.fs.resolve(foldersConfig.active_folder);
|
|
@@ -103230,7 +104343,7 @@ ${result.content}`);
|
|
|
103230
104343
|
context4.setExitCode(1);
|
|
103231
104344
|
}
|
|
103232
104345
|
});
|
|
103233
|
-
task.command("migrate-anchors").summary("Qualify in-repo evidence anchors to repo-relative paths (0583 R1\u2013R3).").option(
|
|
104346
|
+
task.command("migrate-anchors").summary("Qualify in-repo evidence anchors to repo-relative paths (0583 R1\u2013R3).").option(...SHARED_OPTIONS.dryRunTaskReport).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
103234
104347
|
try {
|
|
103235
104348
|
const dryRun = options.dryRun === true;
|
|
103236
104349
|
const report = await anchorQualify(context4.fs, {
|
|
@@ -103275,7 +104388,7 @@ ${result.content}`);
|
|
|
103275
104388
|
context4.setExitCode(1);
|
|
103276
104389
|
}
|
|
103277
104390
|
});
|
|
103278
|
-
task.command("refresh-roster").summary("Regenerate a parent task's sub-task roster block in its ## Plan (0121 roll-up gate's generator).").argument("<wbs>", "Parent task WBS number").option(
|
|
104391
|
+
task.command("refresh-roster").summary("Regenerate a parent task's sub-task roster block in its ## Plan (0121 roll-up gate's generator).").argument("<wbs>", "Parent task WBS number").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
103279
104392
|
const svc = await makeService2(context4, options.folder);
|
|
103280
104393
|
try {
|
|
103281
104394
|
const result = await svc.refreshRoster(wbs);
|
|
@@ -103291,7 +104404,7 @@ ${result.content}`);
|
|
|
103291
104404
|
context4.setExitCode(1);
|
|
103292
104405
|
}
|
|
103293
104406
|
});
|
|
103294
|
-
task.command("batch-create").summary("Create many tasks from a validated JSON file \u2014 all-or-nothing (LLM\u2192CLI gate).").requiredOption(
|
|
104407
|
+
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).action(async (options) => {
|
|
103295
104408
|
const svc = await makeService2(context4, options.folder);
|
|
103296
104409
|
try {
|
|
103297
104410
|
const { children, parentsWired } = await svc.batchCreate(options.file);
|
|
@@ -103335,7 +104448,7 @@ ${result.content}`);
|
|
|
103335
104448
|
}
|
|
103336
104449
|
}
|
|
103337
104450
|
});
|
|
103338
|
-
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(
|
|
104451
|
+
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).action(async (wbs, options) => {
|
|
103339
104452
|
const svc = await makeService2(context4, options.folder);
|
|
103340
104453
|
try {
|
|
103341
104454
|
const result = await svc.record(wbs, {
|
|
@@ -103362,7 +104475,7 @@ ${result.content}`);
|
|
|
103362
104475
|
context4.setExitCode(1);
|
|
103363
104476
|
}
|
|
103364
104477
|
});
|
|
103365
|
-
task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option(
|
|
104478
|
+
task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
103366
104479
|
const { deriveVerdict: deriveVerdict2, verdictRowsMatchScenarios: verdictRowsMatchScenarios2 } = await init_src3().then(() => exports_src2);
|
|
103367
104480
|
const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
|
|
103368
104481
|
let answerText;
|
|
@@ -103404,7 +104517,7 @@ ${result.content}`);
|
|
|
103404
104517
|
context4.setExitCode(1);
|
|
103405
104518
|
}
|
|
103406
104519
|
});
|
|
103407
|
-
task.command("verifyall-aggregate").summary("Aggregate per-task verify outcomes into a deterministic batch verdict (verifyall).").description("Reads a JSON array of per-task outcomes ({wbs,outcome}) and emits the batch " + "verdict with NOT-STARTED tasks excluded from the rollup. Replaces agent-discretion " + "rollup prose (dev-operations.md \xA73a) with deterministic code (task 0341).").option(
|
|
104520
|
+
task.command("verifyall-aggregate").summary("Aggregate per-task verify outcomes into a deterministic batch verdict (verifyall).").description("Reads a JSON array of per-task outcomes ({wbs,outcome}) and emits the batch " + "verdict with NOT-STARTED tasks excluded from the rollup. Replaces agent-discretion " + "rollup prose (dev-operations.md \xA73a) with deterministic code (task 0341).").option(...SHARED_OPTIONS.fromFileOutcomeRows).option(...SHARED_OPTIONS.json).action(async (options) => {
|
|
103408
104521
|
const inputPath = options.fromFile ?? ".spur/run/verifyall-batch-input.json";
|
|
103409
104522
|
let raw2;
|
|
103410
104523
|
try {
|
|
@@ -103451,7 +104564,7 @@ ${result.content}`);
|
|
|
103451
104564
|
context4.setExitCode(1);
|
|
103452
104565
|
}
|
|
103453
104566
|
});
|
|
103454
|
-
task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option(
|
|
104567
|
+
task.command("check").summary("Validate a task file through the four-layer check (design \xA73).").argument("[wbs]", "Task WBS number (validates all tasks in the folder when omitted)").option(...SHARED_OPTIONS.strictTaskAll).option("--strict-core", "Compatibility alias (F92 R2): historically the done-gate label; kept so installed plugins/workflows that call it keep working. No longer meaningful on its own \u2014 target-state selection (`--as`) supplies the real done semantics.").option(...SHARED_OPTIONS.asTaskF92).option("--corpus", "Sweep every task and feature against config/corpus-baseline.json").option("--since <ref>", "Scope the corpus fog check to changes since a git ref (requires --corpus)").option("--fix", "repair structural findings in place (heading presence/level/order, R-item checkboxes)").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
103455
104568
|
const json3 = options.json === true;
|
|
103456
104569
|
const strict = options.strict === true;
|
|
103457
104570
|
const asStatus = options.as === undefined ? undefined : canonicalStatusOrRaw(options.as);
|
|
@@ -103465,6 +104578,11 @@ ${result.content}`);
|
|
|
103465
104578
|
context4.setExitCode(2);
|
|
103466
104579
|
return;
|
|
103467
104580
|
}
|
|
104581
|
+
if (options.fix === true && options.corpus === true) {
|
|
104582
|
+
context4.output.error("--fix repairs files in place and cannot be combined with --corpus");
|
|
104583
|
+
context4.setExitCode(2);
|
|
104584
|
+
return;
|
|
104585
|
+
}
|
|
103468
104586
|
try {
|
|
103469
104587
|
if (options.corpus === true) {
|
|
103470
104588
|
if (wbs !== undefined) {
|
|
@@ -103521,6 +104639,11 @@ ${result.content}`);
|
|
|
103521
104639
|
const printResult = (result) => {
|
|
103522
104640
|
if (json3)
|
|
103523
104641
|
return;
|
|
104642
|
+
if (result.repairs !== undefined && result.repairs.length > 0) {
|
|
104643
|
+
for (const r2 of result.repairs) {
|
|
104644
|
+
context4.output.write(` [FIX] ${r2.kind} ${r2.section}: ${r2.detail}`);
|
|
104645
|
+
}
|
|
104646
|
+
}
|
|
103524
104647
|
context4.output.write(`
|
|
103525
104648
|
${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
103526
104649
|
for (const f of result.findings) {
|
|
@@ -103543,7 +104666,8 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
103543
104666
|
strict,
|
|
103544
104667
|
asStatus,
|
|
103545
104668
|
severityOverrides: planningFolders.severityOverrides,
|
|
103546
|
-
accepted
|
|
104669
|
+
accepted,
|
|
104670
|
+
fix: options.fix === true
|
|
103547
104671
|
});
|
|
103548
104672
|
results.push(result);
|
|
103549
104673
|
printResult(result);
|
|
@@ -103562,7 +104686,8 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
103562
104686
|
strict,
|
|
103563
104687
|
asStatus,
|
|
103564
104688
|
severityOverrides: planningFolders.severityOverrides,
|
|
103565
|
-
accepted
|
|
104689
|
+
accepted,
|
|
104690
|
+
fix: options.fix === true
|
|
103566
104691
|
});
|
|
103567
104692
|
results.push(result);
|
|
103568
104693
|
printResult(result);
|
|
@@ -103596,7 +104721,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
103596
104721
|
context4.setExitCode(1);
|
|
103597
104722
|
}
|
|
103598
104723
|
});
|
|
103599
|
-
task.command("resolve").summary("Resolve a file path to its owning task WBS.").argument("<file-path>", "File path to resolve").option(
|
|
104724
|
+
task.command("resolve").summary("Resolve a file path to its owning task WBS.").argument("<file-path>", "File path to resolve").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.strictTaskPath).option(...SHARED_OPTIONS.json).action(async (filePath, options) => {
|
|
103600
104725
|
const svc = await makeService2(context4, options.folder);
|
|
103601
104726
|
try {
|
|
103602
104727
|
const result = await svc.resolve(filePath, { strict: options.strict === true });
|
|
@@ -103615,7 +104740,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
103615
104740
|
context4.setExitCode(1);
|
|
103616
104741
|
}
|
|
103617
104742
|
});
|
|
103618
|
-
task.command("path").summary("Resolve a WBS to its absolute task file path.").argument("<wbs>", "Task WBS number").option(
|
|
104743
|
+
task.command("path").summary("Resolve a WBS to its absolute task file path.").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
103619
104744
|
const svc = await makeService2(context4, options.folder);
|
|
103620
104745
|
try {
|
|
103621
104746
|
const filePath = await svc.getFilePath(wbs);
|
|
@@ -103634,7 +104759,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
103634
104759
|
context4.setExitCode(1);
|
|
103635
104760
|
}
|
|
103636
104761
|
});
|
|
103637
|
-
task.command("run-link").summary("Record a pipeline provenance link for a task (used by --next auto chains to satisfy testing\u2192done guard).").argument("<wbs>", "Task WBS number").option(
|
|
104762
|
+
task.command("run-link").summary("Record a pipeline provenance link for a task (used by --next auto chains to satisfy testing\u2192done guard).").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.sourceLink, "chain").option(...SHARED_OPTIONS.runIdTask).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
103638
104763
|
try {
|
|
103639
104764
|
const db2 = await context4.getDb();
|
|
103640
104765
|
const ensured = await ensurePipelineRunLink(db2, wbs, {
|
|
@@ -103659,7 +104784,7 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
|
|
|
103659
104784
|
context4.setExitCode(1);
|
|
103660
104785
|
}
|
|
103661
104786
|
});
|
|
103662
|
-
task.command("scaffold-tests").summary("Generate BDD test stubs from task Acceptance Criteria.").argument("<wbs>", "Task WBS number").option(
|
|
104787
|
+
task.command("scaffold-tests").summary("Generate BDD test stubs from task Acceptance Criteria.").argument("<wbs>", "Task WBS number").option(...SHARED_OPTIONS.fileTaskTest).option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).action(async (wbs, options) => {
|
|
103663
104788
|
const { TaskScaffoldService: TaskScaffoldService2, resolvePlanningFolders: resolvePlanningFolders2 } = await init_src3().then(() => exports_src2);
|
|
103664
104789
|
const foldersConfig = (await resolvePlanningFolders2(context4.fs)).foldersConfig;
|
|
103665
104790
|
const tasksDir = options.folder ?? context4.fs.resolve(foldersConfig.active_folder);
|
|
@@ -103711,15 +104836,15 @@ function loadTemplateBodies(projectRoot, variant) {
|
|
|
103711
104836
|
if (cached2 !== undefined)
|
|
103712
104837
|
return cached2;
|
|
103713
104838
|
let bodies = {};
|
|
103714
|
-
const localPath =
|
|
103715
|
-
if (
|
|
103716
|
-
bodies = extractTemplateBodies(
|
|
104839
|
+
const localPath = join33(projectRoot, ".spur", "tasks", "templates", `${variant}.md`);
|
|
104840
|
+
if (existsSync13(localPath)) {
|
|
104841
|
+
bodies = extractTemplateBodies(readFileSync12(localPath, "utf8"));
|
|
103717
104842
|
} else {
|
|
103718
104843
|
const root = bundledConfigRoot();
|
|
103719
104844
|
if (root !== null) {
|
|
103720
|
-
const templatePath =
|
|
103721
|
-
if (
|
|
103722
|
-
bodies = extractTemplateBodies(
|
|
104845
|
+
const templatePath = join33(root, "templates", "task", `${variant}.md`);
|
|
104846
|
+
if (existsSync13(templatePath)) {
|
|
104847
|
+
bodies = extractTemplateBodies(readFileSync12(templatePath, "utf8"));
|
|
103723
104848
|
}
|
|
103724
104849
|
}
|
|
103725
104850
|
}
|
|
@@ -103777,7 +104902,7 @@ async function loadSectionMatrixUncached(projectRoot) {
|
|
|
103777
104902
|
}
|
|
103778
104903
|
const root = bundledConfigRoot();
|
|
103779
104904
|
if (root !== null) {
|
|
103780
|
-
const matrixPath =
|
|
104905
|
+
const matrixPath = join33(root, "tasks", "section-matrix.yaml");
|
|
103781
104906
|
if (await fs3.exists(matrixPath)) {
|
|
103782
104907
|
const data = await loadStructuredSpurConfig(matrixPath, {
|
|
103783
104908
|
validateJsonSchema: true,
|
|
@@ -103788,7 +104913,7 @@ async function loadSectionMatrixUncached(projectRoot) {
|
|
|
103788
104913
|
}
|
|
103789
104914
|
throw new Error(`no canonical section-matrix found for task section authority (F92 R1); tried:
|
|
103790
104915
|
` + ` - ${localPath}
|
|
103791
|
-
` + (root !== null ? ` - ${
|
|
104916
|
+
` + (root !== null ? ` - ${join33(root, "tasks", "section-matrix.yaml")}
|
|
103792
104917
|
` : "") + "copy/generate section-matrix.yaml from the canonical build-time matrix asset (repo `config` `tasks` tree) into one of those paths");
|
|
103793
104918
|
}
|
|
103794
104919
|
|
|
@@ -103807,23 +104932,23 @@ function registerTeamCommand(program2, context4) {
|
|
|
103807
104932
|
const code = await runTeamAssign(taskId, agentId, context4);
|
|
103808
104933
|
context4.setExitCode(code);
|
|
103809
104934
|
});
|
|
103810
|
-
noun.command("status").description("List agent specs and their run status; --by-team groups by team (0258 R4).").option(
|
|
104935
|
+
noun.command("status").description("List agent specs and their run status; --by-team groups by team (0258 R4).").option(...SHARED_OPTIONS.json).option("--by-team", "Group specs by their agent.team.<id> membership").option("--server <url>", "Server API URL for live run status", DEFAULT_SERVER).action(async (options) => {
|
|
103811
104936
|
const code = options.byTeam ? await runTeamStatusGrouped(options, context4) : await runTeamStatus(options, context4);
|
|
103812
104937
|
context4.setExitCode(code);
|
|
103813
104938
|
});
|
|
103814
|
-
noun.command("up").description("Materialize a team roster into agent specs; best-effort start when spur serve is reachable.").argument("<team>", "Team id (agent.team.<team>)").option("--check", "Dry-run: show the add/prune diff without writing").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(
|
|
104939
|
+
noun.command("up").description("Materialize a team roster into agent specs; best-effort start when spur serve is reachable.").argument("<team>", "Team id (agent.team.<team>)").option("--check", "Dry-run: show the add/prune diff without writing").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (team, options) => {
|
|
103815
104940
|
const code = await runTeamUp(team, options, context4);
|
|
103816
104941
|
context4.setExitCode(code);
|
|
103817
104942
|
});
|
|
103818
|
-
noun.command("down").description("Tear down a team: stop members; --purge also removes generated specs.").argument("<team>", "Team id").option("--purge", "Also delete spur:generated specs (never manual / ref:)").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(
|
|
104943
|
+
noun.command("down").description("Tear down a team: stop members; --purge also removes generated specs.").argument("<team>", "Team id").option("--purge", "Also delete spur:generated specs (never manual / ref:)").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (team, options) => {
|
|
103819
104944
|
const code = await runTeamDown(team, options, context4);
|
|
103820
104945
|
context4.setExitCode(code);
|
|
103821
104946
|
});
|
|
103822
|
-
noun.command("start").description("Start a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(
|
|
104947
|
+
noun.command("start").description("Start a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (agentId, options) => {
|
|
103823
104948
|
const code = await runTeamStart(agentId, options, context4);
|
|
103824
104949
|
context4.setExitCode(code);
|
|
103825
104950
|
});
|
|
103826
|
-
noun.command("stop").description("Stop a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(
|
|
104951
|
+
noun.command("stop").description("Stop a supervised agent process (requires spur serve).").argument("<agent-id>", "Agent spec id").option("--server <url>", "Server API URL", DEFAULT_SERVER).option(...SHARED_OPTIONS.json).action(async (agentId, options) => {
|
|
103827
104952
|
const code = await runTeamStop(agentId, options, context4);
|
|
103828
104953
|
context4.setExitCode(code);
|
|
103829
104954
|
});
|
|
@@ -104062,9 +105187,88 @@ init_dist6();
|
|
|
104062
105187
|
init_dist5();
|
|
104063
105188
|
await init_src3();
|
|
104064
105189
|
import { closeSync as closeSync5, fstatSync as fstatSync3, openSync as openSync5, readSync as readSync3 } from "fs";
|
|
104065
|
-
import { join as
|
|
105190
|
+
import { join as join34, resolve as resolve19 } from "path";
|
|
104066
105191
|
import { createInterface } from "readline";
|
|
104067
105192
|
import { setTimeout as sleep5 } from "timers/promises";
|
|
105193
|
+
|
|
105194
|
+
// src/workflow/mermaid-render.ts
|
|
105195
|
+
function esc2(text4) {
|
|
105196
|
+
return text4.replace(/"/g, """).replace(/\[/g, "[").replace(/\]/g, "]");
|
|
105197
|
+
}
|
|
105198
|
+
function classLine(id, cls) {
|
|
105199
|
+
return ` class ${esc2(id)} ${cls};`;
|
|
105200
|
+
}
|
|
105201
|
+
function renderWorkflowMermaid(def) {
|
|
105202
|
+
const lines = [];
|
|
105203
|
+
lines.push("```mermaid");
|
|
105204
|
+
lines.push("flowchart LR");
|
|
105205
|
+
lines.push(" classDef terminal fill:#d4edda,stroke:#1e7e34,color:#0a3d1f;");
|
|
105206
|
+
lines.push(" classDef failure fill:#f8d7da,stroke:#c62828,color:#5f1414;");
|
|
105207
|
+
lines.push(" classDef initial fill:#fff3cd,stroke:#b8860b,color:#5a4a00;");
|
|
105208
|
+
lines.push(" classDef gate fill:#e8e8f8,stroke:#5b5bd6,color:#1a1a5e;");
|
|
105209
|
+
lines.push(" classDef decision fill:#ffe9d1,stroke:#e07b00,color:#6b3a00;");
|
|
105210
|
+
lines.push(" classDef parallel fill:#f0f4f8,stroke:#2c7fb8,color:#123a55;");
|
|
105211
|
+
if (def.kind === "transition-flow") {
|
|
105212
|
+
const terminal = new Set(def.terminalNodes ?? []);
|
|
105213
|
+
const typeShape = {
|
|
105214
|
+
gate: '{{"',
|
|
105215
|
+
decision: '{"',
|
|
105216
|
+
parallel: '[("'
|
|
105217
|
+
};
|
|
105218
|
+
for (const node of def.nodes) {
|
|
105219
|
+
const id = node.id;
|
|
105220
|
+
const type = node.type ?? "action";
|
|
105221
|
+
const terminalNode = terminal.has(id);
|
|
105222
|
+
if (terminalNode) {
|
|
105223
|
+
lines.push(` ${esc2(id)}(["${esc2(id)}"])`);
|
|
105224
|
+
lines.push(classLine(id, "terminal"));
|
|
105225
|
+
} else if (type === "action") {
|
|
105226
|
+
lines.push(` ${esc2(id)}["${esc2(id)}"]`);
|
|
105227
|
+
} else {
|
|
105228
|
+
const open = typeShape[type] ?? '["';
|
|
105229
|
+
lines.push(` ${esc2(id)}${open}${esc2(id)}"]`);
|
|
105230
|
+
const cls = type === "gate" ? "gate" : type === "decision" ? "decision" : "parallel";
|
|
105231
|
+
lines.push(classLine(id, cls));
|
|
105232
|
+
}
|
|
105233
|
+
}
|
|
105234
|
+
lines.push(classLine(def.initialNode, "initial"));
|
|
105235
|
+
for (const edge of def.edges) {
|
|
105236
|
+
const label = [edge.condition !== undefined ? `cond:${edge.condition.kind}` : undefined, edge.description].filter((s3) => s3 !== undefined).join(" \xB7 ");
|
|
105237
|
+
lines.push(label.length > 0 ? ` ${esc2(edge.from)} -->|${esc2(label)}| ${esc2(edge.to)}` : ` ${esc2(edge.from)} --> ${esc2(edge.to)}`);
|
|
105238
|
+
}
|
|
105239
|
+
} else {
|
|
105240
|
+
const terminal = new Set(def.terminalStates ?? []);
|
|
105241
|
+
const failure = new Set(def.failureStates ?? []);
|
|
105242
|
+
for (const state of def.states) {
|
|
105243
|
+
const id = state.id;
|
|
105244
|
+
const isTerminal = terminal.has(id);
|
|
105245
|
+
const isFailure = failure.has(id);
|
|
105246
|
+
if (isFailure) {
|
|
105247
|
+
lines.push(` ${esc2(id)}["${esc2(id)}"]`);
|
|
105248
|
+
lines.push(classLine(id, "failure"));
|
|
105249
|
+
} else if (isTerminal) {
|
|
105250
|
+
lines.push(` ${esc2(id)}(["${esc2(id)}"])`);
|
|
105251
|
+
lines.push(classLine(id, "terminal"));
|
|
105252
|
+
} else {
|
|
105253
|
+
lines.push(` ${esc2(id)}["${esc2(id)}"]`);
|
|
105254
|
+
}
|
|
105255
|
+
}
|
|
105256
|
+
lines.push(classLine(def.initialState, "initial"));
|
|
105257
|
+
for (const t of def.transitions) {
|
|
105258
|
+
const label = [
|
|
105259
|
+
t.trigger !== undefined ? `trigger:${t.trigger}` : undefined,
|
|
105260
|
+
t.guard !== undefined ? `guard:${t.guard.kind}` : undefined,
|
|
105261
|
+
t.description
|
|
105262
|
+
].filter((s3) => s3 !== undefined).join(" \xB7 ");
|
|
105263
|
+
lines.push(label.length > 0 ? ` ${esc2(t.from)} -->|${esc2(label)}| ${esc2(t.to)}` : ` ${esc2(t.from)} --> ${esc2(t.to)}`);
|
|
105264
|
+
}
|
|
105265
|
+
}
|
|
105266
|
+
lines.push("```");
|
|
105267
|
+
return lines.join(`
|
|
105268
|
+
`);
|
|
105269
|
+
}
|
|
105270
|
+
|
|
105271
|
+
// src/commands/workflow.ts
|
|
104068
105272
|
function shQuote2(value2) {
|
|
104069
105273
|
return `'${value2.replace(/'/g, `'\\''`)}'`;
|
|
104070
105274
|
}
|
|
@@ -104160,12 +105364,22 @@ function registerWorkflowCommand(program2, context4) {
|
|
|
104160
105364
|
} : {}
|
|
104161
105365
|
});
|
|
104162
105366
|
const workflow = program2.command("workflow").summary("validate and execute workflow YAML files");
|
|
104163
|
-
workflow.command("validate").description("Validate a workflow definition.").argument("<file>", "Workflow YAML file").option(
|
|
105367
|
+
workflow.command("validate").description("Validate a workflow definition.").argument("<file>", "Workflow YAML file").option(...SHARED_OPTIONS.noSchema).option(...SHARED_OPTIONS.jsonSupported).action(async (file2, options) => {
|
|
104164
105368
|
const result = await makeSvc().validate(file2, { validateSchema: options.schema });
|
|
104165
105369
|
if (options.json) {
|
|
104166
105370
|
context4.output.write(toJson2(result));
|
|
104167
105371
|
} else if (result.valid) {
|
|
104168
105372
|
context4.output.write(`workflow valid: ${result.workflow.name}`);
|
|
105373
|
+
const c3 = result.composition;
|
|
105374
|
+
if (c3 && (c3.findings.length > 0 || c3.suppressed > 0)) {
|
|
105375
|
+
for (const f of c3.findings) {
|
|
105376
|
+
const m = f.measure.kind === "shell-lines" ? `${f.measure.measured} shell lines (threshold ${f.measure.threshold})` : `${f.measure.measured} prompt chars (severity ${f.measure.severity})`;
|
|
105377
|
+
context4.output.error(`composition advisory: ${f.actionKey} \u2014 ${m} \u2014 ${f.recommendation}`);
|
|
105378
|
+
}
|
|
105379
|
+
if (c3.suppressed > 0) {
|
|
105380
|
+
context4.output.error(`composition advisory: ${c3.suppressed} shell finding(s) suppressed by baseline dispositions (guards excluded wholesale; docs/design/workflow-shell-ownership.md)`);
|
|
105381
|
+
}
|
|
105382
|
+
}
|
|
104169
105383
|
} else {
|
|
104170
105384
|
context4.output.error(`workflow invalid: ${result.file}
|
|
104171
105385
|
${result.errors.map((m) => ` - ${m}`).join(`
|
|
@@ -104173,7 +105387,7 @@ ${result.errors.map((m) => ` - ${m}`).join(`
|
|
|
104173
105387
|
}
|
|
104174
105388
|
context4.setExitCode(result.valid ? 0 : 1);
|
|
104175
105389
|
});
|
|
104176
|
-
workflow.command("run").description("Execute a workflow definition.").argument("<file>", "Workflow YAML file").option(
|
|
105390
|
+
workflow.command("run").description("Execute a workflow definition.").argument("<file>", "Workflow YAML file").option(...SHARED_OPTIONS.runIdWorkflow).option("--vars <json>", `Per-run variable overrides as a JSON object, e.g. '{"taskId":"0042"}'`).option(...SHARED_OPTIONS.dryRunWorkflowValidate).option("--async", "Start the workflow in the background and exit immediately \u2014 monitor with `spur workflow trace <run-id>`").option("--no-plan", "Suppress the run-start plan preview (synchronous runs only)").option("--quiet", "Suppress plan and per-step progress; keep the final summary").option("--silent", "Suppress all routine output; errors still set a non-zero exit status").option(...SHARED_OPTIONS.verboseWorkflow).option("--detail <level>", "Human detail level: minimal, invocation, or full").option("--trace-file", "Append a redacted schema-versioned JSONL trace under .spur/runs/workflow/").option("--no-log", "Opt out of writing the consolidated .spur/run/<RUNID>.log").option("--steer", "Accept local in-process steering commands on stdin at declared action boundaries").option(...SHARED_OPTIONS.jsonSupported).action(async (file2, options) => {
|
|
104177
105391
|
const json3 = options.json === true;
|
|
104178
105392
|
const silent = !json3 && options.silent === true;
|
|
104179
105393
|
const quiet = !json3 && options.quiet === true;
|
|
@@ -104289,7 +105503,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
104289
105503
|
}
|
|
104290
105504
|
const runLog = options.log === false ? undefined : new WorkflowRunLogSink({
|
|
104291
105505
|
bus,
|
|
104292
|
-
dir:
|
|
105506
|
+
dir: join34(context4.cwd, ".spur", "run"),
|
|
104293
105507
|
runId,
|
|
104294
105508
|
...planPreview !== undefined ? { planPreview } : {},
|
|
104295
105509
|
...await resolveOutputLogConfig(context4.cwd)
|
|
@@ -104386,7 +105600,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
104386
105600
|
context4.setExitCode(result.status === "done" ? 0 : 1);
|
|
104387
105601
|
await maybeTriggerHistoryRefresh(context4, "pipeline-run", runId);
|
|
104388
105602
|
});
|
|
104389
|
-
workflow.command("continue").description("Resume a paused (HITL) workflow run. Omit run-id to resume the most recent paused run.").argument("[run-id]", "Run ID to resume (default: the most recent paused run)").option("--yes", "Skip the CLI resume confirmation (does not set the persisted HITL answer)").option("--answer <yes|no|cancel>", "Inject a HITL gate answer before guard re-evaluation (0433). Does not imply --yes.").option(
|
|
105603
|
+
workflow.command("continue").description("Resume a paused (HITL) workflow run. Omit run-id to resume the most recent paused run.").argument("[run-id]", "Run ID to resume (default: the most recent paused run)").option("--yes", "Skip the CLI resume confirmation (does not set the persisted HITL answer)").option("--answer <yes|no|cancel>", "Inject a HITL gate answer before guard re-evaluation (0433). Does not imply --yes.").option(...SHARED_OPTIONS.jsonSupported).action(async (runId, options) => {
|
|
104390
105604
|
const json3 = options.json === true;
|
|
104391
105605
|
let hitlAnswer;
|
|
104392
105606
|
if (options.answer !== undefined) {
|
|
@@ -104439,7 +105653,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
104439
105653
|
ledger.unsubscribe();
|
|
104440
105654
|
}
|
|
104441
105655
|
});
|
|
104442
|
-
workflow.command("clean").description("Housekeeping: finalize orphaned runs stuck in running/pending past a staleness threshold " + "(mark as failed) and reclaim retained run logs older than workflow.logRetentionDays. " + "`--logs` scopes to log reclamation only. To cancel a single live run by id, use " + "`spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes (stale-run scope only)", "30").option(
|
|
105656
|
+
workflow.command("clean").description("Housekeeping: finalize orphaned runs stuck in running/pending past a staleness threshold " + "(mark as failed) and reclaim retained run logs older than workflow.logRetentionDays. " + "`--logs` scopes to log reclamation only. To cancel a single live run by id, use " + "`spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes (stale-run scope only)", "30").option(...SHARED_OPTIONS.forceWorkflowClean).option("--logs", "Scope to retained run-log reclamation only (skip stale-run finalization)").option(...SHARED_OPTIONS.dryRunWorkflowClean).option(...SHARED_OPTIONS.jsonSupported).action(async (options) => {
|
|
104443
105657
|
const dryRun = options.dryRun === true;
|
|
104444
105658
|
const logsOnly = options.logs === true;
|
|
104445
105659
|
const force = options.force === true;
|
|
@@ -104481,7 +105695,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
104481
105695
|
}
|
|
104482
105696
|
}
|
|
104483
105697
|
});
|
|
104484
|
-
workflow.command("cancel").description("Cancel a single non-terminal run by id (mark as failed). The bulk/stale variant is `spur workflow clean`.").argument("<run-id>", "Run id to cancel").option(
|
|
105698
|
+
workflow.command("cancel").description("Cancel a single non-terminal run by id (mark as failed). The bulk/stale variant is `spur workflow clean`.").argument("<run-id>", "Run id to cancel").option(...SHARED_OPTIONS.jsonSupported).action(async (runId, options) => {
|
|
104485
105699
|
const result = await makeSvc(options.json).cancel(runId);
|
|
104486
105700
|
if (options.json) {
|
|
104487
105701
|
context4.output.write(toJson2(result));
|
|
@@ -104499,7 +105713,7 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
104499
105713
|
context4.output.write(`Run ${runId} already terminal (${result.status}) \u2014 no change.`);
|
|
104500
105714
|
}
|
|
104501
105715
|
});
|
|
104502
|
-
workflow.command("list").description("List available workflow YAML files.").option(
|
|
105716
|
+
workflow.command("list").description("List available workflow YAML files.").option(...SHARED_OPTIONS.jsonSupported).action(async (options) => {
|
|
104503
105717
|
const paths = await resolveWorkflowPaths(context4.cwd);
|
|
104504
105718
|
const result = await makeSvc().list(paths);
|
|
104505
105719
|
if (options.json) {
|
|
@@ -104508,7 +105722,19 @@ Monitor with: spur workflow trace ${runId2} --follow`);
|
|
|
104508
105722
|
context4.output.write(formatListHuman(result));
|
|
104509
105723
|
}
|
|
104510
105724
|
});
|
|
104511
|
-
workflow.command("
|
|
105725
|
+
workflow.command("show").description("Render a workflow definition as a mermaid FSM diagram.").argument("<file>", "Workflow YAML file").action(async (file2) => {
|
|
105726
|
+
const filePath = resolve19(context4.cwd, file2);
|
|
105727
|
+
let def;
|
|
105728
|
+
try {
|
|
105729
|
+
def = await loadWorkflowDef(filePath, { validateSchema: true });
|
|
105730
|
+
} catch (err) {
|
|
105731
|
+
context4.output.error(`workflow show: cannot read or parse ${file2} \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
105732
|
+
context4.setExitCode(1);
|
|
105733
|
+
return;
|
|
105734
|
+
}
|
|
105735
|
+
context4.output.write(renderWorkflowMermaid(def));
|
|
105736
|
+
});
|
|
105737
|
+
workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option(...SHARED_OPTIONS.statusDoneFailedRunning).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option("--follow", "Replay a run timeline and poll persisted state until it becomes terminal").option(...SHARED_OPTIONS.pollWorkflow, "1000").option("--output", "With --follow: stream .spur/run/<RUNID>.log instead of the DB timeline").option(...SHARED_OPTIONS.jsonSupported).action(async (runId, options) => {
|
|
104512
105738
|
const svc = makeSvc();
|
|
104513
105739
|
const last = parseInt(options.last, 10);
|
|
104514
105740
|
if (Number.isNaN(last) || last < 1) {
|
|
@@ -104619,18 +105845,18 @@ function formatTraceList2(result) {
|
|
|
104619
105845
|
`);
|
|
104620
105846
|
}
|
|
104621
105847
|
function formatTraceTimeline(result) {
|
|
104622
|
-
const { run, events: events2 } = result;
|
|
104623
|
-
const dryLabel =
|
|
104624
|
-
const reasonLabel =
|
|
105848
|
+
const { run: run2, events: events2 } = result;
|
|
105849
|
+
const dryLabel = run2.isDryRun ? " [DRY RUN]" : "";
|
|
105850
|
+
const reasonLabel = run2.failureReason ? ` \u2014 ${run2.failureReason}` : "";
|
|
104625
105851
|
const lines = [
|
|
104626
|
-
`Run: ${
|
|
104627
|
-
`Project: ${
|
|
104628
|
-
`Started: ${
|
|
104629
|
-
`Outcome: ${
|
|
105852
|
+
`Run: ${run2.runId} \u2014 ${run2.workflowName} (${run2.mode}) \u2014 ${run2.status}${dryLabel}${reasonLabel}`,
|
|
105853
|
+
`Project: ${run2.project.name} (${run2.project.root})`,
|
|
105854
|
+
`Started: ${run2.startedAt} Completed: ${run2.completedAt ?? "unavailable"} Duration: ${run2.durationMs === null ? "unavailable" : `${run2.durationMs}ms`}`,
|
|
105855
|
+
`Outcome: ${run2.outcome} Events: ${events2.length}`,
|
|
104630
105856
|
""
|
|
104631
105857
|
];
|
|
104632
|
-
if (
|
|
104633
|
-
lines.splice(4, 0, `Next: ${
|
|
105858
|
+
if (run2.nextAction !== undefined)
|
|
105859
|
+
lines.splice(4, 0, `Next: ${run2.nextAction.label} \u2014 ${run2.nextAction.value}`);
|
|
104634
105860
|
if (result.outputArtifact !== undefined) {
|
|
104635
105861
|
lines.push(`Run log: ${result.outputArtifact} (tail -f for live view)`);
|
|
104636
105862
|
lines.push("");
|
|
@@ -104748,7 +105974,7 @@ function readRunLogChunk(logPath, offset) {
|
|
|
104748
105974
|
}
|
|
104749
105975
|
}
|
|
104750
105976
|
async function followRunLog(service, runId, dir, pollMs, write, wait = (ms) => sleep5(ms)) {
|
|
104751
|
-
const logPath =
|
|
105977
|
+
const logPath = join34(dir, ".spur", "run", `${runId}.log`);
|
|
104752
105978
|
let offset = 0;
|
|
104753
105979
|
let everRead = false;
|
|
104754
105980
|
while (true) {
|
|
@@ -104811,7 +106037,7 @@ init_src();
|
|
|
104811
106037
|
init_src2();
|
|
104812
106038
|
init_dist5();
|
|
104813
106039
|
await init_src3();
|
|
104814
|
-
import { dirname as dirname22, join as
|
|
106040
|
+
import { dirname as dirname22, join as join35, resolve as resolve20 } from "path";
|
|
104815
106041
|
import { isatty as isatty4 } from "tty";
|
|
104816
106042
|
|
|
104817
106043
|
// ../../node_modules/.bun/@clack+core@1.4.1/node_modules/@clack/core/dist/index.mjs
|
|
@@ -106269,7 +107495,7 @@ function createCliContext(options) {
|
|
|
106269
107495
|
function noopSetExitCode(_code) {}
|
|
106270
107496
|
async function createMigratedDbAdapter(cwd = process.cwd(), env = process.env, dbUrl) {
|
|
106271
107497
|
const config4 = buildConfigFromEnv(env);
|
|
106272
|
-
const configuredUrl = env.DATABASE_URL === undefined ?
|
|
107498
|
+
const configuredUrl = env.DATABASE_URL === undefined ? join35(cwd, DEFAULT_DATABASE_URL) : config4.database.url;
|
|
106273
107499
|
const url2 = dbUrl ?? configuredUrl;
|
|
106274
107500
|
if (url2 !== IN_MEMORY_DATABASE_URL) {
|
|
106275
107501
|
await createNodeFileSystem3().ensureDir(dirname22(url2));
|
|
@@ -106329,15 +107555,21 @@ async function runCommandDispatch(argv, context4, output2) {
|
|
|
106329
107555
|
});
|
|
106330
107556
|
program2.option("-v, --cli-verbose", "Show internal diagnostics");
|
|
106331
107557
|
registerAgentCommand(program2, context4);
|
|
107558
|
+
registerBuilderCommand(program2, context4);
|
|
106332
107559
|
registerFeatureCommand(program2, context4);
|
|
106333
107560
|
registerHistoryCommand(program2, context4);
|
|
106334
|
-
registerInitCommand(program2, context4);
|
|
106335
107561
|
registerMessageCommand(program2, context4);
|
|
106336
|
-
registerMigrateCommand(program2, context4);
|
|
106337
107562
|
registerProjectsCommand(program2, context4);
|
|
106338
107563
|
registerRuleCommand(program2, context4);
|
|
106339
|
-
|
|
106340
|
-
|
|
107564
|
+
const selfCommand = program2.command("self").summary("inspect and manage the Spur installation itself");
|
|
107565
|
+
registerInitCommand(selfCommand, context4);
|
|
107566
|
+
registerMigrateCommand(selfCommand, context4);
|
|
107567
|
+
registerServeCommand(selfCommand, context4);
|
|
107568
|
+
registerStatusCommand(selfCommand, context4);
|
|
107569
|
+
registerInitCommand(program2, context4, { hidden: true });
|
|
107570
|
+
registerMigrateCommand(program2, context4, { hidden: true });
|
|
107571
|
+
registerServeCommand(program2, context4, { hidden: true });
|
|
107572
|
+
registerStatusCommand(program2, context4, { hidden: true });
|
|
106341
107573
|
registerTeamCommand(program2, context4);
|
|
106342
107574
|
registerTaskCommand(program2, context4);
|
|
106343
107575
|
registerWorkflowCommand(program2, context4);
|