specrails-core 5.3.0 → 5.5.0

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 (156) hide show
  1. package/README.md +2 -0
  2. package/dist/agent-runtime/capabilities.d.ts +28 -0
  3. package/dist/agent-runtime/capabilities.js +81 -0
  4. package/dist/agent-runtime/capabilities.js.map +1 -0
  5. package/dist/agent-runtime/cli-executor.d.ts +9 -0
  6. package/dist/agent-runtime/cli-executor.js +109 -34
  7. package/dist/agent-runtime/cli-executor.js.map +1 -1
  8. package/dist/agent-runtime/cli-process.d.ts +1 -0
  9. package/dist/agent-runtime/cli-process.js +13 -2
  10. package/dist/agent-runtime/cli-process.js.map +1 -1
  11. package/dist/agent-runtime/cli.d.ts +1 -0
  12. package/dist/agent-runtime/cli.js +60 -11
  13. package/dist/agent-runtime/cli.js.map +1 -1
  14. package/dist/agent-runtime/codex-schema.d.ts +4 -0
  15. package/dist/agent-runtime/codex-schema.js +31 -0
  16. package/dist/agent-runtime/codex-schema.js.map +1 -0
  17. package/dist/agent-runtime/compact/architect.d.ts +349 -0
  18. package/dist/agent-runtime/compact/architect.js +371 -0
  19. package/dist/agent-runtime/compact/architect.js.map +1 -0
  20. package/dist/agent-runtime/compact/chat-client.d.ts +83 -0
  21. package/dist/agent-runtime/compact/chat-client.js +307 -0
  22. package/dist/agent-runtime/compact/chat-client.js.map +1 -0
  23. package/dist/agent-runtime/compact/developer.d.ts +160 -0
  24. package/dist/agent-runtime/compact/developer.js +823 -0
  25. package/dist/agent-runtime/compact/developer.js.map +1 -0
  26. package/dist/agent-runtime/compact/environment.d.ts +62 -0
  27. package/dist/agent-runtime/compact/environment.js +213 -0
  28. package/dist/agent-runtime/compact/environment.js.map +1 -0
  29. package/dist/agent-runtime/compact/exit-code-honesty.d.ts +8 -0
  30. package/dist/agent-runtime/compact/exit-code-honesty.js +28 -0
  31. package/dist/agent-runtime/compact/exit-code-honesty.js.map +1 -0
  32. package/dist/agent-runtime/compact/guarded-loop.d.ts +73 -0
  33. package/dist/agent-runtime/compact/guarded-loop.js +320 -0
  34. package/dist/agent-runtime/compact/guarded-loop.js.map +1 -0
  35. package/dist/agent-runtime/compact/prompt-inputs.d.ts +42 -0
  36. package/dist/agent-runtime/compact/prompt-inputs.js +59 -0
  37. package/dist/agent-runtime/compact/prompt-inputs.js.map +1 -0
  38. package/dist/agent-runtime/compact/reviewer.d.ts +24 -0
  39. package/dist/agent-runtime/compact/reviewer.js +178 -0
  40. package/dist/agent-runtime/compact/reviewer.js.map +1 -0
  41. package/dist/agent-runtime/compact/step.d.ts +70 -0
  42. package/dist/agent-runtime/compact/step.js +136 -0
  43. package/dist/agent-runtime/compact/step.js.map +1 -0
  44. package/dist/agent-runtime/compact/test-reachability.d.ts +30 -0
  45. package/dist/agent-runtime/compact/test-reachability.js +147 -0
  46. package/dist/agent-runtime/compact/test-reachability.js.map +1 -0
  47. package/dist/agent-runtime/config.d.ts +4 -0
  48. package/dist/agent-runtime/config.js +146 -15
  49. package/dist/agent-runtime/config.js.map +1 -1
  50. package/dist/agent-runtime/core-host.d.ts +12 -4
  51. package/dist/agent-runtime/core-host.js +61 -21
  52. package/dist/agent-runtime/core-host.js.map +1 -1
  53. package/dist/agent-runtime/efficiency-summary.d.ts +67 -0
  54. package/dist/agent-runtime/efficiency-summary.js +52 -0
  55. package/dist/agent-runtime/efficiency-summary.js.map +1 -0
  56. package/dist/agent-runtime/efficiency-types.d.ts +60 -0
  57. package/dist/agent-runtime/efficiency-types.js +8 -0
  58. package/dist/agent-runtime/efficiency-types.js.map +1 -0
  59. package/dist/agent-runtime/efficiency.d.ts +5 -0
  60. package/dist/agent-runtime/efficiency.js +35 -0
  61. package/dist/agent-runtime/efficiency.js.map +1 -0
  62. package/dist/agent-runtime/evaluation-corpus.d.ts +13 -0
  63. package/dist/agent-runtime/evaluation-corpus.js +8 -0
  64. package/dist/agent-runtime/evaluation-corpus.js.map +1 -0
  65. package/dist/agent-runtime/evaluation.d.ts +43 -0
  66. package/dist/agent-runtime/evaluation.js +171 -0
  67. package/dist/agent-runtime/evaluation.js.map +1 -0
  68. package/dist/agent-runtime/executor-types.d.ts +71 -4
  69. package/dist/agent-runtime/executor-types.js +7 -2
  70. package/dist/agent-runtime/executor-types.js.map +1 -1
  71. package/dist/agent-runtime/executors.d.ts +1 -0
  72. package/dist/agent-runtime/executors.js +4 -0
  73. package/dist/agent-runtime/executors.js.map +1 -1
  74. package/dist/agent-runtime/graph/artifacts.d.ts +8 -22
  75. package/dist/agent-runtime/graph/artifacts.js +99 -73
  76. package/dist/agent-runtime/graph/artifacts.js.map +1 -1
  77. package/dist/agent-runtime/graph/nodes.d.ts +7 -2
  78. package/dist/agent-runtime/graph/nodes.js +207 -53
  79. package/dist/agent-runtime/graph/nodes.js.map +1 -1
  80. package/dist/agent-runtime/graph/roles.d.ts +11 -3
  81. package/dist/agent-runtime/graph/roles.js +109 -37
  82. package/dist/agent-runtime/graph/roles.js.map +1 -1
  83. package/dist/agent-runtime/graph/state.d.ts +10 -1
  84. package/dist/agent-runtime/graph/state.js +2 -1
  85. package/dist/agent-runtime/graph/state.js.map +1 -1
  86. package/dist/agent-runtime/guardrails.d.ts +15 -0
  87. package/dist/agent-runtime/guardrails.js +76 -0
  88. package/dist/agent-runtime/guardrails.js.map +1 -0
  89. package/dist/agent-runtime/index.d.ts +7 -1
  90. package/dist/agent-runtime/index.js +7 -1
  91. package/dist/agent-runtime/index.js.map +1 -1
  92. package/dist/agent-runtime/kimi-acp.d.ts +4 -0
  93. package/dist/agent-runtime/kimi-acp.js +20 -9
  94. package/dist/agent-runtime/kimi-acp.js.map +1 -1
  95. package/dist/agent-runtime/openai-executor.d.ts +33 -1
  96. package/dist/agent-runtime/openai-executor.js +76 -113
  97. package/dist/agent-runtime/openai-executor.js.map +1 -1
  98. package/dist/agent-runtime/openspec-tool-server.d.ts +1 -0
  99. package/dist/agent-runtime/openspec-tool-server.js +37 -0
  100. package/dist/agent-runtime/openspec-tool-server.js.map +1 -0
  101. package/dist/agent-runtime/openspec.d.ts +198 -0
  102. package/dist/agent-runtime/openspec.js +329 -0
  103. package/dist/agent-runtime/openspec.js.map +1 -0
  104. package/dist/agent-runtime/prompts.d.ts +24 -1
  105. package/dist/agent-runtime/prompts.js +112 -43
  106. package/dist/agent-runtime/prompts.js.map +1 -1
  107. package/dist/agent-runtime/provider-diagnostic.d.ts +2 -0
  108. package/dist/agent-runtime/provider-diagnostic.js +25 -0
  109. package/dist/agent-runtime/provider-diagnostic.js.map +1 -0
  110. package/dist/agent-runtime/repository-context.d.ts +25 -0
  111. package/dist/agent-runtime/repository-context.js +127 -0
  112. package/dist/agent-runtime/repository-context.js.map +1 -0
  113. package/dist/agent-runtime/review-context.d.ts +14 -0
  114. package/dist/agent-runtime/review-context.js +41 -0
  115. package/dist/agent-runtime/review-context.js.map +1 -0
  116. package/dist/agent-runtime/role-routing.d.ts +10 -0
  117. package/dist/agent-runtime/role-routing.js +30 -0
  118. package/dist/agent-runtime/role-routing.js.map +1 -0
  119. package/dist/agent-runtime/role-state.d.ts +19 -0
  120. package/dist/agent-runtime/role-state.js +24 -0
  121. package/dist/agent-runtime/role-state.js.map +1 -0
  122. package/dist/agent-runtime/runtime-identity.d.ts +11 -0
  123. package/dist/agent-runtime/runtime-identity.js +33 -0
  124. package/dist/agent-runtime/runtime-identity.js.map +1 -0
  125. package/dist/agent-runtime/tool-event.d.ts +3 -0
  126. package/dist/agent-runtime/tool-event.js +24 -0
  127. package/dist/agent-runtime/tool-event.js.map +1 -0
  128. package/dist/agent-runtime/verification-plan.d.ts +55 -0
  129. package/dist/agent-runtime/verification-plan.js +206 -0
  130. package/dist/agent-runtime/verification-plan.js.map +1 -0
  131. package/dist/agent-runtime/workflow-types.d.ts +19 -1
  132. package/dist/agent-runtime/workflow.js +29 -2
  133. package/dist/agent-runtime/workflow.js.map +1 -1
  134. package/dist/agent-runtime/workspace-tools.d.ts +4 -0
  135. package/dist/agent-runtime/workspace-tools.js +182 -20
  136. package/dist/agent-runtime/workspace-tools.js.map +1 -1
  137. package/dist/installer/phases/scaffold.js +39 -109
  138. package/dist/installer/phases/scaffold.js.map +1 -1
  139. package/dist/installer/runtime/pipeline-state.d.ts +143 -1
  140. package/dist/installer/runtime/pipeline-state.js +510 -30
  141. package/dist/installer/runtime/pipeline-state.js.map +1 -1
  142. package/docs/agent-runtime-efficiency.md +65 -0
  143. package/docs/agent-runtime.md +86 -15
  144. package/integration-contract.json +2 -1
  145. package/package.json +5 -2
  146. package/schemas/agent-runtime.schema.json +34 -4
  147. package/schemas/fixtures/runtime-efficiency-summary.v1.json +790 -0
  148. package/templates/codex-skills/batch-implement/SKILL.md +33 -58
  149. package/templates/codex-skills/implement/SKILL.md +21 -124
  150. package/templates/codex-skills/retry/SKILL.md +8 -34
  151. package/templates/commands/specrails/batch-implement.md +21 -16
  152. package/templates/commands/specrails/implement.md +17 -276
  153. package/templates/commands/specrails/retry.md +6 -34
  154. package/templates/gemini-commands/batch-implement.toml +34 -28
  155. package/templates/gemini-commands/implement.toml +34 -55
  156. package/templates/gemini-commands/retry.toml +10 -16
