@gobing-ai/spur 0.4.4 → 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 (82) 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/docs/sourced-api-claims.yaml +3 -2
  5. package/config/rules/quality/coverage-gate.yaml +6 -4
  6. package/config/rules/recommended-pre-check.yaml +11 -9
  7. package/config/transition-shims.json +0 -14
  8. package/config/workflows/task-pipeline.yaml +31 -71
  9. package/package.json +1 -1
  10. package/plugins/sp/agents/super-reviewer.md +11 -27
  11. package/plugins/sp/commands/dev-review.md +1 -1
  12. package/plugins/sp/lib/inline-run.generated.mjs +3 -3
  13. package/plugins/sp/lib/residual-scan.generated.d.mts +0 -13
  14. package/plugins/sp/lib/residual-scan.generated.mjs +3 -47
  15. package/plugins/sp/plugin.json +1 -1
  16. package/plugins/sp/scripts/commit-guard.mjs +49 -39
  17. package/plugins/sp/scripts/commit-guard.ts +60 -61
  18. package/plugins/sp/scripts/persist-out-check.mjs +28 -24
  19. package/plugins/sp/scripts/persist-out-check.ts +42 -33
  20. package/plugins/sp/scripts/residual-scan.mjs +5 -50
  21. package/plugins/sp/scripts/residual-scan.ts +4 -6
  22. package/plugins/sp/skills/code-verification/SKILL.md +1 -2
  23. package/plugins/sp/skills/code-verification/references/verdict-schema.md +16 -35
  24. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +4 -2
  25. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +3 -1
  26. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -2
  27. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +7 -11
  28. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +7 -6
  29. package/spur.js +422 -431
  30. package/web/_astro/{BoardApp.hF0RAIrg.js → BoardApp.C5jnkkKP.js} +124 -124
  31. package/web/_astro/{BoardApp.Niubpe6z.js → BoardApp.CaNvQBD_.js} +1 -1
  32. package/web/_astro/{TaskDetail.DJpapt38.js → TaskDetail.DGKndhI8.js} +1 -1
  33. package/web/_astro/{arc.BgKcK0i8.js → arc.DHrwdg3G.js} +1 -1
  34. package/web/_astro/{architectureDiagram-3BPJPVTR.ChP-Ig0V.js → architectureDiagram-3BPJPVTR.pvKpK1s5.js} +1 -1
  35. package/web/_astro/{blockDiagram-GPEHLZMM.BVaCBoeN.js → blockDiagram-GPEHLZMM.BQmRcZsS.js} +1 -1
  36. package/web/_astro/{c4Diagram-AAUBKEIU.CHSDpEO5.js → c4Diagram-AAUBKEIU.CCLjHeH1.js} +1 -1
  37. package/web/_astro/channel.C7wG9RxA.js +1 -0
  38. package/web/_astro/{chunk-2J33WTMH.MYRcYVRa.js → chunk-2J33WTMH.DL6Yuoma.js} +1 -1
  39. package/web/_astro/{chunk-4BX2VUAB.S5j22Zqa.js → chunk-4BX2VUAB.CzlyEI7l.js} +1 -1
  40. package/web/_astro/{chunk-55IACEB6.gciRPvmZ.js → chunk-55IACEB6.B60EWxp1.js} +1 -1
  41. package/web/_astro/{chunk-727SXJPM.CdqrSQkX.js → chunk-727SXJPM.c7v_fRLo.js} +1 -1
  42. package/web/_astro/{chunk-AQP2D5EJ.CyokdHT3.js → chunk-AQP2D5EJ.8BxLWdDc.js} +1 -1
  43. package/web/_astro/{chunk-FMBD7UC4.Bg9KJhjW.js → chunk-FMBD7UC4.DmKuxGFT.js} +1 -1
  44. package/web/_astro/{chunk-ND2GUHAM.1gxxI-vD.js → chunk-ND2GUHAM.1Zwd4mqV.js} +1 -1
  45. package/web/_astro/{chunk-QZHKN3VN.DDEYtCKU.js → chunk-QZHKN3VN.BvYLu-A1.js} +1 -1
  46. package/web/_astro/{classDiagram-4FO5ZUOK.Ch3zi37T.js → classDiagram-4FO5ZUOK.CEqRt0WU.js} +1 -1
  47. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Ch3zi37T.js → classDiagram-v2-Q7XG4LA2.CEqRt0WU.js} +1 -1
  48. package/web/_astro/{cose-bilkent-S5V4N54A.D2q5AINI.js → cose-bilkent-S5V4N54A.C7JiUdj2.js} +1 -1
  49. package/web/_astro/{cynefin-OW5HDTMX.BA9kC2YT.js → cynefin-OW5HDTMX.B6mZ-1be.js} +1 -1
  50. package/web/_astro/{dagre-BM42HDAG.CHO08nRp.js → dagre-BM42HDAG.BuryfDy6.js} +1 -1
  51. package/web/_astro/{diagram-2AECGRRQ.BfFQt0LW.js → diagram-2AECGRRQ.dao39YQM.js} +1 -1
  52. package/web/_astro/{diagram-5GNKFQAL.CHMwMTe4.js → diagram-5GNKFQAL.lnGZe4At.js} +1 -1
  53. package/web/_astro/{diagram-KO2AKTUF.BJ-7gK-w.js → diagram-KO2AKTUF.uWfhzEfT.js} +1 -1
  54. package/web/_astro/{diagram-LMA3HP47.C5xqcc0t.js → diagram-LMA3HP47.ppq8-198.js} +1 -1
  55. package/web/_astro/{diagram-OG6HWLK6.C2pQjfrC.js → diagram-OG6HWLK6.CKSCqDt5.js} +1 -1
  56. package/web/_astro/{erDiagram-TEJ5UH35._FoNyxgC.js → erDiagram-TEJ5UH35.sFSf2yBN.js} +1 -1
  57. package/web/_astro/{flowDiagram-I6XJVG4X.B_ENkT2I.js → flowDiagram-I6XJVG4X.BJQjKLbz.js} +1 -1
  58. package/web/_astro/{ganttDiagram-6RSMTGT7.B3q8h77C.js → ganttDiagram-6RSMTGT7.DAHKNj2N.js} +1 -1
  59. package/web/_astro/{gitGraphDiagram-PVQCEYII.QWZFDboZ.js → gitGraphDiagram-PVQCEYII.BlmzcW_O.js} +1 -1
  60. package/web/_astro/index.BCY1JJHd.css +1 -0
  61. package/web/_astro/{infoDiagram-5YYISTIA.Cs6eEc92.js → infoDiagram-5YYISTIA.DGhjaqsi.js} +1 -1
  62. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BU9DJMHA.js → ishikawaDiagram-YF4QCWOH.gKIpgt6F.js} +1 -1
  63. package/web/_astro/{journeyDiagram-JHISSGLW.D35k6Jpe.js → journeyDiagram-JHISSGLW.DG3tmP0R.js} +1 -1
  64. package/web/_astro/{kanban-definition-UN3LZRKU.DBqcAT4m.js → kanban-definition-UN3LZRKU.CgHDSxkR.js} +1 -1
  65. package/web/_astro/{linear.D-LSNK8W.js → linear.BuGEpcuM.js} +1 -1
  66. package/web/_astro/{mermaid.core.CSruWVOa.js → mermaid.core.DBVqkWBp.js} +4 -4
  67. package/web/_astro/{mindmap-definition-RKZ34NQL.BiFaUsfj.js → mindmap-definition-RKZ34NQL.D-kOcji9.js} +1 -1
  68. package/web/_astro/{pieDiagram-4H26LBE5.D1KTKqb9.js → pieDiagram-4H26LBE5.BvGPwtZt.js} +1 -1
  69. package/web/_astro/{quadrantDiagram-W4KKPZXB.DBHflsMG.js → quadrantDiagram-W4KKPZXB.BKb_9FV6.js} +1 -1
  70. package/web/_astro/{requirementDiagram-4Y6WPE33.hwX9CNdk.js → requirementDiagram-4Y6WPE33.Cx0fvD8v.js} +1 -1
  71. package/web/_astro/{sankeyDiagram-5OEKKPKP.DkI8mJiE.js → sankeyDiagram-5OEKKPKP.fatULi0W.js} +1 -1
  72. package/web/_astro/{sequenceDiagram-3UESZ5HK.DMz7jBDx.js → sequenceDiagram-3UESZ5HK.ChHyKqco.js} +1 -1
  73. package/web/_astro/{stateDiagram-AJRCARHV.B6Ef2xJ1.js → stateDiagram-AJRCARHV.BdUpKWyj.js} +1 -1
  74. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CgoaufB9.js → stateDiagram-v2-BHNVJYJU.fmJxqIm5.js} +1 -1
  75. package/web/_astro/{timeline-definition-PNZ67QCA.B3c8h1t-.js → timeline-definition-PNZ67QCA.BayDn5-V.js} +1 -1
  76. package/web/_astro/{vennDiagram-CIIHVFJN.BdHPAxAb.js → vennDiagram-CIIHVFJN.Bm2Aggwa.js} +1 -1
  77. package/web/_astro/{wardleyDiagram-YWT4CUSO.BrJXPPAj.js → wardleyDiagram-YWT4CUSO.DI75O-uG.js} +1 -1
  78. package/web/_astro/{xychartDiagram-2RQKCTM6.CMH5yFsV.js → xychartDiagram-2RQKCTM6.DL90E7Q7.js} +1 -1
  79. package/web/index.html +2 -2
  80. package/config/rules/docs/verdict-confidence.yaml +0 -68
  81. package/web/_astro/channel.DkpvN2tD.js +0 -1
  82. package/web/_astro/index.BFyW13JJ.css +0 -1
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 ?? [],
@@ -88794,13 +88795,12 @@ function aggregateVerifyVerdict(input) {
88794
88795
  return "PARTIAL";
88795
88796
  return "PASS";
88796
88797
  }
