@gobing-ai/spur 0.4.5 → 0.4.6

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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +3 -4
  3. package/config/config.global.yaml +3 -4
  4. package/config/rules/quality/coverage-gate.yaml +6 -4
  5. package/config/transition-shims.json +0 -14
  6. package/package.json +1 -1
  7. package/plugins/sp/lib/inline-run.generated.mjs +1 -1
  8. package/plugins/sp/plugin.json +1 -1
  9. package/plugins/sp/scripts/commit-guard.mjs +49 -39
  10. package/plugins/sp/scripts/commit-guard.ts +60 -61
  11. package/plugins/sp/scripts/persist-out-check.mjs +28 -24
  12. package/plugins/sp/scripts/persist-out-check.ts +42 -33
  13. package/plugins/sp/skills/code-verification/references/verdict-schema.md +5 -3
  14. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +4 -2
  15. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +3 -1
  16. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -2
  17. package/spur.js +155 -48
  18. package/web/_astro/{BoardApp.hF0RAIrg.js → BoardApp.C5jnkkKP.js} +124 -124
  19. package/web/_astro/{BoardApp.Niubpe6z.js → BoardApp.CaNvQBD_.js} +1 -1
  20. package/web/_astro/{TaskDetail.DJpapt38.js → TaskDetail.DGKndhI8.js} +1 -1
  21. package/web/_astro/{arc.BgKcK0i8.js → arc.DHrwdg3G.js} +1 -1
  22. package/web/_astro/{architectureDiagram-3BPJPVTR.ChP-Ig0V.js → architectureDiagram-3BPJPVTR.pvKpK1s5.js} +1 -1
  23. package/web/_astro/{blockDiagram-GPEHLZMM.BVaCBoeN.js → blockDiagram-GPEHLZMM.BQmRcZsS.js} +1 -1
  24. package/web/_astro/{c4Diagram-AAUBKEIU.CHSDpEO5.js → c4Diagram-AAUBKEIU.CCLjHeH1.js} +1 -1
  25. package/web/_astro/channel.C7wG9RxA.js +1 -0
  26. package/web/_astro/{chunk-2J33WTMH.MYRcYVRa.js → chunk-2J33WTMH.DL6Yuoma.js} +1 -1
  27. package/web/_astro/{chunk-4BX2VUAB.S5j22Zqa.js → chunk-4BX2VUAB.CzlyEI7l.js} +1 -1
  28. package/web/_astro/{chunk-55IACEB6.gciRPvmZ.js → chunk-55IACEB6.B60EWxp1.js} +1 -1
  29. package/web/_astro/{chunk-727SXJPM.CdqrSQkX.js → chunk-727SXJPM.c7v_fRLo.js} +1 -1
  30. package/web/_astro/{chunk-AQP2D5EJ.CyokdHT3.js → chunk-AQP2D5EJ.8BxLWdDc.js} +1 -1
  31. package/web/_astro/{chunk-FMBD7UC4.Bg9KJhjW.js → chunk-FMBD7UC4.DmKuxGFT.js} +1 -1
  32. package/web/_astro/{chunk-ND2GUHAM.1gxxI-vD.js → chunk-ND2GUHAM.1Zwd4mqV.js} +1 -1
  33. package/web/_astro/{chunk-QZHKN3VN.DDEYtCKU.js → chunk-QZHKN3VN.BvYLu-A1.js} +1 -1
  34. package/web/_astro/{classDiagram-4FO5ZUOK.Ch3zi37T.js → classDiagram-4FO5ZUOK.CEqRt0WU.js} +1 -1
  35. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Ch3zi37T.js → classDiagram-v2-Q7XG4LA2.CEqRt0WU.js} +1 -1
  36. package/web/_astro/{cose-bilkent-S5V4N54A.D2q5AINI.js → cose-bilkent-S5V4N54A.C7JiUdj2.js} +1 -1
  37. package/web/_astro/{cynefin-OW5HDTMX.BA9kC2YT.js → cynefin-OW5HDTMX.B6mZ-1be.js} +1 -1
  38. package/web/_astro/{dagre-BM42HDAG.CHO08nRp.js → dagre-BM42HDAG.BuryfDy6.js} +1 -1
  39. package/web/_astro/{diagram-2AECGRRQ.BfFQt0LW.js → diagram-2AECGRRQ.dao39YQM.js} +1 -1
  40. package/web/_astro/{diagram-5GNKFQAL.CHMwMTe4.js → diagram-5GNKFQAL.lnGZe4At.js} +1 -1
  41. package/web/_astro/{diagram-KO2AKTUF.BJ-7gK-w.js → diagram-KO2AKTUF.uWfhzEfT.js} +1 -1
  42. package/web/_astro/{diagram-LMA3HP47.C5xqcc0t.js → diagram-LMA3HP47.ppq8-198.js} +1 -1
  43. package/web/_astro/{diagram-OG6HWLK6.C2pQjfrC.js → diagram-OG6HWLK6.CKSCqDt5.js} +1 -1
  44. package/web/_astro/{erDiagram-TEJ5UH35._FoNyxgC.js → erDiagram-TEJ5UH35.sFSf2yBN.js} +1 -1
  45. package/web/_astro/{flowDiagram-I6XJVG4X.B_ENkT2I.js → flowDiagram-I6XJVG4X.BJQjKLbz.js} +1 -1
  46. package/web/_astro/{ganttDiagram-6RSMTGT7.B3q8h77C.js → ganttDiagram-6RSMTGT7.DAHKNj2N.js} +1 -1
  47. package/web/_astro/{gitGraphDiagram-PVQCEYII.QWZFDboZ.js → gitGraphDiagram-PVQCEYII.BlmzcW_O.js} +1 -1
  48. package/web/_astro/index.BCY1JJHd.css +1 -0
  49. package/web/_astro/{infoDiagram-5YYISTIA.Cs6eEc92.js → infoDiagram-5YYISTIA.DGhjaqsi.js} +1 -1
  50. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BU9DJMHA.js → ishikawaDiagram-YF4QCWOH.gKIpgt6F.js} +1 -1
  51. package/web/_astro/{journeyDiagram-JHISSGLW.D35k6Jpe.js → journeyDiagram-JHISSGLW.DG3tmP0R.js} +1 -1
  52. package/web/_astro/{kanban-definition-UN3LZRKU.DBqcAT4m.js → kanban-definition-UN3LZRKU.CgHDSxkR.js} +1 -1
  53. package/web/_astro/{linear.D-LSNK8W.js → linear.BuGEpcuM.js} +1 -1
  54. package/web/_astro/{mermaid.core.CSruWVOa.js → mermaid.core.DBVqkWBp.js} +4 -4
  55. package/web/_astro/{mindmap-definition-RKZ34NQL.BiFaUsfj.js → mindmap-definition-RKZ34NQL.D-kOcji9.js} +1 -1
  56. package/web/_astro/{pieDiagram-4H26LBE5.D1KTKqb9.js → pieDiagram-4H26LBE5.BvGPwtZt.js} +1 -1
  57. package/web/_astro/{quadrantDiagram-W4KKPZXB.DBHflsMG.js → quadrantDiagram-W4KKPZXB.BKb_9FV6.js} +1 -1
  58. package/web/_astro/{requirementDiagram-4Y6WPE33.hwX9CNdk.js → requirementDiagram-4Y6WPE33.Cx0fvD8v.js} +1 -1
  59. package/web/_astro/{sankeyDiagram-5OEKKPKP.DkI8mJiE.js → sankeyDiagram-5OEKKPKP.fatULi0W.js} +1 -1
  60. package/web/_astro/{sequenceDiagram-3UESZ5HK.DMz7jBDx.js → sequenceDiagram-3UESZ5HK.ChHyKqco.js} +1 -1
  61. package/web/_astro/{stateDiagram-AJRCARHV.B6Ef2xJ1.js → stateDiagram-AJRCARHV.BdUpKWyj.js} +1 -1
  62. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CgoaufB9.js → stateDiagram-v2-BHNVJYJU.fmJxqIm5.js} +1 -1
  63. package/web/_astro/{timeline-definition-PNZ67QCA.B3c8h1t-.js → timeline-definition-PNZ67QCA.BayDn5-V.js} +1 -1
  64. package/web/_astro/{vennDiagram-CIIHVFJN.BdHPAxAb.js → vennDiagram-CIIHVFJN.Bm2Aggwa.js} +1 -1
  65. package/web/_astro/{wardleyDiagram-YWT4CUSO.BrJXPPAj.js → wardleyDiagram-YWT4CUSO.DI75O-uG.js} +1 -1
  66. package/web/_astro/{xychartDiagram-2RQKCTM6.CMH5yFsV.js → xychartDiagram-2RQKCTM6.DL90E7Q7.js} +1 -1
  67. package/web/index.html +2 -2
  68. package/web/_astro/channel.DkpvN2tD.js +0 -1
  69. package/web/_astro/index.BFyW13JJ.css +0 -1