@@ -0,0 +1,178 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { artifactPath } from '../openspec.js';
5
+ import { REVIEW_OUTPUT_SCHEMA } from '../prompts.js';
6
+ import { REVIEW_ASPECTS } from '../graph/review-policy.js';
7
+ import { hasBinaryAssets } from './architect.js';
8
+ import { bounded } from './prompt-inputs.js';
9
+ import { finalJson, openspecCall, strings, text, toolStep } from './step.js';
10
+ /** Tool calls the reviewer may spend inspecting beyond the supplied diff. */
11
+ export const DEFAULT_REVIEW_TOOL_BUDGET = 15;
12
+ /** A re-review only opens the changed files. */
13
+ export const RE_REVIEW_TOOL_BUDGET = 6;
14
+ const DIFF_LIMIT = 40 * 1024;
15
+ const UNTRACKED_LIMIT = 8 * 1024;
16
+ const MAX_UNTRACKED = 12;
17
+ /** Working-tree changes of every allowed root against HEAD, plus the content of new files, bounded. `only` restricts the diff and the new-file listing to those repository-relative paths (a re-review reads just what the fixer touched). */
18
+ export function collectDiff(roots, only) {
19
+ // Pathspec magic stays enabled so openspec/ artifacts are excluded from the reviewed diff.
20
+ const env = { ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_'))), GIT_OPTIONAL_LOCKS: '0' };
21
+ const options = { encoding: 'utf8', env, windowsHide: true, timeout: 10_000, maxBuffer: 4 * 1024 * 1024 };
22
+ const sections = [];
23
+ let remaining = DIFF_LIMIT;
24
+ for (const root of roots) {
25
+ const git = ['-c', 'core.fsmonitor=false', '-C', root];
26
+ const scope = only?.length ? only.filter(file => !file.startsWith('openspec/')) : ['.'];
27
+ if (only && !scope.length) {
28
+ sections.push(`### ${root}\n(no reviewable files changed since the previous verdict)`);
29
+ continue;
30
+ }
31
+ const diff = spawnSync('git', [...git, 'diff', '--no-ext-diff', '--no-textconv', '--no-color', 'HEAD', '--', ...scope, ...(only ? [] : [':(exclude)openspec'])], options);
32
+ if (diff.error || diff.status !== 0) {
33
+ sections.push(`### ${root}\nGit diff unavailable for this repository; inspect files with the tools.`);
34
+ continue;
35
+ }
36
+ const untracked = spawnSync('git', [...git, 'ls-files', '--others', '--exclude-standard', '--', ...scope], options);
37
+ const files = untracked.status === 0 ? untracked.stdout.split('\n').map(line => line.trim()).filter(line => line && !line.startsWith('openspec/')) : [];
38
+ let body = diff.stdout.length > remaining ? diff.stdout.slice(0, remaining) + '\n…(diff truncated)' : diff.stdout;
39
+ remaining = Math.max(0, remaining - body.length);
40
+ for (const file of files.slice(0, MAX_UNTRACKED)) {
41
+ let content = '';
42
+ try {
43
+ content = readFileSync(path.join(root, file), 'utf8');
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ if (content.includes('\0'))
49
+ continue;
50
+ const shown = content.length > UNTRACKED_LIMIT ? content.slice(0, UNTRACKED_LIMIT) + '\n…(file truncated)' : content;
51
+ if (remaining <= 0) {
52
+ body += `\n+++ new file: ${file} (omitted: diff budget spent)`;
53
+ continue;
54
+ }
55
+ body += `\n+++ new file: ${file}\n${shown}`;
56
+ remaining = Math.max(0, remaining - shown.length);
57
+ }
58
+ if (files.length > MAX_UNTRACKED)
59
+ body += `\n(${files.length - MAX_UNTRACKED} more new files not shown)`;
60
+ sections.push(`### ${root}\n${body.trim() || '(no changes)'}`);
61
+ }
62
+ return sections.join('\n\n');
63
+ }
64
+ /** The gate thresholds the host rendered into the reviewer prompt (`score is at least N`, `aspect ≥ N`). */
65
+ export function reviewGateThresholds(gate) {
66
+ const minScore = Number(/score\` is at least (\d+)/.exec(gate)?.[1] ?? 0);
67
+ const aspects = {};
68
+ for (const match of gate.matchAll(/`([a-z_]+)` ≥ (\d+)/g))
69
+ aspects[match[1]] = Number(match[2]);
70
+ return { minScore, aspects };
71
+ }
72
+ /** Files named by the previous review's issues (`path: what must change` lines under `## Previous review`). */
73
+ export function previousIssueFiles(feedback) {
74
+ const section = feedback.split('## Previous review')[1] ?? '';
75
+ return [...new Set([...section.matchAll(/(?:^|[\s-])((?:[\w.-]+\/)*[\w.-]+\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|swift|cs|rb|php|c|cc|cpp|h|hpp|vue|svelte|html|css|scss|json|ya?ml|md))(?=[\s:,;)]|$)/g)].map(match => match[1].replace(/\\/g, '/')))];
76
+ }
77
+ function readArtifact(status, name) {
78
+ try {
79
+ return readFileSync(artifactPath(status.changeRoot, name), 'utf8');
80
+ }
81
+ catch {
82
+ return '';
83
+ }
84
+ }
85
+ function clampScore(value) { return typeof value === 'number' && Number.isFinite(value) ? Math.min(100, Math.max(0, Math.round(value))) : 0; }
86
+ /**
87
+ * The compact reviewer: the host supplies the diff and the frozen criteria, the
88
+ * model inspects read-only within a small budget, and the verdict is forced
89
+ * into REVIEW_OUTPUT_SCHEMA. Missing criteria are recorded as pending, never
90
+ * invented as met.
91
+ */
92
+ export async function runCompactReviewer(env, options = {}) {
93
+ const loaded = await openspecCall(env, { action: 'load_skill' });
94
+ const status = loaded.planning?.status ?? await openspecCall(env, { action: 'status' });
95
+ const criteria = env.inputs.criteria;
96
+ // A re-review after a correction round: the diff covers only the files the
97
+ // fixer touched and the stance is "settle your previous issues", not "review
98
+ // the candidate again" (observed: three full passes, the second one adding
99
+ // an issue to code verify had just accepted).
100
+ const reReview = env.inputs.reReviewChanges;
101
+ const diff = collectDiff(env.toolset.roots, reReview.length ? reReview.map(item => item.path) : undefined);
102
+ const stance = reReview.length
103
+ ? `Re-review an implementation read-only after a correction round. Your previous review and its issues are supplied; the diff shows ONLY the files changed since then. For each previous issue state whether it is resolved; raise a new issue only for a regression inside these changed lines — never for code you already accepted. Certify every listed criterion: keep the ones you certified as met last time unless a changed file affects them. Use the tools only for the changed files. Do not modify anything.`
104
+ : `Review an implementation read-only. The diff and the acceptance criteria are supplied; use the tools only to open files the diff references when the diff alone is not enough. Do not modify anything. Judge behaviour against the acceptance criteria, not the code's shape against the plan's wording: module layout, file lists, class/function/SFX names and techniques named in the ticket, design or contract layer are suggestions — a working implementation that meets a criterion another way is correct, and "rename/move/rewrite to match the plan" is never an issue (observed: six such issues, zero real defects).`;
105
+ if (reReview.length)
106
+ env.onEvent?.({ kind: 'text', text: `Compact reviewer: re-review of ${reReview.length} changed file${reReview.length === 1 ? '' : 's'} (${reReview.map(item => item.path).slice(0, 6).join(', ')}${reReview.length > 6 ? ', …' : ''}).` });
107
+ const changed = new Set(reReview.map(item => item.path.replace(/\\/g, '/')));
108
+ // Reviewers are not told about binary assets (the architect is): without
109
+ // this fact a spec that mentions "assets/audio/*.wav" makes the reviewer
110
+ // demand files nobody can produce and reject runtime synthesis (observed:
111
+ // "replace oscillator synthesis with static assets; create placeholder .wav
112
+ // files" — issues the fixer cannot satisfy, score 60 after a green verify).
113
+ const assetsFact = hasBinaryAssets(env.toolset.roots) ? '' : ' The repository contains no binary assets (audio, images, fonts) and none can be added — the implementer has no shell; effects produced at runtime by code (Web Audio API, canvas) SATISFY any spec wording about sound or image files, and asking for asset files is not a valid issue.';
114
+ const loop = await toolStep(env, {
115
+ system: `${stance}${assetsFact} Finish with one JSON object matching the review contract: {"approved":boolean,"summary":"","issues":["file: what must change"],"score":0-100,"aspects":{${REVIEW_ASPECTS.map(name => `"${name}":0-100`).join(',')}},"acceptance":{"criteria":[{"specId":"<id>","criterionIndex":0,"status":"met|blocked|pending","evidence":["file or test proving it"]}],"checks":[],"findings":[]}}. Certify EVERY listed criterion by its exact specId and criterionIndex; use "met" only when the diff proves it. ${env.inputs.reviewGate}`,
116
+ user: [
117
+ `Requested work (frozen scope):\n${bounded(env.inputs.scope, 2500)}`,
118
+ `Acceptance criteria to certify:\n${criteria.map(item => `- specId "${item.specId}", criterionIndex ${item.criterionIndex}: ${item.requirement}`).join('\n') || '- (none listed)'}`,
119
+ `tasks.md:\n${bounded(readArtifact(status, 'tasks.md'), 2000)}`,
120
+ ...(env.inputs.developerSummary ? [`Developer summary (a claim to verify):\n${bounded(env.inputs.developerSummary, 2500)}`] : []),
121
+ ...(env.inputs.feedback ? [`Verification result from the host:\n${bounded(env.inputs.feedback, 3000)}`] : []),
122
+ reReview.length ? `Diff of the files changed since your previous verdict:\n${diff}` : `Diff against HEAD:\n${diff}`,
123
+ ].join('\n\n'),
124
+ tools: ['read_file', 'list_files'], maxToolCalls: options.reviewToolBudget ?? (reReview.length ? RE_REVIEW_TOOL_BUDGET : DEFAULT_REVIEW_TOOL_BUDGET),
125
+ // A re-review reads the changed files and nothing else: the instruction
126
+ // alone did not hold (observed: every file re-read, then new objections).
127
+ ...(reReview.length ? { execute: async (name, args) => {
128
+ if (name === 'read_file' && typeof args.path === 'string' && !changed.has(args.path.replace(/\\/g, '/')))
129
+ return JSON.stringify({ error: `"${args.path}" did not change since your previous verdict; this re-review covers only ${[...changed].join(', ')}. Judge the previous issues on those files and reply.` });
130
+ return env.toolset.execute(name, args);
131
+ } } : {}),
132
+ });
133
+ const raw = await finalJson(env, 'review', loop.messages, loop.text, REVIEW_OUTPUT_SCHEMA, value => typeof value.approved === 'boolean' && text(value.summary) ? undefined : '"approved" must be a boolean and "summary" a non-empty string');
134
+ const aspectsRaw = raw.aspects && typeof raw.aspects === 'object' && !Array.isArray(raw.aspects) ? raw.aspects : {};
135
+ const acceptanceRaw = raw.acceptance && typeof raw.acceptance === 'object' && !Array.isArray(raw.acceptance) ? raw.acceptance : {};
136
+ const rows = (Array.isArray(acceptanceRaw.criteria) ? acceptanceRaw.criteria : []).flatMap(item => {
137
+ const entry = item && typeof item === 'object' && !Array.isArray(item) ? item : undefined;
138
+ if (!entry)
139
+ return [];
140
+ const evidence = strings(entry.evidence, 10, 500);
141
+ const state = ['met', 'blocked', 'pending'].includes(String(entry.status)) ? String(entry.status) : 'pending';
142
+ return [{ specId: String(entry.specId), criterionIndex: Number(entry.criterionIndex), status: state, evidence: evidence.length ? evidence : ['No evidence given by the reviewer'] }];
143
+ });
144
+ let issues = strings(raw.issues, 50, 1000);
145
+ // On a re-review the host owns the scope: an issue that names none of the
146
+ // changed files and none of the previous issues' files is a NEW objection to
147
+ // code the reviewer already accepted — dropped, logged, never sent to the fixer.
148
+ if (reReview.length) {
149
+ const previous = previousIssueFiles(env.inputs.feedback);
150
+ const allowed = [...changed, ...previous];
151
+ const kept = issues.filter(issue => allowed.some(file => issue.includes(file) || issue.includes(path.basename(file))));
152
+ const dropped = issues.filter(issue => !kept.includes(issue));
153
+ if (dropped.length)
154
+ env.onEvent?.({ kind: 'text', text: `Compact reviewer: dropped ${dropped.length} re-review issue${dropped.length === 1 ? '' : 's'} about code that did not change since the previous verdict (${dropped.map(item => JSON.stringify(item.slice(0, 80))).join('; ')}).` });
155
+ issues = kept;
156
+ }
157
+ const certified = criteria.map(item => rows.find(row => row.specId === item.specId && row.criterionIndex === item.criterionIndex) ?? { specId: item.specId, criterionIndex: item.criterionIndex, status: 'pending', evidence: ['The reviewer did not certify this criterion'] });
158
+ const uncertified = certified.filter(row => row.status !== 'met').length;
159
+ if (uncertified && raw.approved === true)
160
+ issues.push(`Review incomplete: ${uncertified} acceptance criteri${uncertified === 1 ? 'on' : 'a'} not certified as met.`);
161
+ // After the host filter a re-review with no surviving issue and every
162
+ // criterion met IS an approval, whatever the model's own flag or score said:
163
+ // a fix round cannot make an already-reviewed candidate worse, and the
164
+ // score of a reviewer that was refused the files it wanted is not evidence.
165
+ const resolved = reReview.length > 0 && !uncertified && issues.length === 0;
166
+ const gate = resolved ? reviewGateThresholds(env.inputs.reviewGate) : undefined;
167
+ const lift = (value, floor) => floor === undefined ? clampScore(value) : Math.max(clampScore(value), floor);
168
+ if (resolved)
169
+ env.onEvent?.({ kind: 'text', text: 'Compact reviewer: every previous issue is settled and every criterion certified; the re-review approves.' });
170
+ const structured = {
171
+ approved: (raw.approved === true || resolved) && !uncertified && issues.length === 0,
172
+ summary: text(raw.summary).slice(0, 8000), issues, score: lift(raw.score, gate?.minScore),
173
+ aspects: Object.fromEntries(REVIEW_ASPECTS.map(name => [name, lift(aspectsRaw[name], gate?.aspects[name])])),
174
+ acceptance: { criteria: certified, checks: [], findings: strings(acceptanceRaw.findings, 50, 1000) },
175
+ };
176
+ return { text: JSON.stringify(structured), usage: env.client.usage, structured };
177
+ }
178
+ //# sourceMappingURL=reviewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewer.js","sourceRoot":"","sources":["../../../src/agent-runtime/compact/reviewer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import { type GuardrailId, type GuardrailSettings } from '../guardrails.js';
2
+ import { type AgentEvent, type AgentRequest } from '../executor-types.js';
3
+ import type { OpenSpecTools } from '../openspec.js';
4
+ import type { WorkspaceTools } from '../workspace-tools.js';
5
+ import { type ChatClient, type ChatMessage } from './chat-client.js';
6
+ import { type ToolLoopResult } from './guarded-loop.js';
7
+ import type { PromptInputs } from './prompt-inputs.js';
8
+ /** Everything one compact role pipeline needs; built once per `execute`. */
9
+ export interface CompactEnv {
10
+ client: ChatClient;
11
+ request: AgentRequest;
12
+ inputs: PromptInputs;
13
+ toolset: WorkspaceTools;
14
+ openspec: OpenSpecTools;
15
+ contextWindowTokens: number;
16
+ /** Connection-declared output budget per tool turn (see ToolLoopOptions.maxOutputTokens). */
17
+ maxOutputTokens?: number;
18
+ /** Re-arms the role's wall-clock budget: the compact developer calls it at every task group so the timeout is per group, not per role. */
19
+ resetDeadline?: () => void;
20
+ signal: AbortSignal;
21
+ onEvent?: (event: AgentEvent) => void;
22
+ /** Project switches; a guardrail is on unless set to false. */
23
+ guardrails?: GuardrailSettings;
24
+ /** `fixer`: correction round on the fixer engine. */
25
+ stance?: 'fixer';
26
+ }
27
+ /** Shorthand for the guards: `on(env, 'empty-write')`. */
28
+ export declare function on(env: Pick<CompactEnv, 'guardrails'>, id: GuardrailId): boolean;
29
+ export declare const COMPACT_STANCE = "You are one step of a Specrails pipeline driven by a host program. Keep replies short and concrete. Never invent files, paths or commands: everything you state must come from the inputs or from tool results. When asked for JSON, reply with exactly one JSON object and nothing else.";
30
+ /** One host-driven `openspec_workflow` call, visible in the host log like a model tool call. */
31
+ export declare function openspecCall(env: CompactEnv, input: Parameters<OpenSpecTools['execute']>[0]): Promise<unknown>;
32
+ /** A tool-less structured call with one tolerant retry; the object is validated by the caller. */
33
+ /**
34
+ * Generation controls for the planning steps (proposal, design, specs, tasks).
35
+ * Generic OpenAI request fields, not model-specific: a low temperature keeps
36
+ * structured JSON literal instead of "creative", a wide output budget stops the
37
+ * plan from being truncated, and — only when the endpoint declared support —
38
+ * a high reasoning effort makes models that think privately (and answer
39
+ * tersely) spend their thinking on the plan.
40
+ */
41
+ export declare const PLANNING_CONTROLS: {
42
+ readonly temperature: 0.2;
43
+ readonly maxOutputTokens: 16384;
44
+ readonly reasoningEffort: "high";
45
+ };
46
+ export declare function structuredStep(env: CompactEnv, name: string, system: string, user: string, schema: Record<string, unknown>, validate?: (value: Record<string, unknown>) => string | undefined, controls?: {
47
+ temperature?: number;
48
+ maxOutputTokens?: number;
49
+ reasoningEffort?: string;
50
+ }): Promise<Record<string, unknown>>;
51
+ /** A bounded mini-loop with a subset of the workspace tools, then its final text. */
52
+ export declare function toolStep(env: CompactEnv, options: {
53
+ system: string;
54
+ user: string;
55
+ tools: string[];
56
+ maxToolCalls: number;
57
+ maxTurns?: number;
58
+ extraTools?: unknown[];
59
+ writeExtension?: {
60
+ tools: readonly string[];
61
+ extraCalls: number;
62
+ };
63
+ execute?: (name: string, args: Record<string, unknown>) => Promise<string> | string;
64
+ }): Promise<ToolLoopResult & {
65
+ messages: ChatMessage[];
66
+ }>;
67
+ /** Parses the loop's final text as JSON, or asks the same transcript once for the JSON object. */
68
+ export declare function finalJson(env: CompactEnv, name: string, messages: ChatMessage[], text: string, schema: Record<string, unknown>, validate?: (value: Record<string, unknown>) => string | undefined): Promise<Record<string, unknown>>;
69
+ export declare function strings(value: unknown, limit: number, each?: number): string[];
70
+ export declare function text(value: unknown, fallback?: string): string;
@@ -0,0 +1,136 @@
1
+ import { guardrailEnabled } from '../guardrails.js';
2
+ import { toolEvent } from '../tool-event.js';
3
+ import { AgentExecutionError } from '../executor-types.js';
4
+ import { parseAgentObject } from '../graph/artifacts.js';
5
+ import { record } from './chat-client.js';
6
+ import { runToolLoop } from './guarded-loop.js';
7
+ /** Shorthand for the guards: `on(env, 'empty-write')`. */
8
+ export function on(env, id) { return guardrailEnabled(env.guardrails, id); }
9
+ export const COMPACT_STANCE = 'You are one step of a Specrails pipeline driven by a host program. Keep replies short and concrete. Never invent files, paths or commands: everything you state must come from the inputs or from tool results. When asked for JSON, reply with exactly one JSON object and nothing else.';
10
+ /** One host-driven `openspec_workflow` call, visible in the host log like a model tool call. */
11
+ export async function openspecCall(env, input) {
12
+ env.onEvent?.(toolEvent('openspec_workflow', input));
13
+ try {
14
+ return await env.openspec.execute(input);
15
+ }
16
+ finally {
17
+ env.onEvent?.({ kind: 'tool-end', tool: 'openspec_workflow' });
18
+ }
19
+ }
20
+ /** A tool-less structured call with one tolerant retry; the object is validated by the caller. */
21
+ /**
22
+ * Generation controls for the planning steps (proposal, design, specs, tasks).
23
+ * Generic OpenAI request fields, not model-specific: a low temperature keeps
24
+ * structured JSON literal instead of "creative", a wide output budget stops the
25
+ * plan from being truncated, and — only when the endpoint declared support —
26
+ * a high reasoning effort makes models that think privately (and answer
27
+ * tersely) spend their thinking on the plan.
28
+ */
29
+ export const PLANNING_CONTROLS = { temperature: 0.2, maxOutputTokens: 16384, reasoningEffort: 'high' };
30
+ /** When a planning call runs out of output budget, the hidden reasoning ate
31
+ * it (models that think privately count that against max_tokens). Retry
32
+ * once with less thinking rather than fail the step. */
33
+ const EFFORT_FALLBACK = { high: 'medium', medium: 'low' };
34
+ export async function structuredStep(env, name, system, user, schema, validate = () => undefined, controls = PLANNING_CONTROLS) {
35
+ const messages = [{ role: 'system', content: COMPACT_STANCE + '\n' + system }, { role: 'user', content: user }];
36
+ // One structured artifact (proposal, design, a spec, tasks) is one unit of work: it gets the full budget.
37
+ env.resetDeadline?.();
38
+ let problem = '';
39
+ // A connection that declares a wider output budget than the planning default gets it for its structured (long-form) steps too.
40
+ let effective = { ...controls, ...(env.maxOutputTokens !== undefined && env.maxOutputTokens > (controls.maxOutputTokens ?? 0) ? { maxOutputTokens: env.maxOutputTokens } : {}) };
41
+ const started = Date.now();
42
+ for (let attempt = 0; attempt < 2; attempt++) {
43
+ env.signal.throwIfAborted();
44
+ // A structured step is one long generation with no tool calls: without
45
+ // this line the run log goes silent for minutes (observed: 5 min after
46
+ // the inventory with nothing to show). Name the step and, on the retry,
47
+ // why the first answer was rejected.
48
+ env.onEvent?.({ kind: 'text', text: attempt === 0 ? `Compact ${env.request.role}: writing ${name}…` : `Compact ${env.request.role}: ${name} rejected (${problem.slice(0, 160)}); retrying.` });
49
+ let completion;
50
+ try {
51
+ completion = await env.client.completeStructured(attempt === 0 ? messages : [...messages, { role: 'assistant', content: '' }, { role: 'user', content: `Your previous reply could not be used: ${problem}. Reply again with exactly one JSON object matching the schema.` }], name, schema, effective);
52
+ }
53
+ catch (error) {
54
+ const fallback = error instanceof AgentExecutionError && error.code === 'incomplete_response' && effective.reasoningEffort ? EFFORT_FALLBACK[effective.reasoningEffort] : undefined;
55
+ if (!fallback || attempt > 0)
56
+ throw error;
57
+ env.onEvent?.({ kind: 'text', text: `Compact ${name} step ran out of output budget while reasoning at "${effective.reasoningEffort}"; retrying at "${fallback}".` });
58
+ effective = { ...effective, reasoningEffort: fallback };
59
+ problem = 'the reply was cut off before the JSON object was complete';
60
+ continue;
61
+ }
62
+ const { message } = completion;
63
+ const text = typeof message.content === 'string' ? message.content : '';
64
+ let parsed;
65
+ try {
66
+ parsed = text.trim() ? parseAgentObject(text) : undefined;
67
+ }
68
+ catch {
69
+ parsed = undefined;
70
+ }
71
+ problem = parsed ? validate(parsed) ?? '' : 'the reply was not one JSON object';
72
+ if (parsed && !problem) {
73
+ env.onEvent?.({ kind: 'text', text: `Compact ${env.request.role}: ${name} written in ${Math.max(1, Math.round((Date.now() - started) / 1000))}s.` });
74
+ return parsed;
75
+ }
76
+ }
77
+ throw new AgentExecutionError(`Compact ${name} step did not produce a valid result: ${problem}`, 'invalid_response', env.client.usage);
78
+ }
79
+ /** A bounded mini-loop with a subset of the workspace tools, then its final text. */
80
+ export async function toolStep(env, options) {
81
+ const definitions = [...env.toolset.definitions().filter(tool => options.tools.includes(tool.function.name)), ...options.extraTools ?? []];
82
+ const examples = definitions.map(tool => `- ${record(record(tool)?.function)?.name}: ${exampleFor(String(record(record(tool)?.function)?.name))}`).join('\n');
83
+ const messages = [
84
+ { role: 'system', content: COMPACT_STANCE + '\n' + options.system + `\nTools available in this step (one example call each):\n${examples}\nCall at most ${options.maxToolCalls} tools, one at a time; never repeat a call whose result you already have.` },
85
+ { role: 'user', content: options.user },
86
+ ];
87
+ // One bounded tool loop (an inventory, a task group, a fix round, a review) is one unit of work: full budget.
88
+ env.resetDeadline?.();
89
+ const result = await runToolLoop({
90
+ client: env.client, messages, tools: definitions, maxTurns: options.maxTurns ?? options.maxToolCalls + 6, maxToolCalls: options.maxToolCalls,
91
+ contextWindowTokens: env.contextWindowTokens, ...(env.maxOutputTokens === undefined ? {} : { maxOutputTokens: env.maxOutputTokens }), ...(options.writeExtension ? { writeExtension: options.writeExtension } : {}), onEvent: env.onEvent, signal: env.signal,
92
+ execute: (name, args) => options.execute ? options.execute(name, args) : env.toolset.execute(name, args),
93
+ });
94
+ return { ...result, messages };
95
+ }
96
+ function exampleFor(name) {
97
+ switch (name) {
98
+ case 'list_files': return '{"path":"."}';
99
+ case 'read_file': return '{"path":"src/index.ts"}';
100
+ case 'read_lines': return '{"path":"src/index.ts","startLine":1,"endLine":120}';
101
+ case 'search_text': return '{"path":"src","query":"export function handle"}';
102
+ case 'get_diff': return '{"path":"src/index.ts"}';
103
+ case 'write_file': return '{"path":"src/feature.ts","content":"export const feature = 1\\n"}';
104
+ case 'apply_patch': return '{"path":"src/feature.ts","oldText":"const a = 1","newText":"const a = 2"}';
105
+ case 'read_verification_evidence': return '{"id":"<evidence id>","section":"stdout"}';
106
+ default: return '{}';
107
+ }
108
+ }
109
+ /** Parses the loop's final text as JSON, or asks the same transcript once for the JSON object. */
110
+ export async function finalJson(env, name, messages, text, schema, validate = () => undefined) {
111
+ try {
112
+ const parsed = parseAgentObject(text);
113
+ const problem = validate(parsed);
114
+ if (!problem)
115
+ return parsed;
116
+ }
117
+ catch { /* fall through to one structured retry */ }
118
+ const trimmed = messages.map(message => message.role === 'tool' && typeof message.content === 'string' && message.content.length > 400 ? { ...message, content: message.content.slice(0, 400) + '…' } : message);
119
+ const { message } = await env.client.completeStructured([...trimmed, { role: 'assistant', content: text || '' }, { role: 'user', content: 'Now reply with exactly one JSON object for this step and nothing else.' }], name, schema);
120
+ const parsed = record(safeParse(typeof message.content === 'string' ? message.content : ''));
121
+ const problem = parsed ? validate(parsed) : 'the reply was not one JSON object';
122
+ if (!parsed || problem)
123
+ throw new AgentExecutionError(`Compact ${name} step did not produce a valid result: ${problem}`, 'invalid_response', env.client.usage);
124
+ return parsed;
125
+ }
126
+ function safeParse(text) { try {
127
+ return parseAgentObject(text);
128
+ }
129
+ catch {
130
+ return undefined;
131
+ } }
132
+ export function strings(value, limit, each = 1000) {
133
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0).map(item => item.trim().slice(0, each)).slice(0, limit) : [];
134
+ }
135
+ export function text(value, fallback = '') { return typeof value === 'string' && value.trim() ? value.trim() : fallback; }
136
+ //# sourceMappingURL=step.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step.js","sourceRoot":"","sources":["../../../src/agent-runtime/compact/step.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ export interface ReachabilityCommand {
2
+ command: string;
3
+ args: string[];
4
+ cwd?: string;
5
+ }
6
+ /** Test-file shapes across ecosystems (`tests/x.test.js`, `x.spec.ts`, `test_x.py`, `x_test.go`, `spec/x_spec.rb`, anything under a test(s)/spec(s) dir). */
7
+ export declare const TEST_FILE_PATTERN: RegExp;
8
+ /** A file that IS a test (not a helper that merely lives under tests/): `x.test.js`, `x.spec.ts`, `test_x.py`, `x_test.go`, `x_spec.rb`, `XTest.java`, `x.t.cpp`… */
9
+ export declare const STRICT_TEST_FILE_PATTERN: RegExp;
10
+ /**
11
+ * Resolves the command line verify runs: an `npm test` / `pnpm run x` /
12
+ * `yarn x` invocation becomes the script body from package.json (nested
13
+ * `npm run` references followed up to a small depth); anything else is the
14
+ * literal argv. Returns the text to inspect for paths.
15
+ */
16
+ export declare function expandCommandText(root: string, command: ReachabilityCommand, depth?: number): string;
17
+ /**
18
+ * Whether one verification command reaches the (repository-relative) test
19
+ * file. Unknown ⇒ true: only an enumerating command can prove absence.
20
+ */
21
+ export declare function commandReachesTest(root: string, command: ReachabilityCommand, file: string): boolean;
22
+ /** Repository-relative POSIX path for a reported test file (absolute paths under the root are relativized), or undefined when outside the root or missing. */
23
+ export declare function relativeTestPath(root: string, file: string): string | undefined;
24
+ /**
25
+ * The test files (repository-relative) among `reported` that exist on disk and
26
+ * that NO command reaches. Empty when every command is unjudgeable.
27
+ */
28
+ export declare function unreachedTestFiles(root: string, commands: readonly ReachabilityCommand[], reported: readonly string[]): string[];
29
+ /** The feedback sentence the developer receives; starts with a capital so the synthetic-corrections parser turns it into a task. */
30
+ export declare function unreachedTestsReason(files: readonly string[]): string;
@@ -0,0 +1,147 @@
1
+ // Test reachability: does any verification command actually execute the test
2
+ // files the developer wrote?
3
+ //
4
+ // Observed (tetris, local 30B developer): the developer created
5
+ // tests/browser.test.js (622 lines, 6 failing cases) but package.json's test
6
+ // script stayed `node tests/board.test.js && node tests/tetromino.test.js &&
7
+ // node tests/game.test.js`. Verify ran the script, exited 0, the reviewer
8
+ // approved, and the run shipped a test nobody had ever run. The developer even
9
+ // confessed it in its summary ("commands you could not run: …").
10
+ //
11
+ // The host knows both halves — the files the developer reported as tests and
12
+ // the exact commands verify executes — so the check is deterministic and needs
13
+ // no model. It is deliberately conservative (fail-open): a command whose
14
+ // discovery cannot be judged (a shell script, `make test`, an unknown runner
15
+ // with no path arguments) is assumed to reach everything. Only a command that
16
+ // ENUMERATES test files, or a discovery runner pointed at specific paths, can
17
+ // leave a file unreached.
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ /** Test-file shapes across ecosystems (`tests/x.test.js`, `x.spec.ts`, `test_x.py`, `x_test.go`, `spec/x_spec.rb`, anything under a test(s)/spec(s) dir). */
21
+ export const TEST_FILE_PATTERN = /(?:\.|_|\/|^)(?:test|spec)s?[./_]|(?:^|\/)test_[^/]+\.py$|_test\.(?:go|py|rs|rb|exs?)$|_spec\.rb$/i;
22
+ /** A file that IS a test (not a helper that merely lives under tests/): `x.test.js`, `x.spec.ts`, `test_x.py`, `x_test.go`, `x_spec.rb`, `XTest.java`, `x.t.cpp`… */
23
+ export const STRICT_TEST_FILE_PATTERN = /(?:^|\/)(?:test_[^/]+\.py|[^/]+(?:\.(?:test|spec)\.[a-z]+|_test\.(?:go|py|rs|rb|exs?|ts|js)|_spec\.rb|Tests?\.(?:java|kt|cs|swift)))$/i;
24
+ /** Runners that discover tests by convention when given no explicit path (the file is then reached unless the runner is pointed elsewhere). */
25
+ const DISCOVERY_RUNNERS = /(?:^|[\s/])(?:jest|vitest|mocha|ava|tap|tape|uvu|jasmine|karma|cypress|playwright|bun|deno|node|pytest|py\.test|python3?|go|cargo|dotnet|mvn|mvnw|gradle|gradlew|rspec|phpunit|pest|mix|swift|ctest|tsx|ts-node)(?:\.exe|\.cmd)?\b/i;
26
+ const RUNNER_TEST_VERBS = /(?:^|\s)--test\b|\b(?:test|check)\b/;
27
+ const SCRIPT_DEPTH = 3;
28
+ const posix = (file) => file.replace(/\\/g, '/').replace(/^\.\//, '');
29
+ /**
30
+ * Resolves the command line verify runs: an `npm test` / `pnpm run x` /
31
+ * `yarn x` invocation becomes the script body from package.json (nested
32
+ * `npm run` references followed up to a small depth); anything else is the
33
+ * literal argv. Returns the text to inspect for paths.
34
+ */
35
+ export function expandCommandText(root, command, depth = 0) {
36
+ const literal = [command.command, ...command.args].join(' ');
37
+ const runner = path.basename(command.command).replace(/\.(?:cmd|exe)$/i, '');
38
+ if (!['npm', 'pnpm', 'yarn', 'bun'].includes(runner) || depth > SCRIPT_DEPTH)
39
+ return literal;
40
+ const args = command.args.filter(arg => !arg.startsWith('-'));
41
+ const name = args[0] === 'run' || args[0] === 'run-script' ? args[1] : args[0] === 'test' || args[0] === 'start' ? args[0] : runner === 'yarn' || runner === 'bun' ? args[0] : undefined;
42
+ if (!name)
43
+ return literal;
44
+ const script = readScript(path.resolve(root, command.cwd ?? '.'), name);
45
+ if (!script)
46
+ return literal;
47
+ // A script that itself runs `npm run other` is followed; anything else is inspected as written.
48
+ return script.replace(/\b(?:npm|pnpm|yarn|bun)(?:\s+run(?:-script)?)?\s+([\w:.-]+)/g, (match, inner) => {
49
+ if (inner === 'test' && name === 'test')
50
+ return match;
51
+ const nested = readScript(path.resolve(root, command.cwd ?? '.'), inner);
52
+ return nested && depth < SCRIPT_DEPTH ? `(${expandCommandText(root, { command: runner, args: ['run', inner], cwd: command.cwd }, depth + 1)})` : match;
53
+ });
54
+ }
55
+ function readScript(dir, name) {
56
+ try {
57
+ const manifest = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
58
+ const script = manifest.scripts?.[name];
59
+ return typeof script === 'string' && script.trim() ? script : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /** Splits a shell command line into its `&&` / `||` / `;` / `|` segments, each tokenized on whitespace (quotes stripped). */
66
+ function segments(text) {
67
+ return text.split(/\s*(?:&&|\|\||;|\|)\s*/).map(segment => segment.replace(/^\(|\)$/g, '').trim()).filter(Boolean)
68
+ .map(segment => segment.match(/"[^"]*"|'[^']*'|\S+/g)?.map(token => token.replace(/^["']|["']$/g, '')) ?? []);
69
+ }
70
+ /** Tokens that address files or directories: a slash, a test-file shape, or a glob. */
71
+ function pathTokens(tokens) {
72
+ return tokens.slice(1).filter(token => !token.startsWith('-') && !/^[A-Z_][A-Z0-9_]*=/.test(token) && (token.includes('/') || token.includes('*') || TEST_FILE_PATTERN.test(token) || /\.[cm]?[jt]sx?$|\.py$|\.rb$|\.go$/.test(token)));
73
+ }
74
+ function globToRegExp(glob) {
75
+ // Placeholders keep the generated regex syntax out of the later `*` / `?` substitutions.
76
+ const source = posix(glob).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*\*\//g, '\u0001').replace(/\*\*/g, '\u0002').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]').replace(/\u0001/g, '(?:.*/)?').replace(/\u0002/g, '.*');
77
+ return new RegExp(`^${source}$`);
78
+ }
79
+ function tokenReaches(token, file, root, discovery) {
80
+ let target = posix(token);
81
+ // Go's `./...` (and `pkg/...`) recurse into every package below.
82
+ if (target === '...' || target === './...')
83
+ return true;
84
+ if (target.endsWith('/...'))
85
+ target = target.slice(0, -4);
86
+ if (target === file || path.posix.basename(target) === file || file.startsWith(target.replace(/\/$/, '') + '/'))
87
+ return true;
88
+ if (target.includes('*'))
89
+ return globToRegExp(target).test(file);
90
+ const absolute = path.resolve(root, target);
91
+ if (existsSync(absolute) && file.startsWith(posix(path.relative(root, absolute)) + '/'))
92
+ return true;
93
+ // A discovery runner treats a bare argument as a name pattern (`jest board` runs board.test.js).
94
+ return discovery && !target.includes('/') && file.includes(target);
95
+ }
96
+ /**
97
+ * Whether one verification command reaches the (repository-relative) test
98
+ * file. Unknown ⇒ true: only an enumerating command can prove absence.
99
+ */
100
+ export function commandReachesTest(root, command, file) {
101
+ const target = posix(file);
102
+ let judged = false;
103
+ for (const tokens of segments(expandCommandText(root, command))) {
104
+ if (!tokens.length)
105
+ continue;
106
+ const head = tokens.join(' ');
107
+ const discovery = DISCOVERY_RUNNERS.test(head);
108
+ const paths = pathTokens(tokens);
109
+ if (discovery && !paths.length) {
110
+ // `npm test` → `jest` discovers everything; `node --test` likewise; but bare `node`/`python` with no path runs nothing test-shaped.
111
+ if (!/^(?:node|python3?|deno|bun)$/i.test(path.basename(tokens[0])) || RUNNER_TEST_VERBS.test(head))
112
+ return true;
113
+ continue;
114
+ }
115
+ // Paths that are not test-shaped (`node scripts/run-tests.js`, `tsx src/cli.ts`)
116
+ // enumerate nothing we can judge: the script may discover tests itself.
117
+ if (!paths.some(token => TEST_FILE_PATTERN.test(posix(token)) || token.includes('*') || token.endsWith('...')))
118
+ continue;
119
+ judged = true;
120
+ if (paths.some(token => tokenReaches(token, target, root, discovery)))
121
+ return true;
122
+ }
123
+ return !judged;
124
+ }
125
+ /** Repository-relative POSIX path for a reported test file (absolute paths under the root are relativized), or undefined when outside the root or missing. */
126
+ export function relativeTestPath(root, file) {
127
+ const absolute = path.isAbsolute(file) ? file : path.resolve(root, file);
128
+ const relative = posix(path.relative(root, absolute));
129
+ if (!relative || relative.startsWith('..') || !existsSync(absolute))
130
+ return undefined;
131
+ return relative;
132
+ }
133
+ /**
134
+ * The test files (repository-relative) among `reported` that exist on disk and
135
+ * that NO command reaches. Empty when every command is unjudgeable.
136
+ */
137
+ export function unreachedTestFiles(root, commands, reported) {
138
+ if (!commands.length)
139
+ return [];
140
+ const files = [...new Set(reported.map(file => relativeTestPath(root, file)).filter((file) => !!file && STRICT_TEST_FILE_PATTERN.test(file)))];
141
+ return files.filter(file => !commands.some(command => commandReachesTest(root, command, file)));
142
+ }
143
+ /** The feedback sentence the developer receives; starts with a capital so the synthetic-corrections parser turns it into a task. */
144
+ export function unreachedTestsReason(files) {
145
+ return `Test file${files.length === 1 ? '' : 's'} written by the developer ${files.length === 1 ? 'is' : 'are'} not executed by any verification command: ${files.join(', ')}. A test nobody runs proves nothing. Wire ${files.length === 1 ? 'it' : 'them'} into the repository's test command (for example the package.json "test" script) or propose a verificationCheck that runs ${files.length === 1 ? 'it' : 'them'}, then make ${files.length === 1 ? 'it' : 'them'} pass.`;
146
+ }
147
+ //# sourceMappingURL=test-reachability.js.map