88797
- var VERDICT_AGGREGATES, ROW_STATUSES, CHECK_SEVERITIES, CONFIDENCE_LEVELS, verdictSchema, coverageRowSchema, checkSchema, verifyVerdictSchema, NORM = (s) => String(s ?? "").toUpperCase();
88798
+ var VERDICT_AGGREGATES, ROW_STATUSES, CHECK_SEVERITIES, verdictSchema, coverageRowSchema, checkSchema, verifyVerdictSchema, NORM = (s) => String(s ?? "").toUpperCase();
88798
88799
  var init_verify_verdict = __esm(() => {
88799
88800
  init_zod();
88800
88801
  VERDICT_AGGREGATES = ["PASS", "PARTIAL", "FAIL", "UNKNOWN"];
88801
88802
  ROW_STATUSES = ["MET", "PARTIAL", "UNMET", "N/A"];
88802
88803
  CHECK_SEVERITIES = ["blocker", "major", "minor", "advisory"];
88803
- CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
88804
88804
  verdictSchema = string2().transform((v, ctx) => {
88805
88805
  const up = v.toUpperCase();
88806
88806
  if (!VERDICT_AGGREGATES.includes(up)) {
@@ -88856,18 +88856,6 @@ var init_verify_verdict = __esm(() => {
88856
88856
  acceptanceCriteria: array(coverageRowSchema).optional().default([]),
88857
88857
  checks: array(checkSchema).optional().default([]),
88858
88858
  source: string2().optional(),
88859
- confidence: string2().transform((c, ctx) => {
88860
- const up = c.toUpperCase();
88861
- if (!CONFIDENCE_LEVELS.includes(up)) {
88862
- ctx.addIssue({
88863
- code: "custom",
88864
- message: `invalid confidence "${c}" (expected HIGH|MEDIUM|LOW)`,
88865
- path: ["confidence"]
88866
- });
88867
- return "LOW";
88868
- }
88869
- return up;
88870
- }).optional(),
88871
88859
  pipelineRunId: string2().optional(),
88872
88860
  recordedAt: string2().optional()
88873
88861
  });
@@ -90130,19 +90118,6 @@ async function readVerdictFrom(fs, path, wbs) {
90130
90118
  };
90131
90119
  }
90132
90120
  }
90133
- if ("confidence" in parsed) {
90134
- const rawConfidence = parsed.confidence;
90135
- const up = typeof rawConfidence === "string" ? rawConfidence.toUpperCase() : "";
90136
- if (up !== "HIGH" && up !== "MEDIUM" && up !== "LOW") {
90137
- return {
90138
- artifact: undefined,
90139
- path,
90140
- readError: `invalid confidence field at ${path}: expected HIGH|MEDIUM|LOW, actual ${JSON.stringify(rawConfidence)}`,
90141
- missing: false
90142
- };
90143
- }
90144
- parsed.confidence = up;
90145
- }
90146
90121
  return { artifact: parsed, path, missing: false };
90147
90122
  }
90148
90123
  async function readVerdictArtifact(fs, runDir, wbs) {
@@ -90156,6 +90131,30 @@ async function readVerdictArtifact(fs, runDir, wbs) {
90156
90131
  const { missing: _scratchMissing, ...result } = scratch;
90157
90132
  return result;
90158
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
+ }
90159
90158
  function computeAggregate(artifact) {
90160
90159
  const reqs = artifact.requirements ?? [];
90161
90160
  const acs = artifact.acceptanceCriteria ?? [];
@@ -90237,15 +90236,17 @@ function evaluateDoneTransition(input) {
90237
90236
  const acs = artifact.acceptanceCriteria ?? [];
90238
90237
  const internallyConsistentPass = artifact.verdict !== "PASS" || reqs.length > 0 || acs.length > 0;
90239
90238
  const effective = internallyConsistentPass ? harshnessMax(artifact.verdict, computed) : "UNKNOWN";
90240
- if (effective === "PASS" && artifact.confidence === undefined) {
90239
+ const unresolved = input.unresolvedTestEvidence ?? [];
90240
+ if (effective === "PASS" && unresolved.length > 0) {
90241
90241
  return {
90242
90242
  kind: "deny",
90243
- verdict: "PASS",
90243
+ verdict: "PARTIAL",
90244
90244
  message: [
90245
- `Cannot transition task ${wbs} to done: PASS verdict does not state a confidence level.`,
90246
- ` task: ${taskFilePath}`,
90247
- ` verdict: ${verdictPath} (missing \`confidence\`)`,
90248
- ` remediation: add "confidence": "HIGH|MEDIUM|LOW" to the artifact ` + `(re-run \`/sp:dev-verify ${wbs}\`), ` + `or override with \`spur task update ${wbs} done --force-done --reason "<why>"\`.`
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>"\`.`
90249
90250
  ].join(`
90250
90251
  `)
90251
90252
  };
@@ -90271,8 +90272,10 @@ function harshnessMax(a, b) {
90271
90272
  const rank = { PASS: 0, UNKNOWN: 1, PARTIAL: 2, FAIL: 3 };
90272
90273
  return rank[a] >= rank[b] ? a : b;
90273
90274
  }
90275
+ var TEST_PATH_RE;
90274
90276
  var init_done_transition_guard = __esm(() => {
90275
90277
  init_verify_verdict();
90278
+ TEST_PATH_RE = /(?:^|[\s`'"([,])((?:[\w@.-]+\/)+[\w@.-]+\.(?:test|spec)\.[cm]?[jt]sx?)\b/g;
90276
90279
  });
90277
90280
 
90278
90281
  // ../../packages/app/src/workflow/workflow-resolver.ts
@@ -91196,8 +91199,6 @@ function renderTesting(v) {
91196
91199
  lines.push("**Pipeline verify results**");
91197
91200
  lines.push("");
91198
91201
  lines.push(`- Verdict: ${v.verdict} (from verdict artifact)`);
91199
- if (v.confidence !== undefined)
91200
- lines.push(`- Confidence: ${v.confidence}`);
91201
91202
  lines.push("");
91202
91203
  if (v.requirements.length === 0) {
91203
91204
  lines.push("| Requirement | Status | Evidence |");
@@ -91629,11 +91630,6 @@ function lintVerifyAnswer(answerText, taskContent, featureContent) {
91629
91630
  } else if (!/^(PASS|PARTIAL|FAIL)$/i.test(tables.verdict.value)) {
91630
91631
  add(tables.verdict.line, "verdict-value", `invalid Verdict value "${tables.verdict.value}" (PASS | PARTIAL | FAIL)`);
91631
91632
  }
91632
- if (tables.confidence === null) {
91633
- add(0, "confidence-missing", "no `Confidence:` line (expected exactly one `Confidence: HIGH|MEDIUM|LOW` line)");
91634
- } else if (!/^(HIGH|MEDIUM|LOW)$/i.test(tables.confidence.value)) {
91635
- add(tables.confidence.line, "confidence-value", `invalid Confidence value "${tables.confidence.value}" (HIGH | MEDIUM | LOW)`);
91636
- }
91637
91633
  const reqIds = extractRequirementIds(taskContent);
91638
91634
  const acIndex = buildAcIdentityIndex(taskContent, featureContent ?? null);
91639
91635
  const seenReq = new Set;
@@ -91693,19 +91689,6 @@ function lintVerifyAnswer(answerText, taskContent, featureContent) {
91693
91689
  add(row.line, "evidence-hedged", hedgedMessage(`AC "${row.id.slice(0, 40)}"`, hedge));
91694
91690
  }
91695
91691
  }
91696
- const confidenceLevel = tables.confidence === null ? null : tables.confidence.value.toUpperCase();
91697
- if (confidenceLevel === "HIGH" || confidenceLevel === "LOW") {
91698
- const reqStatuses = tables.reqs.map((row) => normalizeReqStatus(row.status));
91699
- const acStatuses = tables.acs.map((row) => normalizeAcStatus(row.status));
91700
- const allRowsMet = reqStatuses.length > 0 && reqStatuses.every((status) => status === "MET") && acStatuses.every((status) => status === "MET");
91701
- const hedgedRow = [...tables.reqs, ...tables.acs].some((row) => (normalizeReqStatus(row.status) ?? normalizeAcStatus(row.status)) === "MET" && firstHedgedPhrase(row.evidence) !== null);
91702
- const line = tables.confidence?.line ?? 0;
91703
- if (confidenceLevel === "HIGH" && (!allRowsMet || hedgedRow)) {
91704
- add(line, "confidence-unwarranted", "Confidence HIGH is not earned by the rows: at least one row is not MET, or a MET " + "row hedges its evidence (downgrade the level, or finish the row)");
91705
- } else if (confidenceLevel === "LOW" && allRowsMet) {
91706
- add(line, "confidence-understated", "Confidence LOW understates an answer whose every row is MET (raise the level to " + "HIGH or MEDIUM)");
91707
- }
91708
- }
91709
91692
  return findings;
91710
91693
  }
91711
91694
  function hasClaimMarker(evidence) {
@@ -91780,7 +91763,7 @@ function normalizeEvidenceType(raw) {
91780
91763
  return EVIDENCE_TYPE_PRECEDENCE.find((candidate) => tokens.includes(candidate)) ?? null;
91781
91764
  }
91782
91765
  function parseAnswer(text) {
91783
- const out = { verdict: null, confidence: null, reqs: [], acs: [] };
91766
+ const out = { verdict: null, reqs: [], acs: [] };
91784
91767
  const lines = text.split(`
91785
91768
  `);
91786
91769
  let reqTable = false;
@@ -91840,26 +91823,11 @@ function parseAnswer(text) {
91840
91823
  out.verdict = {
91841
91824
  value: verdictMatches[0]?.[1] ?? "",
91842
91825
  line: text.slice(0, verdictMatches[0]?.index ?? 0).split(`
91843
- `).length
91844
- };
91845
- }
91846
- const confidenceMatches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
91847
- if (confidenceMatches.length === 1) {
91848
- out.confidence = {
91849
- value: confidenceMatches[0]?.[1] ?? "",
91850
- line: text.slice(0, confidenceMatches[0]?.index ?? 0).split(`
91851
91826
  `).length
91852
91827
  };
91853
91828
  }
91854
91829
  return out;
91855
91830
  }
91856
- function extractAnswerConfidence(text) {
91857
- const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
91858
- if (matches.length !== 1)
91859
- return;
91860
- const value = (matches[0]?.[1] ?? "").toUpperCase();
91861
- return CONFIDENCE_LEVELS2.includes(value) ? value : undefined;
91862
- }
91863
91831
  function sectionBetween(text, heading) {
91864
91832
  const marker = new RegExp(`^#{1,6}\\s+${heading}\\s*$`, "im");
91865
91833
  const match = marker.exec(text);
@@ -91913,7 +91881,9 @@ function buildAcIdentityIndex(taskContent, featureContent) {
91913
91881
  if (!label)
91914
91882
  continue;
91915
91883
  declareIdentity(label);
91916
- 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();
91917
91887
  if (head && head !== label)
91918
91888
  declareIdentity(head);
91919
91889
  const leading = label.split(/\s+/)[0] ?? "";
@@ -91969,7 +91939,7 @@ function resolveAcIdentity(rowId, index) {
91969
91939
  }
91970
91940
  return { ok: false, error: "" };
91971
91941
  }
91972
- var ANSWER_LINT_MAX_FINDINGS = 10, CLAIM_MARKERS, CITATION_FORMS, HEDGED_PATTERNS, EVIDENCE_TYPE_PRECEDENCE, CONFIDENCE_LEVELS2;
91942
+ var ANSWER_LINT_MAX_FINDINGS = 10, CLAIM_MARKERS, CITATION_FORMS, HEDGED_PATTERNS, EVIDENCE_TYPE_PRECEDENCE;
91973
91943
  var init_verify_answer_lint = __esm(() => {
91974
91944
  CLAIM_MARKERS = [
91975
91945
  /@[a-z0-9._-]+\/[a-z0-9._-]+/i,
@@ -91993,7 +91963,6 @@ var init_verify_answer_lint = __esm(() => {
91993
91963
  /\bit could\b/i
91994
91964
  ];
91995
91965
  EVIDENCE_TYPE_PRECEDENCE = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
91996
- CONFIDENCE_LEVELS2 = ["HIGH", "MEDIUM", "LOW"];
91997
91966
  });
91998
91967
 
91999
91968
  // ../../packages/app/src/services/feature-check.ts
@@ -92252,6 +92221,21 @@ async function readFeatureAcBody(fs, featuresDir, featureId) {
92252
92221
  return null;
92253
92222
  }
92254
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
+ }
92255
92239
  async function lintVerifyAnswerForTask(wbs, answerText, deps) {
92256
92240
  let taskContent;
92257
92241
  let featureId = "";
@@ -104473,7 +104457,11 @@ class AgentService {
104473
104457
  const phaseSuffix = phase !== undefined ? ` for phase '${phase}'` : "";
104474
104458
  if (executor !== undefined) {
104475
104459
  if (source === "default") {
104476
- warnAgentDefaultExecutorOnce(selector, this.ctx.output);
104460
+ return {
104461
+ ok: false,
104462
+ exitCode: 2,
104463
+ message: agentDefaultExecutorMessage(selector, this.ctx.roles)
104464
+ };
104477
104465
  }
104478
104466
  if (executorDisabled(executor)) {
104479
104467
  return {
@@ -104522,13 +104510,11 @@ class AgentService {
104522
104510
  }
104523
104511
  if (source === "default") {
104524
104512
  const roleNames = this.ctx.roles !== undefined ? [...this.ctx.roles.keys()].join(", ") : "";
104525
- const names = this.ctx.agentConfig?.executors?.map((e) => e.name) ?? [];
104526
- const roleList = roleNames !== "" ? `role (${roleNames})` : "a role";
104527
- const executorList = names.length > 0 ? `configured executor (${names.join(", ")})` : "a configured executor";
104513
+ const roleList = roleNames !== "" ? `a role (${roleNames})` : "a role";
104528
104514
  return {
104529
104515
  ok: false,
104530
104516
  exitCode: 2,
104531
- message: `Unknown agent.default value: '${selector}'. Accepted: ${roleList} or ${executorList}.`
104517
+ message: `Unknown agent.default value: '${selector}'. Accepted: ${roleList}, e.g. agent.default: coder.`
104532
104518
  };
104533
104519
  }
104534
104520
  const canonical = resolveAgentName(selector);
@@ -104840,11 +104826,9 @@ function warnBareBinaryOnce(selector, output) {
104840
104826
  warnedBareBinary.add(selector);
104841
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).`);
104842
104828
  }
104843
- function warnAgentDefaultExecutorOnce(selector, output) {
104844
- if (warnedAgentDefaultExecutor.has(selector))
104845
- return;
104846
- warnedAgentDefaultExecutor.add(selector);
104847
- 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.`;
104848
104832
  }
104849
104833
  function buildDoctorRows(results, executors, roles) {
104850
104834
  const executorByName = new Map((executors ?? []).map((e) => [e.name, e]));
@@ -105096,7 +105080,7 @@ function buildRoutingAttribution(result) {
105096
105080
  ...result.model !== undefined ? { model: result.model } : {}
105097
105081
  };
105098
105082
  }
105099
- 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;
105100
105084
  var init_agent_service = __esm(() => {
105101
105085
  init_src();
105102
105086
  init_src2();
@@ -105153,7 +105137,6 @@ var init_agent_service = __esm(() => {
105153
105137
  SENSITIVE_FLAG = /^--?(?:api[-_]?key|authorization|credential|password|secret|token)$/i;
105154
105138
  SENSITIVE_INLINE_FLAG = /^(--?(?:api[-_]?key|authorization|credential|password|secret|token)=).+$/i;
105155
105139
  warnedBareBinary = new Set;
105156
- warnedAgentDefaultExecutor = new Set;
105157
105140
  });
105158
105141
 
105159
105142
  // ../../packages/app/src/services/agent-trace-service.ts
@@ -105869,8 +105852,20 @@ async function prepareBoardModules(input) {
105869
105852
  }
105870
105853
  }
105871
105854
  let realProjectRoot;
105872
- const projectRootRealPath = () => {
105873
- 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");
105874
105869
  return realProjectRoot;
105875
105870
  };
105876
105871
  for (const [index, declaration] of input.declarations.entries()) {
@@ -105883,22 +105878,35 @@ async function prepareBoardModules(input) {
105883
105878
  }
105884
105879
  const root = resolve17(input.projectRoot, declaration.directory);
105885
105880
  const entryPath = resolveContained(index, input.projectRoot, declaration.directory, declaration.entry, declaration.id);
105886
- 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);
105887
105886
  if (!await input.probe.isDirectory(root)) {
105888
105887
  throw new BoardModuleConfigError(index, declaration.id, `module directory "${declaration.directory}" was not found`);
105889
105888
  }
105890
105889
  if (!await input.probe.isFile(entryPath)) {
105891
105890
  throw new BoardModuleConfigError(index, declaration.id, `entry asset "${declaration.entry}" was not found in "${declaration.directory}"`);
105892
105891
  }
105893
- for (const [styleIndex, stylePath] of stylePaths.entries()) {
105894
- if (!await input.probe.isFile(stylePath)) {
105895
- 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`);
105896
105895
  }
105897
105896
  }
105898
- const realRoot = input.probe.realPath(root);
105899
- const realEntry = input.probe.realPath(entryPath);
105900
- if (realRoot !== undefined && realEntry !== undefined && (!isInsideRoot(projectRootRealPath(), realRoot) || !isInsideRoot(realRoot, realEntry))) {
105901
- 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"));
105902
105910
  }
105903
105911
  descriptors.push({
105904
105912
  ...descriptorBase(declaration),
@@ -114571,7 +114579,8 @@ async function transitionTaskGuarded(deps, input) {
114571
114579
  reason: input.reason,
114572
114580
  artifact: loaded.artifact,
114573
114581
  verdictPath: loaded.path,
114574
- readError: loaded.readError
114582
+ readError: loaded.readError,
114583
+ unresolvedTestEvidence: input.forceDone === true ? [] : await findUnresolvedTestEvidence(deps.fs, dirname31(dirname31(deps.runDir)), loaded.artifact)
114575
114584
  });
114576
114585
  if (guardOutcome.kind === "noop") {
114577
114586
  return { kind: "noop", wbs, status: "done", message: guardOutcome.message };
@@ -114609,329 +114618,6 @@ var init_task_transition = __esm(() => {
114609
114618
  init_done_transition_guard();
114610
114619
  });
114611
114620
 
114612
- // ../../packages/app/src/services/task-verdict.ts
114613
- function confidenceCheckRow(confidence, requirements, acceptanceCriteria) {
114614
- const rows = [
114615
- ...requirements,
114616
- ...acceptanceCriteria ?? []
114617
- ];
114618
- const notMet = rows.filter((r) => r.status !== "MET" && r.status !== "N/A");
114619
- const hedged = rows.filter((r) => r.status === "MET").map((r) => firstHedgedPhrase(r.evidence ?? "")).find((phrase) => phrase !== null);
114620
- const level = confidence ?? "LOW";
114621
- const earned = confidence !== undefined && level !== "LOW" && notMet.length === 0 && hedged === undefined;
114622
- const basis = `${confidence === undefined ? "LOW (absent from the answer)" : level} \u2014 ${rows.length - notMet.length}/${rows.length} rows MET` + (notMet.length > 0 ? ` (not MET: ${notMet.map((r) => r.id).join(", ")})` : "") + (hedged !== undefined ? `; hedged MET evidence: "${hedged}"` : "");
114623
- return { name: "confidence", status: earned ? "pass" : "warn", evidence: basis };
114624
- }
114625
- function injectEarnedConfidenceRow(text) {
114626
- let parsed;
114627
- try {
114628
- parsed = JSON.parse(text);
114629
- } catch {
114630
- return text;
114631
- }
114632
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
114633
- return text;
114634
- const artifact = parsed;
114635
- const checks = Array.isArray(artifact.checks) ? artifact.checks : [];
114636
- if (checks.some((c) => c !== null && typeof c === "object" && c.name === "confidence"))
114637
- return text;
114638
- const level = typeof artifact.confidence === "string" ? artifact.confidence : undefined;
114639
- const row = confidenceCheckRow(level, artifact.requirements ?? [], artifact.acceptanceCriteria);
114640
- if (row.status !== "pass")
114641
- return text;
114642
- return `${JSON.stringify({ ...artifact, checks: [...checks, row] }, null, 4)}
114643
- `;
114644
- }
114645
- function deriveVerdict(answerText, taskCheckPassed) {
114646
- const requirements = extractRequirements(answerText);
114647
- const parsedAc = extractAcceptanceCriteria(answerText);
114648
- const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(parsedAc.rows);
114649
- const checks = extractChecks(answerText, taskCheckPassed, requirements, acceptanceCriteria, parsedAc.dropped);
114650
- const confidence = extractAnswerConfidence(answerText);
114651
- const checksWithConfidence = [
114652
- ...checks.filter((c) => c.name !== "confidence"),
114653
- confidenceCheckRow(confidence, requirements, acceptanceCriteria)
114654
- ];
114655
- if (requirements.length === 0) {
114656
- return {
114657
- verdict: "UNKNOWN",
114658
- ...confidence !== undefined ? { confidence } : {},
114659
- requirements,
114660
- acceptanceCriteria,
114661
- checks: checksWithConfidence
114662
- };
114663
- }
114664
- const aggregate = aggregateVerifyVerdict({
114665
- requirements,
114666
- acceptanceCriteria,
114667
- checks: [],
114668
- taskCheckPassed
114669
- });
114670
- return {
114671
- verdict: aggregate,
114672
- ...confidence !== undefined ? { confidence } : {},
114673
- requirements,
114674
- acceptanceCriteria,
114675
- checks: checksWithConfidence
114676
- };
114677
- }
114678
- function extractRequirements(text) {
114679
- const reqs = [];
114680
- const lines = text.split(`
114681
- `);
114682
- let inTable = false;
114683
- let colMap = null;
114684
- for (const line of lines) {
114685
- const trimmed = line.trim();
114686
- if (inTable && /^#{1,6}\s/.test(trimmed)) {
114687
- inTable = false;
114688
- colMap = null;
114689
- continue;
114690
- }
114691
- if (!trimmed.startsWith("|"))
114692
- continue;
114693
- const cells = splitTableCells2(trimmed);
114694
- if (!inTable && cells.length >= 2) {
114695
- const h0 = (cells[0] ?? "").toLowerCase().trim();
114696
- const h0IsId = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
114697
- if (h0IsId) {
114698
- const statusIdx = cells.findIndex((c) => {
114699
- const x = c.toLowerCase().trim();
114700
- return x.includes("status") || x === "verdict";
114701
- });
114702
- if (statusIdx >= 0) {
114703
- const evidenceIdx = cells.findIndex((c) => c.toLowerCase().includes("evidence"));
114704
- colMap = {
114705
- id: 0,
114706
- status: statusIdx,
114707
- evidence: evidenceIdx >= 0 ? evidenceIdx : undefined
114708
- };
114709
- inTable = true;
114710
- continue;
114711
- }
114712
- }
114713
- }
114714
- if (/^[-:]+$/.test(cells[0] ?? ""))
114715
- continue;
114716
- if (inTable && colMap && cells.length >= 2) {
114717
- const first = (cells[0] ?? "").toLowerCase();
114718
- const second = (cells[1] ?? "").toLowerCase();
114719
- if ((first === "ac" || first.includes("acceptance") || first === "check" || first === "name") && (second.includes("status") || second === "verdict")) {
114720
- inTable = false;
114721
- colMap = null;
114722
- continue;
114723
- }
114724
- const id = cells[colMap.id] ?? "";
114725
- const statusRaw = (cells[colMap.status] ?? "").toUpperCase();
114726
- const evidence = colMap.evidence !== undefined ? cells[colMap.evidence] ?? "" : "";
114727
- const status = normalizeStatus(statusRaw);
114728
- if (status !== null && id.length > 0) {
114729
- reqs.push({ id, status, evidence });
114730
- }
114731
- }
114732
- }
114733
- return reqs;
114734
- }
114735
- function normalizeStatus(raw) {
114736
- if (/\bMET\b/.test(raw))
114737
- return "MET";
114738
- if (/\bPARTIAL\b/.test(raw))
114739
- return "PARTIAL";
114740
- if (/\bUNMET\b/.test(raw))
114741
- return "UNMET";
114742
- return null;
114743
- }
114744
- function splitTableCells2(row) {
114745
- return row.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, "|").trim()).filter(Boolean);
114746
- }
114747
- function extractAcceptanceCriteria(text) {
114748
- const rows = [];
114749
- const dropped = [];
114750
- const lines = text.split(`
114751
- `);
114752
- let inTable = false;
114753
- for (const line of lines) {
114754
- const trimmed = line.trim();
114755
- if (inTable && /^#{1,6}\s/.test(trimmed)) {
114756
- inTable = false;
114757
- continue;
114758
- }
114759
- if (!trimmed.startsWith("|"))
114760
- continue;
114761
- const cells = splitTableCells2(trimmed);
114762
- if (!inTable && cells.length >= 4) {
114763
- const h0 = (cells[0] ?? "").toLowerCase();
114764
- const h1 = (cells[1] ?? "").toLowerCase();
114765
- const h2 = (cells[2] ?? "").toLowerCase();
114766
- if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status") && h2.includes("evidence")) {
114767
- inTable = true;
114768
- continue;
114769
- }
114770
- }
114771
- if (/^[-:]+$/.test(cells[0] ?? ""))
114772
- continue;
114773
- if (inTable && cells.length >= 3) {
114774
- const id = cells[0] ?? "";
114775
- const statusRaw = cells[1] ?? "";
114776
- const evidenceTypeRaw = cells[2] ?? "";
114777
- const status = normalizeAcceptanceCriteriaStatus(statusRaw);
114778
- const evidenceType = normalizeEvidenceType2(evidenceTypeRaw);
114779
- const acShaped = cells.length >= 4 || id.length > 0 && status !== null;
114780
- const evidence = cells[3] ?? "";
114781
- if (acShaped && status !== null && evidenceType !== null && id.length > 0) {
114782
- rows.push({ id, status, evidenceType, evidence });
114783
- } else if (acShaped && id.length > 0) {
114784
- dropped.push(evidenceType === null && status !== null ? `${id} (unrecognised evidence type "${evidenceTypeRaw}")` : `${id} (unrecognised status "${statusRaw}")`);
114785
- }
114786
- }
114787
- }
114788
- return { rows, dropped };
114789
- }
114790
- function normalizeAcceptanceCriteriaStatus(raw) {
114791
- const upper = raw.toUpperCase();
114792
- if (/\bMET\b/.test(upper))
114793
- return "MET";
114794
- if (/\bPARTIAL\b/.test(upper))
114795
- return "PARTIAL";
114796
- if (/\bUNMET\b/.test(upper))
114797
- return "UNMET";
114798
- if (/\bN\/A\b/.test(upper) || /\bNA\b/.test(upper))
114799
- return "N/A";
114800
- return null;
114801
- }
114802
- function normalizeEvidenceTypeToken2(normalized) {
114803
- if (normalized === "test")
114804
- return "test";
114805
- if (normalized === "command")
114806
- return "command";
114807
- if (normalized === "static-ref" || normalized === "static" || normalized === "doc" || normalized === "docs" || normalized === "documentation") {
114808
- return "static-ref";
114809
- }
114810
- if (normalized === "manual-review" || normalized === "manual")
114811
- return "manual-review";
114812
- if (normalized === "llm-judge" || normalized === "judge")
114813
- return "llm-judge";
114814
- if (normalized === "n/a" || normalized === "na")
114815
- return "n/a";
114816
- return null;
114817
- }
114818
- function normalizeEvidenceType2(raw) {
114819
- const normalized = raw.toLowerCase().trim();
114820
- const single = normalizeEvidenceTypeToken2(normalized);
114821
- if (single !== null)
114822
- return single;
114823
- const tokens = normalized.split(/[+,/]/).map((part) => normalizeEvidenceTypeToken2(part.trim())).filter((token) => token !== null);
114824
- if (tokens.length < 2 || tokens.length !== normalized.split(/[+,/]/).filter((p) => p.trim()).length) {
114825
- return null;
114826
- }
114827
- return EVIDENCE_TYPE_PRECEDENCE2.find((candidate) => tokens.includes(candidate)) ?? null;
114828
- }
114829
- function applyAcceptanceCriteriaEvidenceRule(rows) {
114830
- return rows.map((row) => {
114831
- if (row.status === "MET" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command") {
114832
- return { ...row, status: "PARTIAL" };
114833
- }
114834
- return row;
114835
- });
114836
- }
114837
- function requiresExecutableEvidence(id) {
114838
- const normalized = id.toLowerCase();
114839
- const nonCore = normalized.includes("[advisory]") || normalized.includes("[non-core]");
114840
- const nonBehavior = normalized.includes("[non-behavior]") || normalized.includes("[docs-only]") || normalized.includes("[doc-only]");
114841
- return !nonCore && !nonBehavior;
114842
- }
114843
- function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria, droppedAcRows = []) {
114844
- const checks = [
114845
- {
114846
- name: "spur task check",
114847
- status: taskCheckPassed ? "pass" : "fail",
114848
- evidence: taskCheckPassed ? "task check passed" : "task check failed"
114849
- }
114850
- ];
114851
- const lines = _text.split(`
114852
- `);
114853
- let inChecks = false;
114854
- for (const line of lines) {
114855
- const trimmed = line.trim();
114856
- if (/^#{1,6}\s/.test(trimmed))
114857
- inChecks = false;
114858
- if (!trimmed.startsWith("|"))
114859
- continue;
114860
- const cells = splitTableCells2(trimmed);
114861
- if (cells.length >= 2) {
114862
- const h0 = (cells[0] ?? "").toLowerCase();
114863
- const h1 = (cells[1] ?? "").toLowerCase();
114864
- if ((h0 === "check" || h0 === "name") && h1 === "status") {
114865
- inChecks = true;
114866
- continue;
114867
- }
114868
- if (inChecks && cells.length >= 3 && !/^[-:]+$/.test(cells[0] ?? "")) {
114869
- checks.push({
114870
- name: cells[0] ?? "",
114871
- status: (cells[1] ?? "").toLowerCase(),
114872
- evidence: cells[2] ?? ""
114873
- });
114874
- }
114875
- }
114876
- }
114877
- if (droppedAcRows.length > 0) {
114878
- checks.push({
114879
- name: "ac-row-dropped",
114880
- status: "fail",
114881
- evidence: `${droppedAcRows.length} AC row(s) could not be parsed and were omitted from the verdict: ` + `${droppedAcRows.join("; ")}. Accepted evidence types: test, command, static-ref (aliases: ` + "static, doc, docs, documentation), manual-review, llm-judge, n/a. Accepted statuses: " + "MET, PARTIAL, UNMET, N/A."
114882
- });
114883
- }
114884
- const hollowMet = [
114885
- ...requirements.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id),
114886
- ...acceptanceCriteria.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id)
114887
- ];
114888
- if (hollowMet.length > 0) {
114889
- checks.push({
114890
- name: "hollow-met-evidence",
114891
- status: "fail",
114892
- severity: "major",
114893
- evidence: `${hollowMet.length} MET row(s) carry no recorded evidence and cannot support PASS: ` + `${hollowMet.join(", ")}. Re-run verify with populated evidence per row.`
114894
- });
114895
- }
114896
- if (acceptanceCriteria.length > 0) {
114897
- const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id) && row.evidenceType !== "test" && row.evidenceType !== "command");
114898
- checks.push({
114899
- name: downgraded.length > 0 ? "evidence-rule-failed" : "evidence-rule-pass",
114900
- status: downgraded.length > 0 ? "fail" : "pass",
114901
- evidence: downgraded.length > 0 ? `Executable evidence missing for: ${downgraded.map((row) => row.id).join(", ")}` : "All behavior-bearing AC rows have executable evidence or are explicitly non-behavioral."
114902
- });
114903
- }
114904
- return checks;
114905
- }
114906
- function aggregateBatchVerdicts(results) {
114907
- const notStarted = results.filter((r) => r.outcome === "NOT-STARTED");
114908
- const rolledUp = results.filter((r) => r.outcome !== "NOT-STARTED");
114909
- const counts = {
114910
- PASS: rolledUp.filter((r) => r.outcome === "PASS").length,
114911
- PARTIAL: rolledUp.filter((r) => r.outcome === "PARTIAL").length,
114912
- FAIL: rolledUp.filter((r) => r.outcome === "FAIL").length,
114913
- UNKNOWN: rolledUp.filter((r) => r.outcome === "UNKNOWN").length
114914
- };
114915
- let verdict;
114916
- if (rolledUp.length === 0) {
114917
- verdict = "UNKNOWN";
114918
- } else if (counts.FAIL > 0) {
114919
- verdict = "FAIL";
114920
- } else if (counts.PARTIAL > 0 || counts.UNKNOWN > 0) {
114921
- verdict = "PARTIAL";
114922
- } else {
114923
- verdict = "PASS";
114924
- }
114925
- const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
114926
- return { verdict, rolledUp, notStarted, summary };
114927
- }
114928
- var EVIDENCE_TYPE_PRECEDENCE2;
114929
- var init_task_verdict = __esm(() => {
114930
- init_verify_answer_lint();
114931
- init_verify_verdict();
114932
- EVIDENCE_TYPE_PRECEDENCE2 = ["test", "command", "static-ref", "manual-review", "llm-judge", "n/a"];
114933
- });
114934
-
114935
114621
  // ../../packages/app/src/services/task-service.ts
114936
114622
  import { dirname as dirname32, isAbsolute as isAbsolute12, join as join57, relative as relative13, sep as sep13 } from "path";
114937
114623
  function citationOrigin(verdict, name) {
@@ -115680,7 +115366,7 @@ ${templateBackground}`;
115680
115366
  const bytes = await this.ctx.fs.readFile(verdictPath);
115681
115367
  ensureDurablePlaneIgnored(paths.projectRoot);
115682
115368
  await this.ctx.fs.ensureDir(dirname32(durablePath));
115683
- await atomicWriteAsync(durablePath, injectEarnedConfidenceRow(bytes), wbs, this.ctx.fs, this.ctx.projectName ?? "spur");
115369
+ await atomicWriteAsync(durablePath, bytes, wbs, this.ctx.fs, this.ctx.projectName ?? "spur");
115684
115370
  }
115685
115371
  const result = {
115686
115372
  testingWritten: false,
@@ -116235,7 +115921,6 @@ var init_task_service = __esm(() => {
116235
115921
  init_task_section_normalizer();
116236
115922
  init_task_size_precheck();
116237
115923
  init_task_transition();
116238
- init_task_verdict();
116239
115924
  DependencyMutationError = class DependencyMutationError extends Error {
116240
115925
  code;
116241
115926
  constructor(code, message) {
@@ -116315,6 +116000,311 @@ var init_task_service = __esm(() => {
116315
116000
  };
116316
116001
  });
116317
116002
 
116003
+ // ../../packages/app/src/services/task-verdict.ts
116004
+ function deriveVerdict(answerText, taskCheckPassed, taskAcceptanceCriteria = "") {
116005
+ const requirements = extractRequirements(answerText);
116006
+ const parsedAc = extractAcceptanceCriteria(answerText);
116007
+ const exempt = taskAcExemptions(taskAcceptanceCriteria);
116008
+ const acceptanceCriteria = applyAcceptanceCriteriaEvidenceRule(parsedAc.rows, exempt);
116009
+ const checks = extractChecks(answerText, taskCheckPassed, requirements, acceptanceCriteria, parsedAc.dropped, exempt);
116010
+ if (requirements.length === 0) {
116011
+ return {
116012
+ verdict: "UNKNOWN",
116013
+ requirements,
116014
+ acceptanceCriteria,
116015
+ checks
116016
+ };
116017
+ }
116018
+ const aggregate = aggregateVerifyVerdict({
116019
+ requirements,
116020
+ acceptanceCriteria,
116021
+ checks: [],
116022
+ taskCheckPassed
116023
+ });
116024
+ return {
116025
+ verdict: aggregate,
116026
+ requirements,
116027
+ acceptanceCriteria,
116028
+ checks
116029
+ };
116030
+ }
116031
+ function extractRequirements(text) {
116032
+ const reqs = [];
116033
+ const lines = text.split(`
116034
+ `);
116035
+ let inTable = false;
116036
+ let colMap = null;
116037
+ for (const line of lines) {
116038
+ const trimmed = line.trim();
116039
+ if (inTable && /^#{1,6}\s/.test(trimmed)) {
116040
+ inTable = false;
116041
+ colMap = null;
116042
+ continue;
116043
+ }
116044
+ if (!trimmed.startsWith("|"))
116045
+ continue;
116046
+ const cells = splitTableCells2(trimmed);
116047
+ if (!inTable && cells.length >= 2) {
116048
+ const h0 = (cells[0] ?? "").toLowerCase().trim();
116049
+ const h0IsId = h0.includes("req") || h0 === "requirement" || h0 === "r#" || h0 === "r" || /^r\d+$/.test(h0);
116050
+ if (h0IsId) {
116051
+ const statusIdx = cells.findIndex((c) => {
116052
+ const x = c.toLowerCase().trim();
116053
+ return x.includes("status") || x === "verdict";
116054
+ });
116055
+ if (statusIdx >= 0) {
116056
+ const evidenceIdx = cells.findIndex((c) => c.toLowerCase().includes("evidence"));
116057
+ colMap = {
116058
+ id: 0,
116059
+ status: statusIdx,
116060
+ evidence: evidenceIdx >= 0 ? evidenceIdx : undefined
116061
+ };
116062
+ inTable = true;
116063
+ continue;
116064
+ }
116065
+ }
116066
+ }
116067
+ if (/^[-:]+$/.test(cells[0] ?? ""))
116068
+ continue;
116069
+ if (inTable && colMap && cells.length >= 2) {
116070
+ const first = (cells[0] ?? "").toLowerCase();
116071
+ const second = (cells[1] ?? "").toLowerCase();
116072
+ if ((first === "ac" || first.includes("acceptance") || first === "check" || first === "name") && (second.includes("status") || second === "verdict")) {
116073
+ inTable = false;
116074
+ colMap = null;
116075
+ continue;
116076
+ }
116077
+ const id = cells[colMap.id] ?? "";
116078
+ const statusRaw = (cells[colMap.status] ?? "").toUpperCase();
116079
+ const evidence = colMap.evidence !== undefined ? cells[colMap.evidence] ?? "" : "";
116080
+ const status = normalizeStatus(statusRaw);
116081
+ if (status !== null && id.length > 0) {
116082
+ reqs.push({ id, status, evidence });
116083
+ }
116084
+ }
116085
+ }
116086
+ return reqs;
116087
+ }
116088
+ function normalizeStatus(raw) {
116089
+ if (/\bMET\b/.test(raw))
116090
+ return "MET";
116091
+ if (/\bPARTIAL\b/.test(raw))
116092
+ return "PARTIAL";
116093
+ if (/\bUNMET\b/.test(raw))
116094
+ return "UNMET";
116095
+ return null;
116096
+ }
116097
+ function splitTableCells2(row) {
116098
+ return row.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, "|").trim()).filter(Boolean);
116099
+ }
116100
+ function extractAcceptanceCriteria(text) {
116101
+ const rows = [];
116102
+ const dropped = [];
116103
+ const lines = text.split(`
116104
+ `);
116105
+ let inTable = false;
116106
+ for (const line of lines) {
116107
+ const trimmed = line.trim();
116108
+ if (inTable && /^#{1,6}\s/.test(trimmed)) {
116109
+ inTable = false;
116110
+ continue;
116111
+ }
116112
+ if (!trimmed.startsWith("|"))
116113
+ continue;
116114
+ const cells = splitTableCells2(trimmed);
116115
+ if (!inTable && cells.length >= 4) {
116116
+ const h0 = (cells[0] ?? "").toLowerCase();
116117
+ const h1 = (cells[1] ?? "").toLowerCase();
116118
+ const h2 = (cells[2] ?? "").toLowerCase();
116119
+ if ((h0 === "ac" || h0.includes("acceptance")) && h1.includes("status") && h2.includes("evidence")) {
116120
+ inTable = true;
116121
+ continue;
116122
+ }
116123
+ }
116124
+ if (/^[-:]+$/.test(cells[0] ?? ""))
116125
+ continue;
116126
+ if (inTable && cells.length >= 3) {
116127
+ const id = cells[0] ?? "";
116128
+ const statusRaw = cells[1] ?? "";
116129
+ const evidenceTypeRaw = cells[2] ?? "";
116130
+ const status = normalizeAcceptanceCriteriaStatus(statusRaw);
116131
+ const evidenceType = normalizeEvidenceType2(evidenceTypeRaw);
116132
+ const acShaped = cells.length >= 4 || id.length > 0 && status !== null;
116133
+ const evidence = cells[3] ?? "";
116134
+ if (acShaped && status !== null && evidenceType !== null && id.length > 0) {
116135
+ rows.push({ id, status, evidenceType, evidence });
116136
+ } else if (acShaped && id.length > 0) {
116137
+ dropped.push(evidenceType === null && status !== null ? `${id} (unrecognised evidence type "${evidenceTypeRaw}")` : `${id} (unrecognised status "${statusRaw}")`);
116138
+ }
116139
+ }
116140
+ }
116141
+ return { rows, dropped };
116142
+ }
116143
+ function normalizeAcceptanceCriteriaStatus(raw) {
116144
+ const upper = raw.toUpperCase();
116145
+ if (/\bMET\b/.test(upper))
116146
+ return "MET";
116147
+ if (/\bPARTIAL\b/.test(upper))
116148
+ return "PARTIAL";
116149
+ if (/\bUNMET\b/.test(upper))
116150
+ return "UNMET";
116151
+ if (/\bN\/A\b/.test(upper) || /\bNA\b/.test(upper))
116152
+ return "N/A";
116153
+ return null;
116154
+ }
116155
+ function normalizeEvidenceTypeToken2(normalized) {
116156
+ if (normalized === "test")
116157
+ return "test";
116158
+ if (normalized === "command")
116159
+ return "command";
116160
+ if (normalized === "static-ref" || normalized === "static" || normalized === "doc" || normalized === "docs" || normalized === "documentation") {
116161
+ return "static-ref";
116162
+ }
116163
+ if (normalized === "manual-review" || normalized === "manual")
116164
+ return "manual-review";
116165
+ if (normalized === "llm-judge" || normalized === "judge")
116166
+ return "llm-judge";
116167
+ if (normalized === "n/a" || normalized === "na")
116168
+ return "n/a";
116169
+ return null;
116170
+ }
116171
+ function normalizeEvidenceType2(raw) {
116172
+ const normalized = raw.toLowerCase().trim();
116173
+ const single = normalizeEvidenceTypeToken2(normalized);
116174
+ if (single !== null)
116175
+ return single;
116176
+ const tokens = normalized.split(/[+,/]/).map((part) => normalizeEvidenceTypeToken2(part.trim())).filter((token) => token !== null);
116177
+ if (tokens.length < 2 || tokens.length !== normalized.split(/[+,/]/).filter((p) => p.trim()).length) {
116178
+ return null;
116179
+ }
116180
+ return EVIDENCE_TYPE_PRECEDENCE2.find((candidate) => tokens.includes(candidate)) ?? null;
116181
+ }
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) {
116196
+ return rows.map((row) => {
116197
+ if (row.status === "MET" && requiresExecutableEvidence(row.id, exempt) && row.evidenceType !== "test" && row.evidenceType !== "command") {
116198
+ return { ...row, status: "PARTIAL" };
116199
+ }
116200
+ return row;
116201
+ });
116202
+ }
116203
+ function requiresExecutableEvidence(id, exempt) {
116204
+ return !exempt.has(normalizeTitle(id));
116205
+ }
116206
+ function extractChecks(_text, taskCheckPassed, requirements, acceptanceCriteria, droppedAcRows = [], exempt = new Set) {
116207
+ const checks = [
116208
+ {
116209
+ name: "spur task check",
116210
+ status: taskCheckPassed ? "pass" : "fail",
116211
+ evidence: taskCheckPassed ? "task check passed" : "task check failed"
116212
+ }
116213
+ ];
116214
+ const lines = _text.split(`
116215
+ `);
116216
+ let inChecks = false;
116217
+ for (const line of lines) {
116218
+ const trimmed = line.trim();
116219
+ if (/^#{1,6}\s/.test(trimmed))
116220
+ inChecks = false;
116221
+ if (!trimmed.startsWith("|"))
116222
+ continue;
116223
+ const cells = splitTableCells2(trimmed);
116224
+ if (cells.length >= 2) {
116225
+ const h0 = (cells[0] ?? "").toLowerCase();
116226
+ const h1 = (cells[1] ?? "").toLowerCase();
116227
+ if ((h0 === "check" || h0 === "name") && h1 === "status") {
116228
+ inChecks = true;
116229
+ continue;
116230
+ }
116231
+ if (inChecks && cells.length >= 3 && !/^[-:]+$/.test(cells[0] ?? "")) {
116232
+ checks.push({
116233
+ name: cells[0] ?? "",
116234
+ status: (cells[1] ?? "").toLowerCase(),
116235
+ evidence: cells[2] ?? ""
116236
+ });
116237
+ }
116238
+ }
116239
+ }
116240
+ if (droppedAcRows.length > 0) {
116241
+ checks.push({
116242
+ name: "ac-row-dropped",
116243
+ status: "fail",
116244
+ evidence: `${droppedAcRows.length} AC row(s) could not be parsed and were omitted from the verdict: ` + `${droppedAcRows.join("; ")}. Accepted evidence types: test, command, static-ref (aliases: ` + "static, doc, docs, documentation), manual-review, llm-judge, n/a. Accepted statuses: " + "MET, PARTIAL, UNMET, N/A."
116245
+ });
116246
+ }
116247
+ const hollowMet = [
116248
+ ...requirements.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id),
116249
+ ...acceptanceCriteria.filter((r) => r.status === "MET" && r.evidence.trim() === "").map((r) => r.id)
116250
+ ];
116251
+ if (hollowMet.length > 0) {
116252
+ checks.push({
116253
+ name: "hollow-met-evidence",
116254
+ status: "fail",
116255
+ severity: "major",
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.`
116257
+ });
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
+ }
116268
+ if (acceptanceCriteria.length > 0) {
116269
+ const downgraded = acceptanceCriteria.filter((row) => row.status === "PARTIAL" && requiresExecutableEvidence(row.id, exempt) && row.evidenceType !== "test" && row.evidenceType !== "command");
116270
+ checks.push({
116271
+ name: downgraded.length > 0 ? "evidence-rule-failed" : "evidence-rule-pass",
116272
+ status: downgraded.length > 0 ? "fail" : "pass",
116273
+ evidence: downgraded.length > 0 ? `Executable evidence missing for: ${downgraded.map((row) => row.id).join(", ")}` : "All behavior-bearing AC rows have executable evidence or are explicitly non-behavioral."
116274
+ });
116275
+ }
116276
+ return checks;
116277
+ }
116278
+ function aggregateBatchVerdicts(results) {
116279
+ const notStarted = results.filter((r) => r.outcome === "NOT-STARTED");
116280
+ const rolledUp = results.filter((r) => r.outcome !== "NOT-STARTED");
116281
+ const counts = {
116282
+ PASS: rolledUp.filter((r) => r.outcome === "PASS").length,
116283
+ PARTIAL: rolledUp.filter((r) => r.outcome === "PARTIAL").length,
116284
+ FAIL: rolledUp.filter((r) => r.outcome === "FAIL").length,
116285
+ UNKNOWN: rolledUp.filter((r) => r.outcome === "UNKNOWN").length
116286
+ };
116287
+ let verdict;
116288
+ if (rolledUp.length === 0) {
116289
+ verdict = "UNKNOWN";
116290
+ } else if (counts.FAIL > 0) {
116291
+ verdict = "FAIL";
116292
+ } else if (counts.PARTIAL > 0 || counts.UNKNOWN > 0) {
116293
+ verdict = "PARTIAL";
116294
+ } else {
116295
+ verdict = "PASS";
116296
+ }
116297
+ const summary = `${counts.PASS} PASS, ${counts.PARTIAL} PARTIAL, ${counts.FAIL} FAIL, ${notStarted.length} NOT-STARTED (excluded)`;
116298
+ return { verdict, rolledUp, notStarted, summary };
116299
+ }
116300
+ var EVIDENCE_TYPE_PRECEDENCE2, EXEMPTION_TAG_RE;
116301
+ var init_task_verdict = __esm(() => {
116302
+ init_src2();
116303
+ init_verify_verdict();
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;
116306
+ });
116307
+
116318
116308
  // ../../packages/app/src/services/token-ledger-service.ts
116319
116309
  import { closeSync as closeSync6, existsSync as existsSync19, fstatSync as fstatSync2, openSync as openSync6, readSync } from "fs";
116320
116310
  import { join as join58 } from "path";
@@ -131528,7 +131518,8 @@ Recover with: ${err.recoveryCommand}` : err.message;
131528
131518
  return;
131529
131519
  }
131530
131520
  const taskCheckPassed = true;
131531
- const result = deriveVerdict(answerText, taskCheckPassed);
131521
+ const taskAc = await readTaskAcceptanceCriteria(wbs, corpusDeps);
131522
+ const result = deriveVerdict(answerText, taskCheckPassed, taskAc);
131532
131523
  const gap = await verdictScenarioKeyGap(wbs, [...result.requirements, ...result.acceptanceCriteria ?? []], corpusDeps);
131533
131524
  if (gap !== null) {
131534
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})`);
@@ -132725,8 +132716,9 @@ ${prompt}`;
132725
132716
  };
132726
132717
  }
132727
132718
  function drainAgentSelector(spec, context) {
132728
- if (spec.executor === undefined)
132729
- 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
+ }
132730
132722
  try {
132731
132723
  resolveExecutor(spec.executor, context.agentConfig, { isCanonicalAgent: isAgentName });
132732
132724
  } catch (error) {
@@ -134429,7 +134421,7 @@ import { createRequire as createRequire3 } from "module";
134429
134421
  var CLI_CONFIG = {
134430
134422
  binaryName: "spur",
134431
134423
  binaryLabel: "spur",
134432
- binaryVersion: "0.4.4",
134424
+ binaryVersion: "0.4.6",
134433
134425
  configDir: ".spur",
134434
134426
  configFile: ".spur/config.yaml",
134435
134427
  databaseFile: ".spur/spur.db"
@@ -142535,14 +142527,13 @@ async function serveModuleAsset(c, modules, fs) {
142535
142527
  const target = resolve27(declared.root, relative);
142536
142528
  if (!isInsideRoot(declared.root, target))
142537
142529
  return refusal(403, "Forbidden path");
142530
+ if (!(await fs.stat(target))?.isFile())
142531
+ return refusal(404, "Not Found");
142538
142532
  const realRoot = resolvedOrUndefined(fs, declared.root);
142539
- const resolvedTarget = resolvedOrUndefined(fs, target);
142540
- if (realRoot !== undefined && resolvedTarget !== undefined && !isInsideRoot(realRoot, resolvedTarget)) {
142533
+ const realTarget = resolvedOrUndefined(fs, target);
142534
+ if (realRoot === undefined || realTarget === undefined || !isInsideRoot(realRoot, realTarget)) {
142541
142535
  return refusal(403, "Forbidden path");
142542
142536
  }
142543
- const realTarget = resolvedTarget ?? target;
142544
- if (!(await fs.stat(realTarget))?.isFile())
142545
- return refusal(404, "Not Found");
142546
142537
  const file = Bun.file(realTarget);
142547
142538
  const headers = new Headers({
142548
142539
  "content-type": file.type || "application/octet-stream",
@@ -143057,7 +143048,7 @@ await init_src3();
143057
143048
  import { homedir as homedir14 } from "os";
143058
143049
  import { basename as basename20, join as join70, relative as relative14, resolve as resolve28 } from "path";
143059
143050
  // ../server/package.json
143060
- var version3 = "0.4.4";
143051
+ var version3 = "0.4.6";
143061
143052
 
143062
143053
  // ../server/src/modules/health/index.ts
143063
143054
  var startedAt = Date.now();