@@ -219,7 +219,9 @@ clean at authoring time.
219
219
  ### Which tags exempt a row from executable evidence
220
220
 
221
221
  A `MET` row is silently demoted to `PARTIAL` unless it carries `test` or `command` evidence — the
222
- executable-evidence rule. Five tags opt a row out, because not every scenario is behavioral:
222
+ executable-evidence rule. Five tags opt a row out, because not every scenario is behavioral. The
223
+ tag counts only when the task author writes it on the AC item in the task file; `spur task verdict`
224
+ ignores (and flags) a tag that appears only in the verify answer:
223
225
 
224
226
  `[doc-only]` · `[docs-only]` · `[non-behavior]` · `[advisory]` · `[non-core]`
225
227
 
@@ -139,8 +139,9 @@ silently incomplete (H6 shipped at 23/48 that way, with one verdict carrying an
139
139
  parses zero AC rows while lint still reports PASS.
140
140
  3. A behavioral AC marked `MET` with a non-executable evidence type (`static-ref`,
141
141
  `manual-review`, `llm-judge`) is **downgraded to PARTIAL** by `task verdict`, making the whole
142
- verdict PARTIAL. Use `test`/`command` (grep-based verification counts as `command`), or tag the
143
- AC id `[non-behavior]`/`[advisory]` when executable evidence genuinely doesn't apply.
142
+ verdict PARTIAL. Use `test`/`command` (grep-based verification counts as `command`). Only the
143
+ task author can exempt an AC, by tagging the AC item in the task file `[non-behavior]`/`[advisory]`;
144
+ the same tag added in the verify answer alone is ignored.
144
145
 
145
146
  **Invariant:** a force-done task has a non-empty `done_reason` naming the timeout, a verdict
146
147
  artifact whose AC rows cover every declared scenario, and a green lint/test run recorded in
package/spur.js CHANGED
@@ -50681,7 +50681,7 @@ class DatabaseInspector {
50681
50681
  name: row.name,
50682
50682
  type: row.type ?? "",
50683
50683
  notNull: row.notnull === 1,
50684
- pk: row.pk === 1
50684
+ pk: row.pk > 0
50685
50685
  }));
