@gobing-ai/spur 0.2.8 → 0.2.10

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/spur.js CHANGED
@@ -50876,6 +50876,13 @@ var init_run_dao = __esm(() => {
50876
50876
  metadata_json = json_set(COALESCE(NULLIF(metadata_json, ''), '{}'), '$.staleReason', ?2)
50877
50877
  WHERE id = ?3 AND status IN ('running', 'pending')`, nowIso, reason, runId);
50878
50878
  }
50879
+ async setPid(runId, pid) {
50880
+ await this.adapter.run("UPDATE runs SET pid = ? WHERE id = ?", pid, runId);
50881
+ }
50882
+ async getPid(runId) {
50883
+ const row = await this.adapter.queryFirst("SELECT pid FROM runs WHERE id = ?", runId);
50884
+ return row?.pid ?? null;
50885
+ }
50879
50886
  };
50880
50887
  });
50881
50888
 
@@ -55315,7 +55322,9 @@ CREATE TABLE IF NOT EXISTS queue_jobs (
55315
55322
  );
55316
55323
 
55317
55324
  CREATE INDEX IF NOT EXISTS queue_jobs_ready_idx ON queue_jobs (status, next_retry_at, created_at);
55318
- `, CLI_SCHEMA_SQL, CLI_MIGRATIONS, CLI_MIGRATION_FILE_MARKER = "_spur_cli_";
55325
+ `, CLI_SCHEMA_SQL, RUN_PID_COLUMN_SCHEMA_SQL = `
55326
+ ALTER TABLE runs ADD COLUMN pid INTEGER;
55327
+ `, CLI_MIGRATIONS, CLI_MIGRATION_FILE_MARKER = "_spur_cli_";
55319
55328
  var init_migrations = __esm(() => {
55320
55329
  init_dist7();
55321
55330
  init_dist8();
@@ -55341,7 +55350,8 @@ ${QUEUE_JOBS_SCHEMA_SQL}
55341
55350
  { id: "0001_spur_cli_team_inbox", sql: INBOX_MESSAGES_SCHEMA_SQL },
55342
55351
  { id: "0002_spur_cli_rule_history", sql: RULE_ENGINE_SCHEMA_SQL },
55343
55352
  { id: "0003_spur_cli_planning", sql: PLANNING_SCHEMA_SQL },
55344
- { id: "0004_spur_cli_queue_jobs", sql: QUEUE_JOBS_SCHEMA_SQL }
55353
+ { id: "0004_spur_cli_queue_jobs", sql: QUEUE_JOBS_SCHEMA_SQL },
55354
+ { id: "0005_spur_cli_run_pid", sql: RUN_PID_COLUMN_SCHEMA_SQL }
55345
55355
  ];
55346
55356
  });
55347
55357
 
@@ -57807,7 +57817,7 @@ class PlanningWriteService {
57807
57817
  const raw = await this.fs.readFile(ref.filePath);
57808
57818
  doc2 = MarkdownDocument.parse(raw, domain2);
57809
57819
  }
57810
- const currentStatus = doc2.frontmatterData?.status ?? "";
57820
+ const currentStatus = normalizeStatusForDomain(doc2.frontmatterData?.status ?? "", domain2);
57811
57821
  const phantomsBefore = new Set(phantomSections(doc2, domain2));
57812
57822
  applyMutation(doc2, mutation);
57813
57823
  assertNoNewPhantomSections(doc2, domain2, phantomsBefore);
@@ -57823,7 +57833,7 @@ class PlanningWriteService {
57823
57833
  throw new Error(`Frontmatter validation failed for feature ${ref.id}: ${result.error.issues.map((i2) => i2.message).join("; ")}`);
57824
57834
  }
57825
57835
  }
57826
- const newStatus = doc2.frontmatterData?.status ?? "";
57836
+ const newStatus = normalizeStatusForDomain(doc2.frontmatterData?.status ?? "", domain2);
57827
57837
  const statusChanged = mutation.kind === "transition" && newStatus !== currentStatus;
57828
57838
  let fromStatus;
57829
57839
  let toStatus;
@@ -57916,6 +57926,16 @@ function resolveEventName(kind, domain2, statusChanged) {
57916
57926
  }
57917
57927
  return domain2 === "task" ? "task.updated" : "feature.updated";
57918
57928
  }
57929
+ function normalizeStatusForDomain(status, domain2) {
57930
+ if (!status)
57931
+ return status;
57932
+ const normalize = domain2 === "task" ? normalizeTaskStatus : normalizeFeatureStatus;
57933
+ try {
57934
+ return normalize(status);
57935
+ } catch {
57936
+ return status;
57937
+ }
57938
+ }
57919
57939
  var init_planning_write_service = __esm(() => {
57920
57940
  init_src2();
57921
57941
  });
@@ -58461,6 +58481,51 @@ function isPlaceholderBody(body) {
58461
58481
  const stripped = body.replace(/<!--[\s\S]*?-->/g, "").replace(/^\s*>\s*TBD\s*$/gim, "").trim();
58462
58482
  return stripped.length === 0;
58463
58483
  }
58484
+ function hasPopulatedPriorityTable(body) {
58485
+ for (const line of body.split(`
58486
+ `)) {
58487
+ const cells = line.split("|");
58488
+ if (cells.length < 3)
58489
+ continue;
58490
+ const severityIdx = cells.findIndex((c3) => /^\s*P[1-4]\s*$/.test(c3));
58491
+ if (severityIdx === -1)
58492
+ continue;
58493
+ const hasContent = cells.some((c3, i2) => i2 !== severityIdx && c3.trim().length > 0);
58494
+ if (hasContent)
58495
+ return true;
58496
+ }
58497
+ return false;
58498
+ }
58499
+ function isReviewScaffold(body) {
58500
+ if (isPlaceholderBody(body))
58501
+ return true;
58502
+ if (hasPopulatedPriorityTable(body))
58503
+ return false;
58504
+ let sawEmptyPRow = false;
58505
+ for (const line of body.split(`
58506
+ `)) {
58507
+ if (!line.includes("|"))
58508
+ continue;
58509
+ const cells = line.split("|").map((c3) => c3.trim());
58510
+ const isSeparator = cells.every((c3) => c3 === "" || /^:?-+:?$/.test(c3));
58511
+ const isEmptyPRow = cells.every((c3) => c3 === "" || /^P[1-4]$/.test(c3)) && cells.some((c3) => /^P[1-4]$/.test(c3));
58512
+ const isHeader = cells.some((c3) => /severity|file|finding|recommendation/i.test(c3));
58513
+ if (isEmptyPRow)
58514
+ sawEmptyPRow = true;
58515
+ else if (!isSeparator && !isHeader)
58516
+ return false;
58517
+ }
58518
+ return sawEmptyPRow;
58519
+ }
58520
+ function isProseOnlyReview(body) {
58521
+ if (isPlaceholderBody(body))
58522
+ return true;
58523
+ if (hasPopulatedPriorityTable(body))
58524
+ return false;
58525
+ const stripped = body.replace(/<!--[\s\S]*?-->/g, "");
58526
+ return !stripped.split(`
58527
+ `).some((l) => l.includes("|"));
58528
+ }
58464
58529
  var TaskCheckService;
58465
58530
  var init_task_check = __esm(() => {
58466
58531
  init_src2();
@@ -58531,10 +58596,11 @@ var init_task_check = __esm(() => {
58531
58596
  }
58532
58597
  }
58533
58598
  const revBody = doc2.getSection("Review");
58534
- const revAllowed = (entry?.required ?? []).includes("Review") || (entry?.optional ?? []).includes("Review");
58535
- if (revBody !== null && !isPlaceholderBody(revBody) && revAllowed) {
58536
- const hasPColumn = /P[1-4]/.test(revBody);
58537
- if (!hasPColumn) {
58599
+ const revRequired = (entry?.required ?? []).includes("Review");
58600
+ const revAllowed = revRequired || (entry?.optional ?? []).includes("Review");
58601
+ const revScaffoldTolerated = revRequired ? isPlaceholderBody(revBody ?? "") : isReviewScaffold(revBody ?? "") || isProseOnlyReview(revBody ?? "");
58602
+ if (revBody !== null && !revScaffoldTolerated && revAllowed) {
58603
+ if (!hasPopulatedPriorityTable(revBody)) {
58538
58604
  findings.push({
58539
58605
  layer: "L3",
58540
58606
  severity: "error",
@@ -58557,7 +58623,7 @@ var init_task_check = __esm(() => {
58557
58623
  }
58558
58624
  const planBody = doc2.getSection("Plan");
58559
58625
  if (planBody !== null && !isPlaceholderBody(planBody)) {
58560
- const isList = /^\s*[-*]\s|^\s*\d+\.\s/.test(planBody.trimStart());
58626
+ const isList = /^\s*[-*]\s|^\s*\d+\.\s/m.test(planBody);
58561
58627
  const isTable2 = /\|/.test(planBody);
58562
58628
  if (!isList && !isTable2) {
58563
58629
  findings.push({
@@ -58599,7 +58665,7 @@ var init_task_check = __esm(() => {
58599
58665
  layer: "L4",
58600
58666
  severity: "warning",
58601
58667
  section: "",
58602
- message: "Missing feature_id \u2014 every task should reference a feature (one direction, DD-07)"
58668
+ message: "Missing feature_id \u2014 every task should reference a feature (one direction, DD-07). To link: `spur task update <wbs> --feature <id>`, or use the sp:spur-dev feature-link helper."
58603
58669
  });
58604
58670
  }
58605
58671
  if (parentWbs && parentWbs.length > 0) {
@@ -58833,7 +58899,7 @@ function renderTesting(v) {
58833
58899
  lines.push(`| ${req.id} | ${req.status} | ${evidence} |`);
58834
58900
  }
58835
58901
  }
58836
- lines.push("");
58902
+ lines.push("- Coverage: N/A (verdict-based; verify pipeline does not measure code coverage)");
58837
58903
  return lines.join(`
58838
58904
  `);
58839
58905
  }
@@ -58988,7 +59054,7 @@ function sectionIsBare(doc2, name) {
58988
59054
  const body = doc2.getSection(name);
58989
59055
  if (body === null)
58990
59056
  return true;
58991
- const trimmed = body.trim();
59057
+ const trimmed = body.replace(/<!--[\s\S]*?-->/g, "").trim();
58992
59058
  if (trimmed === "")
58993
59059
  return true;
58994
59060
  if (/^Pipeline run \d{4}\b/.test(trimmed))
@@ -60514,6 +60580,44 @@ var now = () => new Date().toISOString();
60514
60580
  import { access as access2, readdir as readdir3, readFile as readFile2, realpath, stat as stat2 } from "fs/promises";
60515
60581
  import { homedir as homedir3 } from "os";
60516
60582
  import { join as join11, resolve as resolve3 } from "path";
60583
+ function signalSubprocess(pid) {
60584
+ try {
60585
+ process.kill(-pid, "SIGTERM");
60586
+ return true;
60587
+ } catch {}
60588
+ try {
60589
+ process.kill(pid, "SIGTERM");
60590
+ return true;
60591
+ } catch {
60592
+ return false;
60593
+ }
60594
+ }
60595
+ function withSelfPidRecording(inner, db2) {
60596
+ const stamp = async (runId) => {
60597
+ try {
60598
+ await new RunDao(db2).setPid(runId, process.pid);
60599
+ } catch {}
60600
+ };
60601
+ return new Proxy(inner, {
60602
+ get(target, prop, receiver) {
60603
+ if (prop === "createRun") {
60604
+ return async (record2) => {
60605
+ await target.createRun(record2);
60606
+ await stamp(record2.id);
60607
+ };
60608
+ }
60609
+ if (prop === "createOrAttachRun") {
60610
+ return async (record2) => {
60611
+ const result = await target.createOrAttachRun(record2);
60612
+ await stamp(result.id);
60613
+ return result;
60614
+ };
60615
+ }
60616
+ const value = Reflect.get(target, prop, receiver);
60617
+ return typeof value === "function" ? value.bind(target) : value;
60618
+ }
60619
+ });
60620
+ }
60517
60621
 
60518
60622
  class WorkflowAppService {
60519
60623
  ctx;
@@ -60562,7 +60666,7 @@ class WorkflowAppService {
60562
60666
  }
60563
60667
  }
60564
60668
  async run(file2, opts = {}) {
60565
- const svc = await this.createEngineService();
60669
+ const svc = await this.createEngineService({ recordSelfPid: opts.recordSelfPid === true });
60566
60670
  const runId = opts.runId ?? crypto.randomUUID();
60567
60671
  const isDry = opts.dryRun === true;
60568
60672
  const result = await svc.runFile(file2, {
@@ -60628,6 +60732,21 @@ class WorkflowAppService {
60628
60732
  cleaned: stale.map((r) => ({ runId: r.id, startedAt: r.started_at }))
60629
60733
  };
60630
60734
  }
60735
+ async cancel(runId) {
60736
+ const db2 = await this.ctx.getDb();
60737
+ const dao2 = new RunDao(db2);
60738
+ const before = await dao2.traceRowById(runId);
60739
+ if (!before) {
60740
+ return { runId, finalized: false, status: "not_found", killed: false };
60741
+ }
60742
+ const isNonTerminal = before.status === "running" || before.status === "pending";
60743
+ const pid = isNonTerminal ? await dao2.getPid(runId) : null;
60744
+ const killed = pid != null ? signalSubprocess(pid) : false;
60745
+ await dao2.finalizeStale(runId, "cancelled by operator (spur workflow cancel)");
60746
+ const after = await dao2.traceRowById(runId);
60747
+ const finalized = after?.status === "failed" && before.status !== "failed";
60748
+ return { runId, finalized, status: after?.status ?? "not_found", killed };
60749
+ }
60631
60750
  async continuePaused(runId) {
60632
60751
  const svc = await this.createEngineService();
60633
60752
  const run = await svc.listPausedRuns();
@@ -60751,7 +60870,7 @@ class WorkflowAppService {
60751
60870
  }
60752
60871
  return { run, events: events2 };
60753
60872
  }
60754
- async createEngineService() {
60873
+ async createEngineService(opts = {}) {
60755
60874
  const host = createDefaultWorkflowEngineHost();
60756
60875
  registerSpurBuiltins(host, {
60757
60876
  agentService: this.ctx.agentService(),
@@ -60760,7 +60879,11 @@ class WorkflowAppService {
60760
60879
  httpRequester: this.ctx.httpRequester?.(),
60761
60880
  hostAllowlist: this.ctx.hostAllowlist?.()
60762
60881
  });
60763
- const persistence2 = new DbWorkflowPersistenceAdapter(await this.ctx.getDb());
60882
+ const db2 = await this.ctx.getDb();
60883
+ let persistence2 = new DbWorkflowPersistenceAdapter(db2);
60884
+ if (opts.recordSelfPid === true) {
60885
+ persistence2 = withSelfPidRecording(persistence2, db2);
60886
+ }
60764
60887
  const bus = this.ctx.observabilityBus?.();
60765
60888
  const adapter = bus ? new ObservableWorkflowAdapter(persistence2, bus) : persistence2;
60766
60889
  return new WorkflowService(host, adapter);
@@ -68478,12 +68601,21 @@ function resolveSpurBin() {
68478
68601
  }
68479
68602
 
68480
68603
  // src/workflow/make-lifecycle-adapter.ts
68604
+ function resolveWorkflowPath(context3, profile) {
68605
+ const bundledRoot = bundledConfigRoot();
68606
+ if (bundledRoot !== null) {
68607
+ const bundledPath = join12(bundledRoot, "workflows", `${profile.workflowName}.yaml`);
68608
+ if (existsSync4(bundledPath))
68609
+ return bundledPath;
68610
+ }
68611
+ const projectPath = join12(context3.cwd, ".spur", "workflows", `${profile.workflowName}.yaml`);
68612
+ if (existsSync4(projectPath))
68613
+ return projectPath;
68614
+ return null;
68615
+ }
68481
68616
  function makeLifecycleAdapter(context3, profile) {
68482
- const root = bundledConfigRoot();
68483
- if (root === null)
68484
- return;
68485
- const workflowPath = join12(root, "workflows", `${profile.workflowName}.yaml`);
68486
- if (!existsSync4(workflowPath))
68617
+ const workflowPath = resolveWorkflowPath(context3, profile);
68618
+ if (workflowPath === null)
68487
68619
  return;
68488
68620
  const spurBin = resolveSpurBin();
68489
68621
  return new LifecycleAdapter({
@@ -68732,7 +68864,7 @@ import { join as join13, resolve as resolve4 } from "path";
68732
68864
  var CLI_CONFIG = {
68733
68865
  binaryName: "spur",
68734
68866
  binaryLabel: "spur",
68735
- binaryVersion: "0.2.8",
68867
+ binaryVersion: "0.2.10",
68736
68868
  configDir: ".spur",
68737
68869
  configFile: ".spur/config.yaml",
68738
68870
  databaseFile: ".spur/spur.db"
@@ -68742,21 +68874,21 @@ var CLI_CONFIG = {
68742
68874
  var SCAFFOLD_MANIFEST = [
68743
68875
  { source: "rules/recommended-pre-check.yaml", target: "rules/recommended-pre-check.yaml" },
68744
68876
  { source: "rules/recommended-post-check.yaml", target: "rules/recommended-post-check.yaml" },
68745
- { source: "workflows/basic.yaml", target: "config/workflows/basic.yaml" },
68746
- { source: "workflows/task-lifecycle.yaml", target: "config/workflows/task-lifecycle.yaml" },
68747
- { source: "workflows/feature-lifecycle.yaml", target: "config/workflows/feature-lifecycle.yaml" },
68748
- { source: "workflows/feature-dev.yaml", target: "config/workflows/feature-dev.yaml" },
68749
- { source: "workflows/task-pipeline.yaml", target: "config/workflows/task-pipeline.yaml" },
68750
- { source: "workflows/planning-pipeline.yaml", target: "config/workflows/planning-pipeline.yaml" },
68877
+ { source: "workflows/basic.yaml", target: "workflows/basic.yaml" },
68878
+ { source: "workflows/task-lifecycle.yaml", target: "workflows/task-lifecycle.yaml" },
68879
+ { source: "workflows/feature-lifecycle.yaml", target: "workflows/feature-lifecycle.yaml" },
68880
+ { source: "workflows/feature-dev.yaml", target: "workflows/feature-dev.yaml" },
68881
+ { source: "workflows/task-pipeline.yaml", target: "workflows/task-pipeline.yaml" },
68882
+ { source: "workflows/planning-pipeline.yaml", target: "workflows/planning-pipeline.yaml" },
68751
68883
  { source: "tasks/section-matrix.yaml", target: "tasks/section-matrix.yaml" },
68752
68884
  { source: "templates/task/standard.md", target: "tasks/templates/standard.md" },
68753
68885
  { source: "templates/task/feature-impl.md", target: "tasks/templates/feature-impl.md" },
68754
68886
  { source: "templates/task/issue.md", target: "tasks/templates/issue.md" },
68755
68887
  { source: "templates/task/review.md", target: "tasks/templates/review.md" },
68756
68888
  { source: "templates/task/meta.md", target: "tasks/templates/meta.md" },
68757
- { source: "templates/feature/default.md", target: "config/templates/feature/default.md" },
68758
- { source: "templates/bdd/gherkin.md", target: "config/templates/bdd/gherkin.md" },
68759
- { source: "templates/bdd/checklist.md", target: "config/templates/bdd/checklist.md" },
68889
+ { source: "templates/feature/default.md", target: "templates/feature/default.md" },
68890
+ { source: "templates/bdd/gherkin.md", target: "templates/bdd/gherkin.md" },
68891
+ { source: "templates/bdd/checklist.md", target: "templates/bdd/checklist.md" },
68760
68892
  {
68761
68893
  source: "templates/docs/99_PROJECT_CONSTITUTION.md",
68762
68894
  target: "docs/99_PROJECT_CONSTITUTION.md",
@@ -68769,13 +68901,13 @@ var SCAFFOLD_MANIFEST = [
68769
68901
  { source: "templates/docs/03_ARCHITECTURE.md", target: "docs/03_ARCHITECTURE.md", root: true, preserve: true },
68770
68902
  { source: "templates/docs/04_DESIGN.md", target: "docs/04_DESIGN.md", root: true, preserve: true },
68771
68903
  { source: "templates/docs/05_FEATURES.md", target: "docs/05_FEATURES.md", root: true, preserve: true },
68772
- { source: "templates/docs/99_PROJECT_CONSTITUTION.md", target: "config/templates/docs/99_PROJECT_CONSTITUTION.md" },
68773
- { source: "templates/docs/00_ADR.md", target: "config/templates/docs/00_ADR.md" },
68774
- { source: "templates/docs/01_PRD.md", target: "config/templates/docs/01_PRD.md" },
68775
- { source: "templates/docs/02_ROADMAP.md", target: "config/templates/docs/02_ROADMAP.md" },
68776
- { source: "templates/docs/03_ARCHITECTURE.md", target: "config/templates/docs/03_ARCHITECTURE.md" },
68777
- { source: "templates/docs/04_DESIGN.md", target: "config/templates/docs/04_DESIGN.md" },
68778
- { source: "templates/docs/05_FEATURES.md", target: "config/templates/docs/05_FEATURES.md" }
68904
+ { source: "templates/docs/99_PROJECT_CONSTITUTION.md", target: "templates/docs/99_PROJECT_CONSTITUTION.md" },
68905
+ { source: "templates/docs/00_ADR.md", target: "templates/docs/00_ADR.md" },
68906
+ { source: "templates/docs/01_PRD.md", target: "templates/docs/01_PRD.md" },
68907
+ { source: "templates/docs/02_ROADMAP.md", target: "templates/docs/02_ROADMAP.md" },
68908
+ { source: "templates/docs/03_ARCHITECTURE.md", target: "templates/docs/03_ARCHITECTURE.md" },
68909
+ { source: "templates/docs/04_DESIGN.md", target: "templates/docs/04_DESIGN.md" },
68910
+ { source: "templates/docs/05_FEATURES.md", target: "templates/docs/05_FEATURES.md" }
68779
68911
  ];
68780
68912
 
68781
68913
  // src/commands/init.ts
@@ -77985,6 +78117,18 @@ ${result.content}`);
77985
78117
  context3.output.write(`Set ${key}=${value2} on task ${result.ref.id}`);
77986
78118
  }
