@gobing-ai/spur 0.3.31 → 0.3.32

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.
@@ -76,6 +76,8 @@ vars:
76
76
  # with the same package-manager surface (this monorepo is Bun-only):
77
77
  # `--vars '{"qualityGateCmd":"bun run lint && bun run test"}'`. Soft probe, hard recheck,
78
78
  # and the fixall slash input all use this same var so the command stays single-sourced.
79
+ # TRUSTED CONFIG ONLY — this string is executed via `sh -c` (see test/test-recheck). Never
80
+ # interpolate untrusted operator/LLM input into qualityGateCmd (task 0436 SECUA residual).
79
81
  qualityGateCmd: "bun run autofix && bun run spur-check"
80
82
  # Max /sp:dev-fixall attempts after a red quality-gate probe/recheck (bounded; no thrash).
81
83
  # Attempt counter: .spur/run/<wbs>-test-fix-attempt. Default 2 = two fixall hops before failed.
@@ -179,7 +181,9 @@ states:
179
181
  ATTEMPT_FILE=".spur/run/$wbs-test-fix-attempt" &&
180
182
  echo 0 > "$ATTEMPT_FILE" &&
181
183
  set +e &&
182
- ( $qualityGateCmd ); gate_rc=$?; set -e &&
184
+ # sh -c re-parses multi-command gates (&&, ;); do NOT use bare `$qualityGateCmd`
185
+ # word-split (breaks on &&). qualityGateCmd is pipeline-owned trusted config only.
186
+ ( sh -c "$qualityGateCmd" ); gate_rc=$?; set -e &&
183
187
  if [ "$gate_rc" -eq 0 ]; then
184
188
  printf 'PASS\n' > "$STATUS_FILE";
185
189
  else
@@ -219,7 +223,8 @@ states:
219
223
  mkdir -p .spur/run &&
220
224
  STATUS_FILE=".spur/run/$wbs-test-gate.status" &&
221
225
  set +e &&
222
- ( $qualityGateCmd ); gate_rc=$?; set -e &&
226
+ # See test probe: sh -c for multi-command; qualityGateCmd is trusted config only.
227
+ ( sh -c "$qualityGateCmd" ); gate_rc=$?; set -e &&
223
228
  if [ "$gate_rc" -eq 0 ]; then
224
229
  printf 'PASS\n' > "$STATUS_FILE";
225
230
  else
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.31",
3
+ "version": "0.3.32",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
package/spur.js CHANGED
@@ -63914,6 +63914,37 @@ class JobWorkerService {
63914
63914
  }
63915
63915
  }
63916
63916
 
63917
+ // ../../packages/app/src/services/pipeline-run-link.ts
63918
+ async function ensurePipelineRunLink(db2, wbs, options = {}) {
63919
+ const dao2 = new TaskRunLinkDao(db2);
63920
+ const links = await dao2.listByWbs(wbs, 20);
63921
+ const existing = links.find((l) => l.kind === "pipeline");
63922
+ if (existing) {
63923
+ return {
63924
+ created: false,
63925
+ id: existing.id,
63926
+ wbs: existing.wbs,
63927
+ runId: existing.run_id,
63928
+ kind: "pipeline"
63929
+ };
63930
+ }
63931
+ const id = createId("trl");
63932
+ const runId = options.runId ?? `record:${wbs}:${Date.now()}`;
63933
+ await dao2.insert({
63934
+ id,
63935
+ wbs,
63936
+ run_id: runId,
63937
+ kind: "pipeline",
63938
+ created_at: new Date().toISOString()
63939
+ });
63940
+ return { created: true, id, wbs, runId, kind: "pipeline" };
63941
+ }
63942
+ var TASK_FORWARD_CHAIN;
63943
+ var init_pipeline_run_link = __esm(() => {
63944
+ init_src2();
63945
+ TASK_FORWARD_CHAIN = ["backlog", "todo", "wip", "testing", "done"];
63946
+ });
63947
+
63917
63948
  // ../../packages/app/src/services/planning-events.ts