50686
50686
  }
50687
50687
  resolveColumnName(table, columns, column) {
@@ -86156,6 +86156,7 @@ class AgentCoordinationService {
86156
86156
  id: input.id,
86157
86157
  name: input.name ?? input.id,
86158
86158
  type: input.type,
86159
+ ...input.executor !== undefined ? { executor: input.executor } : {},
86159
86160
  workspace: input.workspace ?? this.ctx.cwd,
86160
86161
  purpose: input.purpose && input.purpose.length > 0 ? input.purpose : `${input.type} agent`,
86161
86162
  tags: input.tags ?? [],
@@ -90130,6 +90131,30 @@ async function readVerdictArtifact(fs, runDir, wbs) {
90130
90131
  const { missing: _scratchMissing, ...result } = scratch;
90131
90132
  return result;
90132
90133
  }
90134
+ function citedTestPaths(evidence) {
90135
+ const paths = new Set;
90136
+ for (const match of evidence.matchAll(TEST_PATH_RE)) {
90137
+ const path = (match[1] ?? "").replace(/^\.\//, "");
90138
+ if (path !== "" && !path.startsWith("/"))
90139
+ paths.add(path);
90140
+ }
90141
+ return [...paths];
90142
+ }
90143
+ async function findUnresolvedTestEvidence(fs, projectRoot, artifact) {
90144
+ if (artifact === undefined)
90145
+ return [];
90146
+ const rows = [...artifact.requirements ?? [], ...artifact.acceptanceCriteria ?? []];
90147
+ const unresolved = [];
90148
+ for (const row of rows) {
90149
+ if (String(row.status).toUpperCase() !== "MET" || typeof row.evidence !== "string")
90150
+ continue;
90151
+ for (const path of citedTestPaths(row.evidence)) {
90152
+ if (!await fs.exists(join31(projectRoot, path)))
90153
+ unresolved.push(`${row.id ?? "?"}: ${path}`);
90154
+ }
90155
+ }
90156
+ return unresolved;
90157
+ }
90133
90158
  function computeAggregate(artifact) {
90134
90159
  const reqs = artifact.requirements ?? [];
90135
90160
  const acs = artifact.acceptanceCriteria ?? [];
@@ -90211,6 +90236,21 @@ function evaluateDoneTransition(input) {
90211
90236
  const acs = artifact.acceptanceCriteria ?? [];
90212
90237
  const internallyConsistentPass = artifact.verdict !== "PASS" || reqs.length > 0 || acs.length > 0;
90213
90238
  const effective = internallyConsistentPass ? harshnessMax(artifact.verdict, computed) : "UNKNOWN";
90239
+ const unresolved = input.unresolvedTestEvidence ?? [];
90240
+ if (effective === "PASS" && unresolved.length > 0) {
90241
+ return {
90242
+ kind: "deny",
90243
+ verdict: "PARTIAL",
90244
+ message: [
90245
+ `Cannot transition task ${wbs} to done: the PASS verdict cites test files that do not exist.`,
90246
+ ` task: ${taskFilePath}`,
90247
+ ` verdict: ${verdictPath}`,
90248
+ ...unresolved.map((u) => ` missing: ${u}`),
90249
+ ` remediation: re-run \`/sp:dev-verify ${wbs}\` with evidence naming real tests, ` + `or override with \`spur task update ${wbs} done --force-done --reason "<why>"\`.`
90250
+ ].join(`
90251
+ `)
90252
+ };
90253
+ }
90214
90254
  if (effective === "PASS") {
90215
90255
  return { kind: "allow", reason: "pass" };
90216
90256
  }
@@ -90232,8 +90272,10 @@ function harshnessMax(a, b) {
90232
90272
  const rank = { PASS: 0, UNKNOWN: 1, PARTIAL: 2, FAIL: 3 };
90233
90273
  return rank[a] >= rank[b] ? a : b;
90234
90274
  }
90275
+ var TEST_PATH_RE;
90235
90276
  var init_done_transition_guard = __esm(() => {
90236
90277
  init_verify_verdict();
90278
+ TEST_PATH_RE = /(?:^|[\s`'"([,])((?:[\w@.-]+\/)+[\w@.-]+\.(?:test|spec)\.[cm]?[jt]sx?)\b/g;
90237
90279
  });
90238
90280
 
90239
90281
  // ../../packages/app/src/workflow/workflow-resolver.ts
@@ -91839,7 +91881,9 @@ function buildAcIdentityIndex(taskContent, featureContent) {
91839
91881
  if (!label)
91840
91882
  continue;
91841
91883
  declareIdentity(label);
91842
- const head = label.split(/:(?:\s|$)/)[0]?.trim() ?? "";
91884
+ const balancedHead = /^(.+?\([^()]*\))\s*:(?:\s|$)/.exec(label)?.[1];
91885
+ const fallback = /\([^():]*:[^()]*\)/.test(label) ? null : label.split(/:(?:\s|$)/)[0];
91886
+ const head = (balancedHead ?? fallback ?? "").trim();
91843
91887
  if (head && head !== label)
91844
91888
  declareIdentity(head);
91845
91889
  const leading = label.split(/\s+/)[0] ?? "";
@@ -92177,6 +92221,21 @@ async function readFeatureAcBody(fs, featuresDir, featureId) {
92177
92221
  return null;
92178
92222
  }
92179
92223
  }
92224
+ async function readTaskAcceptanceCriteria(wbs, deps) {
92225
+ try {
92226
+ const hit = await new TaskLocator({
92227
+ fs: deps.fs,
92228
+ tasksDir: deps.tasksDir,
92229
+ ...deps.foldersConfig !== undefined ? { foldersConfig: deps.foldersConfig } : {}
92230
+ }).findByWbs(wbs);
92231
+ if (hit === null)
92232
+ return "";
92233
+ const doc = MarkdownDocument.parse(await deps.fs.readFile(hit.filePath), "task");
92234
+ return stripAcFence(doc.getSection("Acceptance Criteria") ?? "");
92235
+ } catch {
92236
+ return "";
92237
+ }
92238
+ }
92180
92239
  async function lintVerifyAnswerForTask(wbs, answerText, deps) {
92181
92240
  let taskContent;
92182
92241
  let featureId = "";
@@ -104398,7 +104457,11 @@ class AgentService {
104398
104457
  const phaseSuffix = phase !== undefined ? ` for phase '${phase}'` : "";
104399
104458
  if (executor !== undefined) {
104400
104459
  if (source === "default") {
104401
- warnAgentDefaultExecutorOnce(selector, this.ctx.output);
104460
+ return {
104461
+ ok: false,
104462
+ exitCode: 2,
104463
+ message: agentDefaultExecutorMessage(selector, this.ctx.roles)
104464
+ };
104402
104465
  }
104403
104466
  if (executorDisabled(executor)) {
104404
104467
  return {
@@ -104447,13 +104510,11 @@ class AgentService {
104447
104510
  }
104448
104511
  if (source === "default") {
104449
104512
  const roleNames = this.ctx.roles !== undefined ? [...this.ctx.roles.keys()].join(", ") : "";
104450
- const names = this.ctx.agentConfig?.executors?.map((e) => e.name) ?? [];
104451
- const roleList = roleNames !== "" ? `role (${roleNames})` : "a role";
104452
- const executorList = names.length > 0 ? `configured executor (${names.join(", ")})` : "a configured executor";
104513
+ const roleList = roleNames !== "" ? `a role (${roleNames})` : "a role";
104453
104514
  return {
104454
104515
  ok: false,
104455
104516
  exitCode: 2,
104456
- message: `Unknown agent.default value: '${selector}'. Accepted: ${roleList} or ${executorList}.`
104517
+ message: `Unknown agent.default value: '${selector}'. Accepted: ${roleList}, e.g. agent.default: coder.`
104457
104518
  };
104458
104519
  }
104459
104520
  const canonical = resolveAgentName(selector);
@@ -104765,11 +104826,9 @@ function warnBareBinaryOnce(selector, output) {
104765
104826
  warnedBareBinary.add(selector);
104766
104827
  output.error(`Warning: --agent "${selector}" is a bare coding-agent binary name, not a role or a configured executor. It keeps working during the transition; prefer a role (scribe, coder, reviewer, planner) or an executor name (config/transition-shims.json: agent-bare-binary-name).`);
104767
104828
  }
104768
- function warnAgentDefaultExecutorOnce(selector, output) {
104769
- if (warnedAgentDefaultExecutor.has(selector))
104770
- return;
104771
- warnedAgentDefaultExecutor.add(selector);
104772
- output.error(`Warning: agent.default "${selector}" is a configured executor name, not a role. It keeps working during the transition; prefer a role (scribe, coder, reviewer, planner) (config/transition-shims.json: agent-default-executor).`);
104829
+ function agentDefaultExecutorMessage(selector, roles) {
104830
+ const roleNames = roles !== undefined && roles.size > 0 ? [...roles.keys()].join(", ") : "scribe, coder, reviewer, planner";
104831
+ return `agent.default '${selector}' names a configured executor, but agent.default takes a role (${roleNames}). ` + `Fix: set agent.default to a role such as 'coder' in .spur/config.yaml, and pin '${selector}' explicitly ` + `with --agent ${selector} where you need that executor.`;
104773
104832
  }
104774
104833
  function buildDoctorRows(results, executors, roles) {
104775
104834
  const executorByName = new Map((executors ?? []).map((e) => [e.name, e]));
@@ -105021,7 +105080,7 @@ function buildRoutingAttribution(result) {
105021
105080
  ...result.model !== undefined ? { model: result.model } : {}
105022
105081
  };
105023
105082
  }
105024
- var PidObservingProcessExecutor, RolePropagatingProcessExecutor, DOCTOR_CACHE_REL = ".spur/run/agent-doctor.json", DOCTOR_CACHE_TTL_MS = 60000, USAGE_SNAPSHOT_STALE_MS, TRACE_SAFE_SLASH_COMMAND, TRACE_SAFE_SLASH_TOKEN, SENSITIVE_FLAG, SENSITIVE_INLINE_FLAG, warnedBareBinary, warnedAgentDefaultExecutor;
105083
+ var PidObservingProcessExecutor, RolePropagatingProcessExecutor, DOCTOR_CACHE_REL = ".spur/run/agent-doctor.json", DOCTOR_CACHE_TTL_MS = 60000, USAGE_SNAPSHOT_STALE_MS, TRACE_SAFE_SLASH_COMMAND, TRACE_SAFE_SLASH_TOKEN, SENSITIVE_FLAG, SENSITIVE_INLINE_FLAG, warnedBareBinary;
105025
105084
  var init_agent_service = __esm(() => {
105026
105085
  init_src();
105027
105086
  init_src2();
@@ -105078,7 +105137,6 @@ var init_agent_service = __esm(() => {
105078
105137
  SENSITIVE_FLAG = /^--?(?:api[-_]?key|authorization|credential|password|secret|token)$/i;
105079
105138
  SENSITIVE_INLINE_FLAG = /^(--?(?:api[-_]?key|authorization|credential|password|secret|token)=).+$/i;
105080
105139
  warnedBareBinary = new Set;
105081
- warnedAgentDefaultExecutor = new Set;
105082
105140
  });
105083
105141
 
105084
105142
  // ../../packages/app/src/services/agent-trace-service.ts
@@ -105794,8 +105852,20 @@ async function prepareBoardModules(input) {
105794
105852
  }
105795
105853
  }
105796
105854
  let realProjectRoot;
105797
- const projectRootRealPath = () => {
105798
- realProjectRoot ??= input.probe.realPath(input.projectRoot) ?? input.projectRoot;
105855
+ const verifiedRealPath = (index, id, path, label) => {
105856
+ let resolved;
105857
+ try {
105858
+ resolved = input.probe.realPath(path);
105859
+ } catch {
105860
+ resolved = undefined;
105861
+ }
105862
+ if (resolved === undefined) {
105863
+ throw new BoardModuleConfigError(index, id, `${label} "${path}" could not be resolved to a real path`);
105864
+ }
105865
+ return resolved;
105866
+ };
105867
+ const projectRootRealPath = (index, id) => {
105868
+ realProjectRoot ??= verifiedRealPath(index, id, input.projectRoot, "project root");
105799
105869
  return realProjectRoot;
105800
105870
  };
105801
105871
  for (const [index, declaration] of input.declarations.entries()) {
@@ -105808,22 +105878,35 @@ async function prepareBoardModules(input) {
105808
105878
  }
105809
105879
  const root = resolve17(input.projectRoot, declaration.directory);
105810
105880
  const entryPath = resolveContained(index, input.projectRoot, declaration.directory, declaration.entry, declaration.id);
105811
- const stylePaths = declaration.styles.map((style) => resolveContained(index, input.projectRoot, declaration.directory, style, declaration.id));
105881
+ const styles = declaration.styles.map((style) => ({
105882
+ declared: style,
105883
+ absolute: resolveContained(index, input.projectRoot, declaration.directory, style, declaration.id)
105884
+ }));
105885
+ const stylePaths = styles.map((style) => style.absolute);
105812
105886
  if (!await input.probe.isDirectory(root)) {
105813
105887
  throw new BoardModuleConfigError(index, declaration.id, `module directory "${declaration.directory}" was not found`);
105814
105888
  }
105815
105889
  if (!await input.probe.isFile(entryPath)) {
105816
105890
  throw new BoardModuleConfigError(index, declaration.id, `entry asset "${declaration.entry}" was not found in "${declaration.directory}"`);
105817
105891
  }
105818
- for (const [styleIndex, stylePath] of stylePaths.entries()) {
105819
- if (!await input.probe.isFile(stylePath)) {
105820
- throw new BoardModuleConfigError(index, declaration.id, `style asset "${declaration.styles[styleIndex]}" was not found`);
105892
+ for (const style of styles) {
105893
+ if (!await input.probe.isFile(style.absolute)) {
105894
+ throw new BoardModuleConfigError(index, declaration.id, `style asset "${style.declared}" was not found`);
105821
105895
  }
105822
105896
  }
105823
- const realRoot = input.probe.realPath(root);
105824
- const realEntry = input.probe.realPath(entryPath);
105825
- if (realRoot !== undefined && realEntry !== undefined && (!isInsideRoot(projectRootRealPath(), realRoot) || !isInsideRoot(realRoot, realEntry))) {
105826
- throw new BoardModuleConfigError(index, declaration.id, `"${declaration.directory}" resolves outside the project root or its entry resolves outside the module directory`);
105897
+ const realProject = projectRootRealPath(index, declaration.id);
105898
+ const realRoot = verifiedRealPath(index, declaration.id, root, "module directory");
105899
+ if (!isInsideRoot(realProject, realRoot)) {
105900
+ throw new BoardModuleConfigError(index, declaration.id, `"${declaration.directory}" resolves outside the project root`);
105901
+ }
105902
+ const requireInsideModule = (declaredPath, realPath) => {
105903
+ if (!isInsideRoot(realRoot, realPath)) {
105904
+ throw new BoardModuleConfigError(index, declaration.id, `"${declaredPath}" resolves outside the project root or its module directory`);
105905
+ }
105906
+ };
105907
+ requireInsideModule(declaration.entry, verifiedRealPath(index, declaration.id, entryPath, "entry asset"));
105908
+ for (const style of styles) {
105909
+ requireInsideModule(style.declared, verifiedRealPath(index, declaration.id, style.absolute, "style asset"));
105827
105910
  }
105828
105911
  descriptors.push({
105829
105912
  ...descriptorBase(declaration),
@@ -114496,7 +114579,8 @@ async function transitionTaskGuarded(deps, input) {
114496
114579
  reason: input.reason,
114497
114580
  artifact: loaded.artifact,
114498
114581
  verdictPath: loaded.path,
114499
- readError: loaded.readError
114582
+ readError: loaded.readError,
114583
+ unresolvedTestEvidence: input.forceDone === true ? [] : await findUnresolvedTestEvidence(deps.fs, dirname31(dirname31(deps.runDir)), loaded.artifact)
114500
114584
  });
114501
114585
  if (guardOutcome.kind === "noop") {
114502
114586
  return { kind: "noop", wbs, status: "done", message: guardOutcome.message };
@@ -115917,11 +116001,12 @@ var init_task_service = __esm(() => {
115917
116001
  });
115918
116002
 
115919
116003
  // ../../packages/app/src/services/task-verdict.ts
115920
- function deriveVerdict(answerText, taskCheckPassed) {
116004
+ function deriveVerdict(answerText, taskCheckPassed, taskAcceptanceCriteria = "") {
115921
116005
  const requirements = extractRequirements(answerText);
115922
116006
  const parsedAc = extractAcceptanceCriteria(answerText);
115923
- const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(parsedAc.rows);
115924
- const checks = extractChecks(answerText, taskCheckPassed, requirements, acceptanceCriteria, parsedAc.dropped);
116007
+ const exempt = taskAcExemptions(taskAcceptanceCriteria);
116008
+ const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(parsedAc.rows, exempt);
116009
+ const checks = extractChecks(answerText, taskCheckPassed, requirements, acceptanceCriteria, parsedAc.dropped, exempt);
115925
116010
  if (requirements.length === 0) {
115926
116011
  return {
115927
116012
  verdict: "UNKNOWN",
@@ -116094,21 +116179,31 @@ function normalizeEvidenceType2(raw) {
116094
116179
  }
116095
116180
  return EVIDENCE_TYPE_PRECEDENCE2.find((candidate) => tokens.includes(candidate)) ?? null;
116096
116181
  }
116097
- function applyAcceptanceCriteriaEvidenceRule(rows) {
116182
+ function taskAcExemptions(taskAcceptanceCriteria) {
116183
+ const exempt = new Set;
116184
+ for (const line of taskAcceptanceCriteria.split(`
116185
+ `)) {
116186
+ if (!EXEMPTION_TAG_RE.test(line))
116187
+ continue;
116188
+ const item = line.replace(/^\s*(?:[-*+]|\d+[.)])\s*(?:\[[ xX]\]\s*)?/, "");
116189
+ const key = normalizeTitle(item);
116190
+ if (key !== "")
116191
+ exempt.add(key);
116192
+ }
116193
+ return exempt;
116194
+ }
116195
+ function applyAcceptanceCriteriaEvidenceRule(rows, exempt) {
116098
116196
  return rows.map((row) => {
116099
- if (row.status === "MET" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command") {
116197
+ if (row.status === "MET" && requiresExecutableEvidence(row.id, exempt) && row.evidenceType !== "test" && row.evidenceType !== "command") {
116100
116198
  return { ...row, status: "PARTIAL" };
116101
116199
  }
116102
116200
  return row;
116103
116201
  });
116104
116202
  }
116105
- function requiresExecutableEvidence(id) {
116106
- const normalized = id.toLowerCase();
116107
- const nonCore = normalized.includes("[advisory]") || normalized.includes("[non-core]");
116108
- const nonBehavior = normalized.includes("[non-behavior]") || normalized.includes("[docs-only]") || normalized.includes("[doc-only]");
116109
- return !nonCore && !nonBehavior;
116203
+ function requiresExecutableEvidence(id, exempt) {
116204
+ return !exempt.has(normalizeTitle(id));
116110
116205
  }
116111
- function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria, droppedAcRows = []) {
116206
+ function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria, droppedAcRows = [], exempt = new Set) {
116112
116207
  const checks = [
116113
116208
  {
116114
116209
  name: "spur task check",
@@ -116161,8 +116256,17 @@ function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria,
116161
116256
  evidence: `${hollowMet.length} MET row(s) carry no recorded evidence and cannot support PASS: ` + `${hollowMet.join(", ")}. Re-run verify with populated evidence per row.`
116162
116257
  });
116163
116258
  }
116259
+ const answerOnlyTags = acceptanceCriteria.filter((row) => EXEMPTION_TAG_RE.test(row.id) && requiresExecutableEvidence(row.id, exempt)).map((row) => row.id);
116260
+ if (answerOnlyTags.length > 0) {
116261
+ checks.push({
116262
+ name: "ac-exemption-not-in-task",
116263
+ status: "warn",
116264
+ severity: "advisory",
116265
+ evidence: `${answerOnlyTags.length} AC row(s) carry an evidence-rule exemption tag the task file does not: ` + `${answerOnlyTags.join("; ")}. Exemptions come only from the task's Acceptance Criteria; ` + "these rows still need test or command evidence."
116266
+ });
116267
+ }
116164
116268
  if (acceptanceCriteria.length > 0) {
116165
- const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command");
116269
+ const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id, exempt) && row.evidenceType !== "test" && row.evidenceType !== "command");
116166
116270
  checks.push({
116167
116271
  name: downgraded.length > 0 ? "evidence-rule-failed" : "evidence-rule-pass",
116168
116272
  status: downgraded.length > 0 ? "fail" : "pass",
@@ -116193,10 +116297,12 @@ function aggregateBatchVerdicts(results) {
116193
116297
  const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
116194
116298
  return { verdict, rolledUp, notStarted, summary };
116195
116299
  }
116196
- var EVIDENCE_TYPE_PRECEDENCE2;
116300
+ var EVIDENCE_TYPE_PRECEDENCE2, EXEMPTION_TAG_RE;
116197
116301
  var init_task_verdict = __esm(() => {
116302
+ init_src2();
116198
116303
  init_verify_verdict();
116199
116304
  EVIDENCE_TYPE_PRECEDENCE2 = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
116305
+ EXEMPTION_TAG_RE = /\[(?:advisory|non-core|non-behavior|docs-only|doc-only)\]/i;
116200
116306
  });
116201
116307
 
116202
116308
  // ../../packages/app/src/services/token-ledger-service.ts
@@ -131412,7 +131518,8 @@ Recover with: ${err.recoveryCommand}` : err.message;
131412
131518
  return;
131413
131519
  }
131414
131520
  const taskCheckPassed = true;
131415
- const result = deriveVerdict(answerText, taskCheckPassed);
131521
+ const taskAc = await readTaskAcceptanceCriteria(wbs, corpusDeps);
131522
+ const result = deriveVerdict(answerText, taskCheckPassed, taskAc);
131416
131523
  const gap = await verdictScenarioKeyGap(wbs, [...result.requirements, ...result.acceptanceCriteria ?? []], corpusDeps);
131417
131524
  if (gap !== null) {
131418
131525
  writeJsonError(context.output, options, `Verdict rows key to no scenario of linked feature ${gap.featureId}: ${summarizeRowIds(gap.offendingIds)}. Key each row by ${VERDICT_SCENARIO_KEY_FORMS} (repair: /sp:dev-verify ${wbs})`);
@@ -132609,8 +132716,9 @@ ${prompt}`;
132609
132716
  };
132610
132717
  }
132611
132718
  function drainAgentSelector(spec, context) {
132612
- if (spec.executor === undefined)
132613
- return spec.type;
132719
+ if (spec.executor === undefined) {
132720
+ throw new Error(`Spec "${spec.id}" has no executor field \u2014 add \`executor: <name>\` (an agent.executors entry) to the spec under .spur/agents/ or re-materialize it, then drain again`);
132721
+ }
132614
132722
  try {
132615
132723
  resolveExecutor(spec.executor, context.agentConfig, { isCanonicalAgent: isAgentName });
132616
132724
  } catch (error) {
@@ -134313,7 +134421,7 @@ import { createRequire as createRequire3 } from "module";
134313
134421
  var CLI_CONFIG = {
134314
134422
  binaryName: "spur",
134315
134423
  binaryLabel: "spur",
134316
- binaryVersion: "0.4.5",
134424
+ binaryVersion: "0.4.6",
134317
134425
  configDir: ".spur",
134318
134426
  configFile: ".spur/config.yaml",
134319
134427
  databaseFile: ".spur/spur.db"
@@ -142419,14 +142527,13 @@ async function serveModuleAsset(c, modules, fs) {
142419
142527
  const target = resolve27(declared.root, relative);
142420
142528
  if (!isInsideRoot(declared.root, target))
142421
142529
  return refusal(403, "Forbidden path");
142530
+ if (!(await fs.stat(target))?.isFile())
142531
+ return refusal(404, "Not Found");
142422
142532
  const realRoot = resolvedOrUndefined(fs, declared.root);
142423
- const resolvedTarget = resolvedOrUndefined(fs, target);
142424
- if (realRoot !== undefined && resolvedTarget !== undefined && !isInsideRoot(realRoot, resolvedTarget)) {
142533
+ const realTarget = resolvedOrUndefined(fs, target);
142534
+ if (realRoot === undefined || realTarget === undefined || !isInsideRoot(realRoot, realTarget)) {
142425
142535
  return refusal(403, "Forbidden path");
142426
142536
  }
142427
- const realTarget = resolvedTarget ?? target;
142428
- if (!(await fs.stat(realTarget))?.isFile())
142429
- return refusal(404, "Not Found");
142430
142537
  const file = Bun.file(realTarget);
142431
142538
  const headers = new Headers({
142432
142539
  "content-type": file.type || "application/octet-stream",
@@ -142941,7 +143048,7 @@ await init_src3();
142941
143048
  import { homedir as homedir14 } from "os";
142942
143049
  import { basename as basename20, join as join70, relative as relative14, resolve as resolve28 } from "path";
142943
143050
  // ../server/package.json
142944
- var version3 = "0.4.5";
143051
+ var version3 = "0.4.6";
142945
143052
 
142946
143053
  // ../server/src/modules/health/index.ts
142947
143054
  var startedAt = Date.now();