77987
78119
  } else if (status !== undefined) {
78120
+ if (options.lifecycle !== false && (status === "done" || status === "testing")) {
78121
+ const adapter = makeLifecycleAdapter(context3, TASK_LIFECYCLE_PROFILE);
78122
+ if (adapter === undefined) {
78123
+ context3.output.error(`warning: lifecycle adapter unavailable \u2014 running \`spur task check\` inline as the ${status} gate. ` + "Restore the bundled task-lifecycle workflow to re-enable the real guard.");
78124
+ const ok = await runDoneGateCheck(context3, wbs, options.folder);
78125
+ if (!ok) {
78126
+ context3.output.error(`Lifecycle transition blocked: \`spur task check ${wbs}\` failed. Fix the findings before transitioning to ${status}.`);
78127
+ context3.setExitCode(1);
78128
+ return;
78129
+ }
78130
+ }
78131
+ }
77988
78132
  const result = await svc.updateStatus(wbs, status);
77989
78133
  if (options.json) {
77990
78134
  context3.output.write(toJson2(result));
@@ -78280,6 +78424,18 @@ function loadTemplateBodies(projectRoot, variant) {
78280
78424
  async function makeCheckService(context3) {
78281
78425
  return new TaskCheckService(context3.fs, await loadSectionMatrix(context3.cwd));
78282
78426
  }
78427
+ async function runDoneGateCheck(context3, wbs, folderOverride) {
78428
+ const foldersConfig = (await resolvePlanningFolders(context3.fs)).foldersConfig;
78429
+ const tasksDir = folderOverride ?? context3.fs.resolve(foldersConfig.active_folder);
78430
+ const entries = await context3.fs.readDir(tasksDir);
78431
+ const fileName = entries.find((n2) => n2.startsWith(`${wbs}_`) && n2.endsWith(".md"));
78432
+ if (!fileName) {
78433
+ return false;
78434
+ }
78435
+ const svc = new TaskCheckService(context3.fs, await loadSectionMatrix(context3.cwd));
78436
+ const result = await svc.check(`${tasksDir}/${fileName}`, wbs, { strict: false });
78437
+ return result.pass;
78438
+ }
78283
78439
  async function loadSectionMatrix(projectRoot) {
78284
78440
  const localPath = join18(projectRoot, ".spur", "tasks", "section-matrix.yaml");
78285
78441
  if (existsSync5(localPath)) {
@@ -78363,6 +78519,7 @@ init_src3();
78363
78519
  init_loader();
78364
78520
  init_dist7();
78365
78521
  init_dist5();
78522
+ import { spawn as spawn2 } from "child_process";
78366
78523
  import { resolve as resolve5 } from "path";
78367
78524
  function parseVars(raw2) {
78368
78525
  if (raw2 === undefined) {
@@ -78424,7 +78581,7 @@ ${result.errors.map((m) => ` - ${m}`).join(`
78424
78581
  const spurParts = resolveSpurBin().split(" ");
78425
78582
  const spurBin = spurParts[0] ?? process.execPath;
78426
78583
  const spurArgs = spurParts.slice(1);
78427
- const cmd = [spurBin, ...spurArgs, "workflow", "run", file2, "--run-id", runId];
78584
+ const cmd = [...spurArgs, "workflow", "run", file2, "--run-id", runId];
78428
78585
  if (options.vars) {
78429
78586
  cmd.push("--vars", options.vars);
78430
78587
  }
@@ -78432,11 +78589,12 @@ ${result.errors.map((m) => ` - ${m}`).join(`
78432
78589
  cmd.push("--dry-run");
78433
78590
  }
78434
78591
  try {
78435
- Bun.spawn({
78436
- cmd,
78437
- stdio: ["ignore", "ignore", "ignore"],
78438
- env: process.env
78439
- }).unref();
78592
+ const child = spawn2(spurBin, cmd, {
78593
+ stdio: "ignore",
78594
+ detached: true,
78595
+ env: { ...process.env, SPUR_ASYNC_WORKER: "1" }
78596
+ });
78597
+ child.unref();
78440
78598
  } catch {
78441
78599
  const result2 = await makeSvc(options.json).run(file2, {
78442
78600
  runId,
@@ -78476,7 +78634,8 @@ Monitor with: spur workflow trace ${runId}`);
78476
78634
  const result = await makeSvc(options.json, bus).run(file2, {
78477
78635
  runId: options.runId || undefined,
78478
78636
  vars,
78479
- dryRun: options.dryRun || undefined
78637
+ dryRun: options.dryRun || undefined,
78638
+ recordSelfPid: process.env.SPUR_ASYNC_WORKER === "1"
78480
78639
  });
78481
78640
  context3.output.write(options.json ? toJson2(result) : `workflow ${result.status}: ${result.workflowName} -> ${result.finalState}`);
78482
78641
  context3.setExitCode(result.status === "done" ? 0 : 1);
@@ -78516,7 +78675,7 @@ Monitor with: spur workflow trace ${runId}`);
78516
78675
  context3.setExitCode(1);
78517
78676
  }
78518
78677
  });
78519
- workflow.command("clean").description("Finalize orphaned runs stuck in running/pending past a staleness threshold (mark as failed).").option("--older-than <minutes>", "Staleness threshold in minutes", "30").option("--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)").option("--dry-run", "List what would be cleaned without writing").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
78678
+ workflow.command("clean").description("Finalize orphaned runs stuck in running/pending past a staleness threshold (mark as failed). " + "To cancel a single live run by id, use `spur workflow cancel <run-id>` instead.").option("--older-than <minutes>", "Staleness threshold in minutes", "30").option("--force", "Clean ALL non-terminal runs regardless of age (overrides --older-than)").option("--dry-run", "List what would be cleaned without writing").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
78520
78679
  const force = options.force === true;
78521
78680
  const minutes = force ? 0 : Number.parseInt(options.olderThan ?? "30", 10);
78522
78681
  if (!Number.isFinite(minutes) || minutes < 0) {
@@ -78540,6 +78699,24 @@ Monitor with: spur workflow trace ${runId}`);
78540
78699
  }
78541
78700
  }
78542
78701
  });
78702
+ 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("--json", "Output machine-readable JSON where supported").action(async (runId, options) => {
78703
+ const result = await makeSvc(options.json).cancel(runId);
78704
+ if (options.json) {
78705
+ context3.output.write(toJson2(result));
78706
+ return;
78707
+ }
78708
+ if (result.status === "not_found") {
78709
+ context3.output.error(`Run ${runId} not found.`);
78710
+ context3.setExitCode(1);
78711
+ return;
78712
+ }
78713
+ if (result.finalized) {
78714
+ const killNote = result.killed ? " + signalled worker process group" : "";
78715
+ context3.output.write(`Cancelled run ${runId} (marked failed${killNote}).`);
78716
+ } else {
78717
+ context3.output.write(`Run ${runId} already terminal (${result.status}) \u2014 no change.`);
78718
+ }
78719
+ });
78543
78720
  workflow.command("list").description("List available workflow YAML files.").option("--json", "Output machine-readable JSON where supported").action(async (options) => {
78544
78721
  const paths = await resolveWorkflowPaths(context3.cwd);
78545
78722
  const result = await makeSvc().list(paths);
@@ -1,29 +0,0 @@
1
- ---
2
- schema_version: 1
3
- name: "{{ NAME }}"
4
- description: ""
5
- status: backlog
6
- type: brainstorm
7
- profile: standard
8
- feature_id: null
9
- parent_wbs: null
10
- priority: P2
11
- tags: []
12
- dependencies: []
13
- created_at: "{{ CREATED_AT }}"
14
- updated_at: "{{ CREATED_AT }}"
15
- ---
16
-
17
- ## {{ WBS }}. {{ NAME }}
18
-
19
- ### Background
20
-
21
- {{ BACKGROUND }}
22
-
23
- ### Q&A
24
-
25
- ### Plan
26
-
27
- ### References
28
-
29
- ### History
@@ -1,50 +0,0 @@
1
- ---
2
- schema_version: 1
3
- name: "{{ NAME }}"
4
- description: ""
5
- status: backlog
6
- type: task
7
- profile: standard
8
- feature_id: null
9
- parent_wbs: null
10
- priority: P2
11
- tags: []
12
- dependencies: []
13
- created_at: "{{ CREATED_AT }}"
14
- updated_at: "{{ CREATED_AT }}"
15
- ---
16
-
17
- ## {{ WBS }}. {{ NAME }}
18
-
19
- ### Background
20
-
21
- {{ BACKGROUND }}
22
-
23
- ### Acceptance Criteria
24
-
25
- ```gherkin
26
- Feature: {{ NAME }}
27
-
28
- Scenario: Basic acceptance
29
- Given a precondition
30
- When an action is taken
31
- Then an expected result occurs
32
- ```
33
-
34
- - [ ] Acceptance checklist item
35
-
36
- ### Design
37
-
38
- ### Plan
39
-
40
- - [ ] Implementation step
41
-
42
- ### Solution
43
-
44
- ### Testing
45
-
46
- ### Review
47
-
48
- ### References
49
-
50
- ### History
@@ -1,33 +0,0 @@
1
- ---
2
- schema_version: 1
3
- name: "{{ NAME }}"
4
- description: ""
5
- status: backlog
6
- type: issue
7
- profile: standard
8
- feature_id: null
9
- parent_wbs: null
10
- priority: P2
11
- tags: ["bug"]
12
- dependencies: []
13
- created_at: "{{ CREATED_AT }}"
14
- updated_at: "{{ CREATED_AT }}"
15
- ---
16
-
17
- ## {{ WBS }}. {{ NAME }}
18
-
19
- ### Background
20
-
21
- {{ BACKGROUND }}
22
-
23
- ### Root Cause
24
-
25
- ### Solution
26
-
27
- ### Testing
28
-
29
- ### Review
30
-
31
- ### References
32
-
33
- ### History
@@ -1,31 +0,0 @@
1
- ---
2
- schema_version: 1
3
- name: "{{ NAME }}"
4
- description: ""
5
- status: backlog
6
- type: meta
7
- profile: standard
8
- feature_id: null
9
- parent_wbs: null
10
- priority: P2
11
- tags: ["meta"]
12
- dependencies: []
13
- created_at: "{{ CREATED_AT }}"
14
- updated_at: "{{ CREATED_AT }}"
15
- ---
16
-
17
- ## {{ WBS }}. {{ NAME }}
18
-
19
- ### Background
20
-
21
- {{ BACKGROUND }}
22
-
23
- ### Plan
24
-
25
- - [ ] Implementation step
26
-
27
- ### Testing
28
-
29
- ### References
30
-
31
- ### History
@@ -1,48 +0,0 @@
1
- ---
2
- schema_version: 1
3
- name: "{{ NAME }}"
4
- description: ""
5
- status: backlog
6
- type: review
7
- template: review
8
- profile: standard
9
- feature_id: null
10
- parent_wbs: null
11
- priority: P2
12
- tags: ["review"]
13
- dependencies: []
14
- created_at: "{{ CREATED_AT }}"
15
- updated_at: "{{ CREATED_AT }}"
16
- ---
17
-
18
- ## {{ WBS }}. {{ NAME }}
19
-
20
- ### Background
21
-
22
- {{ BACKGROUND }}
23
-
24
- #### Review Findings
25
-
26
- The code-review findings this task must address — logged here as **input** (what was found
27
- in the reviewed PR/commit/diff). Fix in priority order (P1 → P2 → …); re-review after.
28
-
29
- | Severity | File | Finding | Recommendation |
30
- | -------- | ---- | ------- | -------------- |
31
- | P1 | | | |
32
- | P2 | | | |
33
-
34
- ### Plan
35
-
36
- - [ ] Fix P1 findings
37
- - [ ] Fix P2 findings
38
- - [ ] Fix all the remaining findings if any
39
- - [ ] Re-review the changed code
40
-
41
- ### Review
42
-
43
- Post-implementation reflection — filled **after** the first fix round: what went wrong, what
44
- remains to fix before closing, and any **back-issues** (new findings surfaced by the fix).
45
-
46
- ### References
47
-
48
- ### History