63918
63949
  class BusPlanningEventEmitter {
63919
63950
  bus;
@@ -67372,11 +67403,51 @@ class TaskService {
67372
67403
  result.solutionBackfilled = true;
67373
67404
  }
67374
67405
  if (opts.transition !== undefined) {
67375
- const transitionResult = await this.writeService.transition(ref, opts.transition);
67376
- result.transitionedTo = transitionResult.toStatus;
67406
+ const target = opts.transition;
67407
+ const current = (doc2.frontmatterData?.status ?? "").toLowerCase();
67408
+ if (current === target) {
67409
+ result.transitionedTo = current;
67410
+ } else if (target === "done") {
67411
+ if (verdict.verdict !== "PASS") {
67412
+ throw new GuardDeniedError(`Cannot transition task ${wbs} to done: verify verdict is ${verdict.verdict}, not PASS. ` + `Re-run the pipeline to a PASS verdict, or use ` + `\`spur task update ${wbs} done --force-done --reason "<why>"\` to override.`);
67413
+ }
67414
+ const startIdx = TASK_FORWARD_CHAIN.indexOf(current);
67415
+ const targetIdx = TASK_FORWARD_CHAIN.indexOf(target);
67416
+ if (startIdx !== -1 && targetIdx !== -1 && targetIdx > startIdx) {
67417
+ let reached = current;
67418
+ for (let i2 = startIdx + 1;i2 <= targetIdx; i2 += 1) {
67419
+ const hop = TASK_FORWARD_CHAIN[i2];
67420
+ if (hop === "done") {
67421
+ await this.ensurePipelineRunLink(wbs);
67422
+ }
67423
+ try {
67424
+ const hopResult = await this.writeService.transition(ref, hop);
67425
+ reached = hopResult.toStatus ?? hop;
67426
+ result.transitionedTo = reached;
67427
+ } catch (err) {
67428
+ const msg = err instanceof Error ? err.message : String(err);
67429
+ throw new GuardDeniedError(`Record auto-walk for task ${wbs} stopped at '${reached}' ` + `(failed hop \u2192 '${hop}'): ${msg}. ` + `Re-run \`spur task record ${wbs} --transition done\` once the guard is green.`);
67430
+ }
67431
+ }
67432
+ } else {
67433
+ await this.ensurePipelineRunLink(wbs);
67434
+ const transitionResult = await this.writeService.transition(ref, target);
67435
+ result.transitionedTo = transitionResult.toStatus;
67436
+ }
67437
+ } else {
67438
+ const transitionResult = await this.writeService.transition(ref, target);
67439
+ result.transitionedTo = transitionResult.toStatus;
67440
+ }
67377
67441
  }
67378
67442
  return result;
67379
67443
  }
67444
+ async ensurePipelineRunLink(wbs) {
67445
+ const getDb = this.ctx.getDb;
67446
+ if (getDb === undefined)
67447
+ return;
67448
+ const db2 = await getDb();
67449
+ await ensurePipelineRunLink(db2, wbs);
67450
+ }
67380
67451
  async batchCreate(jsonPath) {
67381
67452
  const raw = await this.ctx.fs.readFile(jsonPath);
67382
67453
  let parsed;
@@ -67714,6 +67785,8 @@ ${block}` : block);
67714
67785
  var DependencyMutationError, SectionMutationError, DuplicateFollowUpError, WbsCollisionError, ROSTER_START = "<!-- AUTO-GENERATED by spur task refresh-roster -->", ROSTER_END = "<!-- END AUTO-GENERATED -->", ROSTER_REGION_RE, TASK_ACTION_COMMANDS, DEFAULT_CREATION_SECTIONS;
67715
67786
  var init_task_service = __esm(() => {
67716
67787
  init_src2();
67788
+ init_errors5();
67789
+ init_pipeline_run_link();
67717
67790
  init_task_locator();
67718
67791
  init_task_record();
67719
67792
  DependencyMutationError = class DependencyMutationError extends Error {
@@ -70746,6 +70819,7 @@ __export(exports_src2, {
70746
70819
  extractSystemEventActor: () => extractSystemEventActor,
70747
70820
  extractReviewSectionBody: () => extractReviewSectionBody,
70748
70821
  evaluateDoneTransition: () => evaluateDoneTransition,
70822
+ ensurePipelineRunLink: () => ensurePipelineRunLink,
70749
70823
  encodeRunListCursor: () => encodeRunListCursor,
70750
70824
  deriveVerdict: () => deriveVerdict,
70751
70825
  decodeRunListCursor: () => decodeRunListCursor,
@@ -70774,6 +70848,7 @@ __export(exports_src2, {
70774
70848
  TOKEN_LEDGER_MAX_LIMIT: () => TOKEN_LEDGER_MAX_LIMIT,
70775
70849
  TOKEN_LEDGER_DEFAULT_LIMIT: () => TOKEN_LEDGER_DEFAULT_LIMIT,
70776
70850
  TASK_LIFECYCLE_PROFILE: () => TASK_LIFECYCLE_PROFILE,
70851
+ TASK_FORWARD_CHAIN: () => TASK_FORWARD_CHAIN,
70777
70852
  TASK_ACTION_COMMANDS: () => TASK_ACTION_COMMANDS,
70778
70853
  SystemEventEmitter: () => SystemEventEmitter,
70779
70854
  SupervisorService: () => SupervisorService,
@@ -70842,6 +70917,7 @@ var init_src3 = __esm(() => {
70842
70917
  init_feature_service();
70843
70918
  init_finding_codes2();
70844
70919
  init_history_service();
70920
+ init_pipeline_run_link();
70845
70921
  init_planning_events();
70846
70922
  init_planning_write_service();
70847
70923
  init_process_inspector();
@@ -78955,7 +79031,7 @@ import { join as join19, resolve as resolve6 } from "path";
78955
79031
  var CLI_CONFIG = {
78956
79032
  binaryName: "spur",
78957
79033
  binaryLabel: "spur",
78958
- binaryVersion: "0.3.31",
79034
+ binaryVersion: "0.3.32",
78959
79035
  configDir: ".spur",
78960
79036
  configFile: ".spur/config.yaml",
78961
79037
  databaseFile: ".spur/spur.db"
@@ -90654,37 +90730,22 @@ ${result.wbs} (${result.status}): ${result.pass ? "PASS" : "FAIL"}`);
90654
90730
  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("--source <source>", "Link source identifier (e.g. next-auto)", "chain").option("--run-id <id>", "Explicit run_id (auto-generated when omitted)").option("--json", "Output machine-readable JSON").action(async (wbs, options) => {
90655
90731
  try {
90656
90732
  const db2 = await context4.getDb();
90657
- const dao2 = new TaskRunLinkDao(db2);
90658
- const existing = await dao2.listByWbs(wbs, 20);
90659
- if (existing.some((row) => row.kind === "pipeline")) {
90660
- const entry = existing.find((row) => row.kind === "pipeline");
90661
- if (!entry) {
90662
- context4.output.error(`Pipeline link lookup inconsistent for ${wbs}`);
90663
- context4.setExitCode(1);
90664
- return;
90665
- }
90666
- const result2 = {
90667
- id: entry.id,
90668
- wbs: entry.wbs,
90669
- runId: entry.run_id,
90670
- kind: entry.kind,
90671
- existed: true
90672
- };
90673
- if (options.json) {
90674
- context4.output.write(toJson2(result2));
90675
- } else {
90676
- context4.output.write(`Pipeline run-link already exists for ${wbs} (${entry.id}). Skipped.`);
90677
- }
90678
- return;
90679
- }
90680
- const id = createId("trl");
90681
- const runId = options.runId ?? `chain:${options.source}:${wbs}:${Date.now()}`;
90682
- await dao2.insert({ id, wbs, run_id: runId, kind: "pipeline", created_at: new Date().toISOString() });
90683
- const result = { id, wbs, runId, kind: "pipeline" };
90733
+ const ensured = await ensurePipelineRunLink(db2, wbs, {
90734
+ runId: options.runId ?? `chain:${options.source}:${wbs}:${Date.now()}`
90735
+ });
90736
+ const result = {
90737
+ id: ensured.id,
90738
+ wbs: ensured.wbs,
90739
+ runId: ensured.runId,
90740
+ kind: ensured.kind,
90741
+ existed: !ensured.created
90742
+ };
90684
90743
  if (options.json) {
90685
90744
  context4.output.write(toJson2(result));
90745
+ } else if (ensured.created) {
90746
+ context4.output.write(`Recorded pipeline run-link ${ensured.id} for task ${wbs} (source: ${options.source})`);
90686
90747
  } else {
90687
- context4.output.write(`Recorded pipeline run-link ${id} for task ${wbs} (source: ${options.source})`);
90748
+ context4.output.write(`Pipeline run-link already exists for ${wbs} (${ensured.id}). Skipped.`);
90688
90749
  }
90689
90750
  } catch (err) {
90690
90751
  context4.output.error(String(err));
@@ -90731,6 +90792,7 @@ async function makeService2(context4, folderOverride, noLifecycle = false) {
90731
90792
  fs: context4.fs,
90732
90793
  tasksDir,
90733
90794
  writeService,
90795
+ getDb: () => context4.getDb(),
90734
90796
  sectionMatrix: await loadSectionMatrix(context4.cwd),
90735
90797
  resolveTemplate: (variant) => loadTemplateContent(context4.cwd, variant),
90736
90798
  resolveTemplateBodies: (variant) => loadTemplateBodies(context4.cwd, variant),