@nathapp/nax 0.68.1 → 0.68.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nax.js +673 -159
- package/package.json +1 -1
package/dist/nax.js
CHANGED
|
@@ -17203,7 +17203,8 @@ var init_schemas_review = __esm(() => {
|
|
|
17203
17203
|
requote: true,
|
|
17204
17204
|
maxRequotes: 5
|
|
17205
17205
|
}),
|
|
17206
|
-
excludePatterns: exports_external.array(exports_external.string()).optional()
|
|
17206
|
+
excludePatterns: exports_external.array(exports_external.string()).optional(),
|
|
17207
|
+
demandInspectionTrail: exports_external.boolean().default(true)
|
|
17207
17208
|
});
|
|
17208
17209
|
AdversarialReviewConfigSchema = exports_external.object({
|
|
17209
17210
|
model: ConfiguredModelSchema.default("balanced"),
|
|
@@ -17214,6 +17215,7 @@ var init_schemas_review = __esm(() => {
|
|
|
17214
17215
|
parallel: exports_external.boolean().default(false),
|
|
17215
17216
|
maxConcurrentSessions: exports_external.number().int().min(1).max(4).default(2),
|
|
17216
17217
|
acRegroundOnDrop: exports_external.boolean().default(true),
|
|
17218
|
+
demandInspectionTrail: exports_external.boolean().default(true),
|
|
17217
17219
|
substantiation: exports_external.object({
|
|
17218
17220
|
requote: exports_external.boolean().default(true),
|
|
17219
17221
|
maxRequotes: exports_external.number().int().min(0).default(5)
|
|
@@ -17428,6 +17430,7 @@ var init_schemas3 = __esm(() => {
|
|
|
17428
17430
|
resetRefOnRerun: false,
|
|
17429
17431
|
rules: [],
|
|
17430
17432
|
timeoutMs: 600000,
|
|
17433
|
+
demandInspectionTrail: true,
|
|
17431
17434
|
substantiation: {
|
|
17432
17435
|
requote: true,
|
|
17433
17436
|
maxRequotes: 5
|
|
@@ -17451,6 +17454,7 @@ var init_schemas3 = __esm(() => {
|
|
|
17451
17454
|
parallel: false,
|
|
17452
17455
|
maxConcurrentSessions: 2,
|
|
17453
17456
|
acRegroundOnDrop: true,
|
|
17457
|
+
demandInspectionTrail: true,
|
|
17454
17458
|
substantiation: {
|
|
17455
17459
|
requote: true,
|
|
17456
17460
|
maxRequotes: 5
|
|
@@ -17735,19 +17739,30 @@ function formatStoryStart(entry, c, _timestamp, mode) {
|
|
|
17735
17739
|
const complexity = typeof data.complexity === "string" ? data.complexity : "unknown";
|
|
17736
17740
|
const tier = typeof data.modelTier === "string" ? data.modelTier : "unknown";
|
|
17737
17741
|
const attempt = typeof data.attempt === "number" ? data.attempt : 1;
|
|
17742
|
+
const agent = typeof data.agent === "string" ? data.agent : undefined;
|
|
17743
|
+
const progress = typeof data.storyNumber === "number" && typeof data.storyTotal === "number" ? `${data.storyNumber}/${data.storyTotal}` : undefined;
|
|
17738
17744
|
const lines = [];
|
|
17739
17745
|
lines.push("");
|
|
17740
17746
|
lines.push(c.bold(`${EMOJI.storyStart} ${c.cyan(storyId)}: ${title}`));
|
|
17741
17747
|
if (mode === "verbose") {
|
|
17748
|
+
if (progress)
|
|
17749
|
+
lines.push(` ${c.gray("\u251C\u2500")} Story: ${c.cyan(progress)}`);
|
|
17742
17750
|
lines.push(` ${c.gray("\u251C\u2500")} Complexity: ${c.yellow(complexity)}`);
|
|
17743
17751
|
lines.push(` ${c.gray("\u251C\u2500")} Tier: ${c.magenta(tier)}`);
|
|
17752
|
+
if (agent)
|
|
17753
|
+
lines.push(` ${c.gray("\u251C\u2500")} Agent: ${c.cyan(agent)}`);
|
|
17744
17754
|
if (attempt > 1) {
|
|
17745
17755
|
lines.push(` ${c.gray("\u2514\u2500")} Attempt: ${c.yellow(`#${attempt}`)} ${EMOJI.retry}`);
|
|
17746
17756
|
} else {
|
|
17747
17757
|
lines.push(` ${c.gray("\u2514\u2500")} Status: ${c.green("starting")}`);
|
|
17748
17758
|
}
|
|
17749
17759
|
} else {
|
|
17750
|
-
const metadata = [
|
|
17760
|
+
const metadata = [];
|
|
17761
|
+
if (progress)
|
|
17762
|
+
metadata.push(progress);
|
|
17763
|
+
metadata.push(complexity, tier);
|
|
17764
|
+
if (agent)
|
|
17765
|
+
metadata.push(agent);
|
|
17751
17766
|
if (attempt > 1)
|
|
17752
17767
|
metadata.push(`attempt #${attempt} ${EMOJI.retry}`);
|
|
17753
17768
|
lines.push(` ${c.gray(metadata.join(" \u2022 "))}`);
|
|
@@ -17809,24 +17824,19 @@ function formatDefault(entry, c, timestamp, mode) {
|
|
|
17809
17824
|
if (entry.storyId) {
|
|
17810
17825
|
parts.push(c.dim(`[${entry.storyId}]`));
|
|
17811
17826
|
}
|
|
17827
|
+
if (entry.sessionRole) {
|
|
17828
|
+
parts.push(c.dim(`(${entry.sessionRole})`));
|
|
17829
|
+
}
|
|
17812
17830
|
parts.push(entry.message);
|
|
17813
17831
|
let output = parts.join(" ");
|
|
17814
17832
|
const data = entry.data;
|
|
17815
17833
|
if (data && typeof data === "object") {
|
|
17816
|
-
const meta3 =
|
|
17817
|
-
if (typeof data.cost === "number" && data.cost > 0)
|
|
17818
|
-
meta3.push(`${EMOJI.cost} ${formatCost(data.cost)}`);
|
|
17819
|
-
if (typeof data.durationMs === "number" && data.durationMs > 0)
|
|
17820
|
-
meta3.push(`${EMOJI.duration} ${formatDuration(data.durationMs)}`);
|
|
17821
|
-
if (typeof data.action === "string")
|
|
17822
|
-
meta3.push(`action: ${data.action}`);
|
|
17823
|
-
if (typeof data.reason === "string" && mode !== "quiet")
|
|
17824
|
-
meta3.push(data.reason);
|
|
17834
|
+
const meta3 = buildDefaultMeta(data, mode);
|
|
17825
17835
|
if (meta3.length > 0) {
|
|
17826
17836
|
output += ` ${c.gray(meta3.join(" "))}`;
|
|
17827
17837
|
}
|
|
17828
17838
|
if (mode === "verbose") {
|
|
17829
|
-
const
|
|
17839
|
+
const filtered = stripConsumedMetaFields(data);
|
|
17830
17840
|
if (Object.keys(filtered).length > 0) {
|
|
17831
17841
|
output += `
|
|
17832
17842
|
${c.gray(JSON.stringify(filtered, null, 2))}`;
|
|
@@ -17838,6 +17848,52 @@ ${c.gray(JSON.stringify(filtered, null, 2))}`;
|
|
|
17838
17848
|
shouldDisplay: true
|
|
17839
17849
|
};
|
|
17840
17850
|
}
|
|
17851
|
+
function buildDefaultMeta(data, mode) {
|
|
17852
|
+
const meta3 = [];
|
|
17853
|
+
const identity = [data.agentName, data.model].filter((v) => typeof v === "string" && v.length > 0);
|
|
17854
|
+
if (identity.length > 0)
|
|
17855
|
+
meta3.push(`${EMOJI.agent} ${identity.join("\xB7")}`);
|
|
17856
|
+
if (typeof data.phaseIndex === "number" && typeof data.totalPhases === "number") {
|
|
17857
|
+
meta3.push(`${data.phaseIndex}/${data.totalPhases}`);
|
|
17858
|
+
}
|
|
17859
|
+
if (typeof data.status === "string")
|
|
17860
|
+
meta3.push(`status: ${data.status}`);
|
|
17861
|
+
if (typeof data.findingsCount === "number")
|
|
17862
|
+
meta3.push(`${data.findingsCount} finding${data.findingsCount === 1 ? "" : "s"}`);
|
|
17863
|
+
const activity = buildActivityMeta(data);
|
|
17864
|
+
if (activity)
|
|
17865
|
+
meta3.push(activity);
|
|
17866
|
+
if (typeof data.cost === "number" && data.cost > 0)
|
|
17867
|
+
meta3.push(`${EMOJI.cost} ${formatCost(data.cost)}`);
|
|
17868
|
+
if (typeof data.durationMs === "number" && data.durationMs > 0)
|
|
17869
|
+
meta3.push(`${EMOJI.duration} ${formatDuration(data.durationMs)}`);
|
|
17870
|
+
if (typeof data.action === "string")
|
|
17871
|
+
meta3.push(`action: ${data.action}`);
|
|
17872
|
+
if (typeof data.reason === "string" && mode !== "quiet")
|
|
17873
|
+
meta3.push(data.reason);
|
|
17874
|
+
return meta3;
|
|
17875
|
+
}
|
|
17876
|
+
function buildActivityMeta(data) {
|
|
17877
|
+
const segments = [];
|
|
17878
|
+
if (typeof data.messageUpdates === "number" && data.messageUpdates > 0)
|
|
17879
|
+
segments.push(`msg ${data.messageUpdates}`);
|
|
17880
|
+
if (typeof data.toolCallUpdates === "number" && data.toolCallUpdates > 0)
|
|
17881
|
+
segments.push(`tools ${data.toolCallUpdates}`);
|
|
17882
|
+
if (typeof data.thinkingUpdates === "number" && data.thinkingUpdates > 0)
|
|
17883
|
+
segments.push(`think ${data.thinkingUpdates}`);
|
|
17884
|
+
if (typeof data.idleMs === "number" && data.idleMs > 0)
|
|
17885
|
+
segments.push(`idle ${formatDuration(data.idleMs)}`);
|
|
17886
|
+
return segments.length > 0 ? segments.join(" ") : null;
|
|
17887
|
+
}
|
|
17888
|
+
function stripConsumedMetaFields(data) {
|
|
17889
|
+
const filtered = {};
|
|
17890
|
+
for (const [key, value] of Object.entries(data)) {
|
|
17891
|
+
if (!CONSUMED_META_KEYS.includes(key)) {
|
|
17892
|
+
filtered[key] = value;
|
|
17893
|
+
}
|
|
17894
|
+
}
|
|
17895
|
+
return filtered;
|
|
17896
|
+
}
|
|
17841
17897
|
function formatRunSummary(summary, options) {
|
|
17842
17898
|
const { mode, useColor = true } = options;
|
|
17843
17899
|
if (mode === "json") {
|
|
@@ -17882,9 +17938,26 @@ function createNoopChalk() {
|
|
|
17882
17938
|
cyan: noop
|
|
17883
17939
|
};
|
|
17884
17940
|
}
|
|
17941
|
+
var CONSUMED_META_KEYS;
|
|
17885
17942
|
var init_formatter = __esm(() => {
|
|
17886
17943
|
init_source();
|
|
17887
17944
|
init_types2();
|
|
17945
|
+
CONSUMED_META_KEYS = [
|
|
17946
|
+
"agentName",
|
|
17947
|
+
"model",
|
|
17948
|
+
"phaseIndex",
|
|
17949
|
+
"totalPhases",
|
|
17950
|
+
"status",
|
|
17951
|
+
"findingsCount",
|
|
17952
|
+
"messageUpdates",
|
|
17953
|
+
"toolCallUpdates",
|
|
17954
|
+
"thinkingUpdates",
|
|
17955
|
+
"idleMs",
|
|
17956
|
+
"cost",
|
|
17957
|
+
"durationMs",
|
|
17958
|
+
"action",
|
|
17959
|
+
"reason"
|
|
17960
|
+
];
|
|
17888
17961
|
});
|
|
17889
17962
|
|
|
17890
17963
|
// src/logging/index.ts
|
|
@@ -17933,6 +18006,43 @@ var init_formatters = __esm(() => {
|
|
|
17933
18006
|
init_source();
|
|
17934
18007
|
});
|
|
17935
18008
|
|
|
18009
|
+
// src/logger/redact.ts
|
|
18010
|
+
function redactString(value) {
|
|
18011
|
+
let out = value;
|
|
18012
|
+
for (const re of SECRET_VALUE_PATTERNS) {
|
|
18013
|
+
re.lastIndex = 0;
|
|
18014
|
+
out = out.replace(re, REDACTED);
|
|
18015
|
+
}
|
|
18016
|
+
return out;
|
|
18017
|
+
}
|
|
18018
|
+
function redactSecrets(input) {
|
|
18019
|
+
if (typeof input === "string")
|
|
18020
|
+
return redactString(input);
|
|
18021
|
+
if (Array.isArray(input))
|
|
18022
|
+
return input.map(redactSecrets);
|
|
18023
|
+
if (input !== null && typeof input === "object") {
|
|
18024
|
+
const out = {};
|
|
18025
|
+
for (const [key, value] of Object.entries(input)) {
|
|
18026
|
+
out[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redactSecrets(value);
|
|
18027
|
+
}
|
|
18028
|
+
return out;
|
|
18029
|
+
}
|
|
18030
|
+
return input;
|
|
18031
|
+
}
|
|
18032
|
+
var SECRET_KEY_PATTERN, SECRET_VALUE_PATTERNS, REDACTED = "[REDACTED]";
|
|
18033
|
+
var init_redact = __esm(() => {
|
|
18034
|
+
SECRET_KEY_PATTERN = /(SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)/i;
|
|
18035
|
+
SECRET_VALUE_PATTERNS = [
|
|
18036
|
+
/sk-[A-Za-z0-9_-]{16,}/g,
|
|
18037
|
+
/ghp_[A-Za-z0-9]{16,}/g,
|
|
18038
|
+
/gh[opsu]_[A-Za-z0-9]{16,}/g,
|
|
18039
|
+
/npm_[A-Za-z0-9]{8,}/g,
|
|
18040
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
18041
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
18042
|
+
/(?:SECRET|TOKEN|API_?KEY|PASSWORD|PRIVATE_?KEY|ACCESS_?KEY|WEBHOOK)=[^\s"',]+/gi
|
|
18043
|
+
];
|
|
18044
|
+
});
|
|
18045
|
+
|
|
17936
18046
|
// src/logger/logger.ts
|
|
17937
18047
|
import { mkdirSync } from "fs";
|
|
17938
18048
|
import { appendFile } from "fs/promises";
|
|
@@ -18027,7 +18137,8 @@ ${JSON.stringify(entry.data, null, 2)}`;
|
|
|
18027
18137
|
writeToFile(entry) {
|
|
18028
18138
|
if (!this.filePath)
|
|
18029
18139
|
return;
|
|
18030
|
-
const
|
|
18140
|
+
const safeEntry = entry.data ? { ...entry, data: redactSecrets(entry.data) } : entry;
|
|
18141
|
+
const line = `${formatJsonl(safeEntry)}
|
|
18031
18142
|
`;
|
|
18032
18143
|
const filePath = this.filePath;
|
|
18033
18144
|
this.writeQueueTail = this.writeQueueTail.then(() => appendFile(filePath, line).catch((error48) => {
|
|
@@ -18089,6 +18200,7 @@ var LOG_LEVEL_PRIORITY, instance = null, noopLogger;
|
|
|
18089
18200
|
var init_logger = __esm(() => {
|
|
18090
18201
|
init_logging();
|
|
18091
18202
|
init_formatters();
|
|
18203
|
+
init_redact();
|
|
18092
18204
|
LOG_LEVEL_PRIORITY = {
|
|
18093
18205
|
silent: -1,
|
|
18094
18206
|
error: 0,
|
|
@@ -19070,7 +19182,7 @@ GOOD (write ACs like these):
|
|
|
19070
19182
|
|
|
19071
19183
|
When a spec is provided, these rules govern acceptance criteria generation:
|
|
19072
19184
|
|
|
19073
|
-
1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria
|
|
19185
|
+
1. **Preserve spec ACs.** Every acceptance criterion stated in the spec must appear in \`acceptanceCriteria\`. Never silently drop a spec AC. ACs the spec tags \`[verbatim]\` (typically executable greps, file-existence checks, regex/count assertions, or architectural invariants) MUST be copied **character-for-character** into an \`acceptanceCriteria\` entry \u2014 preserve every backtick-quoted command, file path, regex, and count exactly; do not paraphrase, retag, split, or move them into a description. Untagged ACs may be lightly rephrased for testability, but must retain the same assertion and concrete identifiers.
|
|
19074
19186
|
2. **Do not invent spec ACs.** If you identify useful behavioral edge cases or negative paths that the spec did not explicitly list, place them in \`suggestedCriteria\` (a string array on the same story object) \u2014 never in \`acceptanceCriteria\`. These go through a separate hardening pass.
|
|
19075
19187
|
3. **Respect story scope.** Each story's criteria must only cover what the spec says for that story. Do not assign criteria that belong to a different story's scope (wrong feature area, wrong file, wrong dependency chain).
|
|
19076
19188
|
4. **\`suggestedCriteria\` format.** Each element must be a plain behavioral assertion \u2014 an observable output, return value, state change, or error condition that a test can assert. Never include implementation details (imports, internal structure), design suggestions, or vague descriptions.
|
|
@@ -22551,9 +22663,16 @@ async function detectTestFramework(_workdir, language, pkg) {
|
|
|
22551
22663
|
}
|
|
22552
22664
|
return;
|
|
22553
22665
|
}
|
|
22666
|
+
function clearLanguageCache() {
|
|
22667
|
+
_languageCache.clear();
|
|
22668
|
+
}
|
|
22554
22669
|
async function detectLanguage(packageDir) {
|
|
22670
|
+
if (_languageCache.has(packageDir))
|
|
22671
|
+
return _languageCache.get(packageDir);
|
|
22555
22672
|
const pkg = await _detectorDeps.readJson(join5(packageDir, "package.json"));
|
|
22556
|
-
|
|
22673
|
+
const result = await _detectLanguageImpl(packageDir, pkg);
|
|
22674
|
+
_languageCache.set(packageDir, result);
|
|
22675
|
+
return result;
|
|
22557
22676
|
}
|
|
22558
22677
|
async function detectLintTool(workdir, language) {
|
|
22559
22678
|
if (language === "go")
|
|
@@ -22581,7 +22700,7 @@ async function detectProjectProfile(workdir, existing) {
|
|
|
22581
22700
|
const lintTool = existing.lintTool !== undefined ? existing.lintTool : await detectLintTool(workdir, language);
|
|
22582
22701
|
return { language, type, testFramework, lintTool };
|
|
22583
22702
|
}
|
|
22584
|
-
var _detectorDeps, WEB_DEPS, API_DEPS;
|
|
22703
|
+
var _detectorDeps, WEB_DEPS, API_DEPS, _languageCache;
|
|
22585
22704
|
var init_detector = __esm(() => {
|
|
22586
22705
|
_detectorDeps = {
|
|
22587
22706
|
async fileExists(path) {
|
|
@@ -22602,6 +22721,7 @@ var init_detector = __esm(() => {
|
|
|
22602
22721
|
};
|
|
22603
22722
|
WEB_DEPS = new Set(["react", "next", "vue", "nuxt"]);
|
|
22604
22723
|
API_DEPS = new Set(["express", "fastify", "hono"]);
|
|
22724
|
+
_languageCache = new Map;
|
|
22605
22725
|
});
|
|
22606
22726
|
|
|
22607
22727
|
// src/test-runners/conventions.ts
|
|
@@ -23730,7 +23850,10 @@ async function detectNaxMonoLayout(workdir) {
|
|
|
23730
23850
|
} catch {}
|
|
23731
23851
|
return dirs;
|
|
23732
23852
|
}
|
|
23733
|
-
|
|
23853
|
+
function clearWorkspaceCache() {
|
|
23854
|
+
_workspaceCache.clear();
|
|
23855
|
+
}
|
|
23856
|
+
async function discoverWorkspacePackagesUncached(workdir) {
|
|
23734
23857
|
const [fromPnpm, fromNpm, fromLerna, fromTurboNx, fromNaxMono] = await Promise.all([
|
|
23735
23858
|
detectPnpmWorkspace(workdir),
|
|
23736
23859
|
detectNpmWorkspaces(workdir),
|
|
@@ -23749,7 +23872,14 @@ async function discoverWorkspacePackages(workdir) {
|
|
|
23749
23872
|
}
|
|
23750
23873
|
return unique;
|
|
23751
23874
|
}
|
|
23752
|
-
|
|
23875
|
+
async function discoverWorkspacePackages(workdir) {
|
|
23876
|
+
if (_workspaceCache.has(workdir))
|
|
23877
|
+
return _workspaceCache.get(workdir) ?? [];
|
|
23878
|
+
const result = await discoverWorkspacePackagesUncached(workdir);
|
|
23879
|
+
_workspaceCache.set(workdir, result);
|
|
23880
|
+
return result;
|
|
23881
|
+
}
|
|
23882
|
+
var _workspaceDeps, _workspaceCache;
|
|
23753
23883
|
var init_workspace = __esm(() => {
|
|
23754
23884
|
init_logger2();
|
|
23755
23885
|
_workspaceDeps = {
|
|
@@ -23762,6 +23892,7 @@ var init_workspace = __esm(() => {
|
|
|
23762
23892
|
spawn: Bun.spawn,
|
|
23763
23893
|
glob: (pattern, cwd) => new Bun.Glob(pattern).scan({ cwd, onlyFiles: false })
|
|
23764
23894
|
};
|
|
23895
|
+
_workspaceCache = new Map;
|
|
23765
23896
|
});
|
|
23766
23897
|
|
|
23767
23898
|
// src/test-runners/detect.ts
|
|
@@ -24625,6 +24756,19 @@ var init_path_filters = __esm(() => {
|
|
|
24625
24756
|
|
|
24626
24757
|
// src/verification/smart-runner.ts
|
|
24627
24758
|
import { join as join8, relative as relative3 } from "path";
|
|
24759
|
+
function clearGitRootCache() {
|
|
24760
|
+
_gitRootCache.clear();
|
|
24761
|
+
}
|
|
24762
|
+
async function getGitRootMemo(workdir) {
|
|
24763
|
+
const cached2 = _gitRootCache.get(workdir);
|
|
24764
|
+
if (cached2 !== undefined)
|
|
24765
|
+
return cached2;
|
|
24766
|
+
const result = await _gitUtilDeps.getGitRoot(workdir);
|
|
24767
|
+
if (result !== null && result !== undefined) {
|
|
24768
|
+
_gitRootCache.set(workdir, result);
|
|
24769
|
+
}
|
|
24770
|
+
return result ?? null;
|
|
24771
|
+
}
|
|
24628
24772
|
function extractPatternSuffix(pattern) {
|
|
24629
24773
|
const lastStar = pattern.lastIndexOf("*");
|
|
24630
24774
|
if (lastStar === -1)
|
|
@@ -24644,28 +24788,28 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
|
|
|
24644
24788
|
return [];
|
|
24645
24789
|
const searchTerms = sourceFiles.flatMap(extractSearchTerms);
|
|
24646
24790
|
const testFilePaths = [];
|
|
24647
|
-
|
|
24648
|
-
const
|
|
24649
|
-
|
|
24650
|
-
|
|
24791
|
+
outer:
|
|
24792
|
+
for (const pattern of testFilePatterns) {
|
|
24793
|
+
const g = _bunDeps.glob(pattern);
|
|
24794
|
+
for await (const file3 of g.scan(workdir)) {
|
|
24795
|
+
testFilePaths.push(`${workdir}/${file3}`);
|
|
24796
|
+
if (testFilePaths.length >= MAX_GREP_TEST_FILES) {
|
|
24797
|
+
getSafeLogger()?.debug("smart-runner", "import-grep glob cap reached \u2014 results truncated", {
|
|
24798
|
+
cap: MAX_GREP_TEST_FILES
|
|
24799
|
+
});
|
|
24800
|
+
break outer;
|
|
24801
|
+
}
|
|
24802
|
+
}
|
|
24651
24803
|
}
|
|
24652
|
-
|
|
24653
|
-
const matched = [];
|
|
24654
|
-
for (const testFile of testFilePaths) {
|
|
24655
|
-
let content;
|
|
24804
|
+
const results = await Promise.all(testFilePaths.map(async (testFile) => {
|
|
24656
24805
|
try {
|
|
24657
|
-
content = await _bunDeps.file(testFile).text();
|
|
24806
|
+
const content = await _bunDeps.file(testFile).text();
|
|
24807
|
+
return searchTerms.some((t) => content.includes(t)) ? testFile : null;
|
|
24658
24808
|
} catch {
|
|
24659
|
-
|
|
24660
|
-
}
|
|
24661
|
-
for (const term of searchTerms) {
|
|
24662
|
-
if (content.includes(term)) {
|
|
24663
|
-
matched.push(testFile);
|
|
24664
|
-
break;
|
|
24665
|
-
}
|
|
24809
|
+
return null;
|
|
24666
24810
|
}
|
|
24667
|
-
}
|
|
24668
|
-
return
|
|
24811
|
+
}));
|
|
24812
|
+
return results.filter((p) => p !== null);
|
|
24669
24813
|
}
|
|
24670
24814
|
async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePatterns = [...DEFAULT_TEST_FILE_PATTERNS]) {
|
|
24671
24815
|
const testSuffixes = [...new Set(testFilePatterns.map(extractPatternSuffix).filter((s) => s !== null))];
|
|
@@ -24690,11 +24834,11 @@ async function mapSourceToTests(sourceFiles, workdir, packagePrefix, testFilePat
|
|
|
24690
24834
|
}
|
|
24691
24835
|
candidates.push(`${workdir}/${sourceWithoutExt}${suffix}`);
|
|
24692
24836
|
}
|
|
24693
|
-
|
|
24694
|
-
|
|
24695
|
-
|
|
24696
|
-
|
|
24697
|
-
}
|
|
24837
|
+
const existsFlags = await Promise.all(candidates.map((c) => _bunDeps.file(c).exists()));
|
|
24838
|
+
candidates.forEach((c, i) => {
|
|
24839
|
+
if (existsFlags[i])
|
|
24840
|
+
result.push(c);
|
|
24841
|
+
});
|
|
24698
24842
|
}
|
|
24699
24843
|
return result;
|
|
24700
24844
|
}
|
|
@@ -24733,7 +24877,7 @@ async function getChangedNonTestFiles(workdir, baseRef, packagePrefix, testFileR
|
|
|
24733
24877
|
const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(effectiveRepoRoot, packageDir);
|
|
24734
24878
|
let effectivePrefix = packagePrefix;
|
|
24735
24879
|
if (packagePrefix && repoRoot) {
|
|
24736
|
-
const gitRoot = await
|
|
24880
|
+
const gitRoot = await getGitRootMemo(workdir);
|
|
24737
24881
|
const extraPrefix2 = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
|
|
24738
24882
|
effectivePrefix = extraPrefix2 ? `${extraPrefix2}/${packagePrefix}` : packagePrefix;
|
|
24739
24883
|
}
|
|
@@ -24760,7 +24904,7 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
|
|
|
24760
24904
|
`).filter(Boolean);
|
|
24761
24905
|
const packageDir = packagePrefix ? join8(repoRoot, packagePrefix) : undefined;
|
|
24762
24906
|
const ignoreMatchers = naxIgnoreIndex?.getMatchers(packageDir) ?? await resolveNaxIgnorePatterns(repoRoot, packageDir);
|
|
24763
|
-
const gitRoot = await
|
|
24907
|
+
const gitRoot = await getGitRootMemo(workdir);
|
|
24764
24908
|
const extraPrefix = gitRoot && gitRoot !== repoRoot ? relative3(gitRoot, repoRoot) : "";
|
|
24765
24909
|
const effectivePrefix = packagePrefix ? extraPrefix ? `${extraPrefix}/${packagePrefix}` : packagePrefix : undefined;
|
|
24766
24910
|
const scopedRaw = effectivePrefix ? lines.filter((f) => f.startsWith(`${effectivePrefix}/`)) : lines;
|
|
@@ -24771,8 +24915,9 @@ async function getChangedTestFiles(workdir, repoRoot, baseRef, packagePrefix, te
|
|
|
24771
24915
|
return [];
|
|
24772
24916
|
}
|
|
24773
24917
|
}
|
|
24774
|
-
var _bunDeps, _gitUtilDeps, _smartRunnerDeps;
|
|
24918
|
+
var _bunDeps, MAX_GREP_TEST_FILES = 200, _gitUtilDeps, _gitRootCache, _smartRunnerDeps;
|
|
24775
24919
|
var init_smart_runner = __esm(() => {
|
|
24920
|
+
init_logger2();
|
|
24776
24921
|
init_conventions();
|
|
24777
24922
|
init_git();
|
|
24778
24923
|
init_path_filters();
|
|
@@ -24783,6 +24928,7 @@ var init_smart_runner = __esm(() => {
|
|
|
24783
24928
|
_gitUtilDeps = {
|
|
24784
24929
|
getGitRoot
|
|
24785
24930
|
};
|
|
24931
|
+
_gitRootCache = new Map;
|
|
24786
24932
|
_smartRunnerDeps = {
|
|
24787
24933
|
glob: _bunDeps.glob,
|
|
24788
24934
|
file: _bunDeps.file,
|
|
@@ -25140,31 +25286,50 @@ async function resolveSourceGlob(override, packageDir) {
|
|
|
25140
25286
|
const language = await _codeNeighborDeps.detectLanguage(packageDir);
|
|
25141
25287
|
return (language && SOURCE_GLOB_BY_LANGUAGE[language]) ?? FALLBACK_SOURCE_GLOB;
|
|
25142
25288
|
}
|
|
25143
|
-
|
|
25289
|
+
function scanDirectory(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCtx) {
|
|
25290
|
+
const { files, truncated } = _codeNeighborDeps.glob(sourceGlob, workdir, ignoreMatchers, maxGlobFiles, globCtx);
|
|
25291
|
+
return { workdir, files, truncated };
|
|
25292
|
+
}
|
|
25293
|
+
async function readCached(absolutePath, cache) {
|
|
25294
|
+
const cached2 = cache.get(absolutePath);
|
|
25295
|
+
if (cached2 !== undefined)
|
|
25296
|
+
return cached2;
|
|
25297
|
+
try {
|
|
25298
|
+
const content = await _codeNeighborDeps.readFile(absolutePath);
|
|
25299
|
+
cache.set(absolutePath, content);
|
|
25300
|
+
return content;
|
|
25301
|
+
} catch {
|
|
25302
|
+
cache.set(absolutePath, "");
|
|
25303
|
+
return null;
|
|
25304
|
+
}
|
|
25305
|
+
}
|
|
25306
|
+
async function collectNeighbors(filePath, workdir, scannedDirs, contentCache, siblingTestContext) {
|
|
25144
25307
|
const neighbors = new Set;
|
|
25145
25308
|
let anyTruncated = false;
|
|
25146
|
-
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
25150
|
-
|
|
25151
|
-
|
|
25309
|
+
const ownAbsPath = join10(workdir, filePath);
|
|
25310
|
+
if (await _codeNeighborDeps.fileExists(ownAbsPath)) {
|
|
25311
|
+
const ownContent = await readCached(ownAbsPath, contentCache);
|
|
25312
|
+
if (ownContent !== null && ownContent.length > 0) {
|
|
25313
|
+
for (const spec of parseImportSpecifiers(ownContent)) {
|
|
25314
|
+
const resolved = resolveImport(spec, filePath, workdir);
|
|
25315
|
+
if (resolved && resolved !== filePath)
|
|
25316
|
+
neighbors.add(resolved);
|
|
25317
|
+
}
|
|
25152
25318
|
}
|
|
25153
25319
|
}
|
|
25154
25320
|
const fileBaseName = (filePath.split("/").pop() ?? filePath).replace(/\.[^.]+$/, "");
|
|
25155
25321
|
const fileNoExt = filePath.replace(/\.[^.]+$/, "");
|
|
25156
|
-
|
|
25157
|
-
const { files: srcFiles, truncated }
|
|
25158
|
-
|
|
25159
|
-
|
|
25160
|
-
|
|
25161
|
-
|
|
25162
|
-
|
|
25163
|
-
|
|
25164
|
-
|
|
25165
|
-
|
|
25166
|
-
|
|
25167
|
-
if (content.includes(fileBaseName)) {
|
|
25322
|
+
outer:
|
|
25323
|
+
for (const { workdir: scanWorkdir, files: srcFiles, truncated } of scannedDirs) {
|
|
25324
|
+
if (truncated)
|
|
25325
|
+
anyTruncated = true;
|
|
25326
|
+
for (const srcFile of srcFiles) {
|
|
25327
|
+
if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
|
|
25328
|
+
break outer;
|
|
25329
|
+
if (srcFile === filePath)
|
|
25330
|
+
continue;
|
|
25331
|
+
const content = await readCached(join10(scanWorkdir, srcFile), contentCache);
|
|
25332
|
+
if (content?.includes(fileBaseName)) {
|
|
25168
25333
|
for (const spec of parseImportSpecifiers(content)) {
|
|
25169
25334
|
const resolved = resolveImport(spec, srcFile, scanWorkdir);
|
|
25170
25335
|
if (resolved === filePath || resolved === fileNoExt) {
|
|
@@ -25173,17 +25338,8 @@ async function collectNeighbors(filePath, workdir, sourceGlob, maxGlobFiles, ext
|
|
|
25173
25338
|
}
|
|
25174
25339
|
}
|
|
25175
25340
|
}
|
|
25176
|
-
}
|
|
25177
|
-
}
|
|
25178
|
-
};
|
|
25179
|
-
await scanForReverseDeps(workdir);
|
|
25180
|
-
if (extraGlobWorkdirs) {
|
|
25181
|
-
for (const extraDir of extraGlobWorkdirs) {
|
|
25182
|
-
if (neighbors.size >= MAX_NEIGHBORS_PER_FILE)
|
|
25183
|
-
break;
|
|
25184
|
-
await scanForReverseDeps(extraDir);
|
|
25341
|
+
}
|
|
25185
25342
|
}
|
|
25186
|
-
}
|
|
25187
25343
|
if (siblingTestContext && !isTestFile2(filePath, siblingTestContext.regex)) {
|
|
25188
25344
|
const candidates = deriveSiblingTestCandidates(filePath, siblingTestContext.globs);
|
|
25189
25345
|
let chosen = null;
|
|
@@ -25246,10 +25402,17 @@ class CodeNeighborProvider {
|
|
|
25246
25402
|
const ignoreMatchers = request.naxIgnoreIndex?.getMatchers(workdir);
|
|
25247
25403
|
const sourceGlob = await resolveSourceGlob(this.sourceGlobOverride, request.packageDir);
|
|
25248
25404
|
const globCtx = { storyId: request.storyId, packageDir: request.packageDir };
|
|
25405
|
+
const scannedDirs = [scanDirectory(sourceGlob, workdir, ignoreMatchers, this.maxGlobFiles, globCtx)];
|
|
25406
|
+
if (extraGlobWorkdirs) {
|
|
25407
|
+
for (const extraDir of extraGlobWorkdirs) {
|
|
25408
|
+
scannedDirs.push(scanDirectory(sourceGlob, extraDir, ignoreMatchers, this.maxGlobFiles, globCtx));
|
|
25409
|
+
}
|
|
25410
|
+
}
|
|
25411
|
+
const contentCache = new Map;
|
|
25249
25412
|
const sections = [];
|
|
25250
25413
|
let anyTruncated = false;
|
|
25251
25414
|
for (const file3 of filesToProcess) {
|
|
25252
|
-
const { neighbors, truncated } = await collectNeighbors(file3, workdir,
|
|
25415
|
+
const { neighbors, truncated } = await collectNeighbors(file3, workdir, scannedDirs, contentCache, siblingTestContext);
|
|
25253
25416
|
if (truncated)
|
|
25254
25417
|
anyTruncated = true;
|
|
25255
25418
|
if (neighbors.length > 0) {
|
|
@@ -26167,13 +26330,25 @@ function buildPullToolDescriptors(stageToolNames, pullConfig) {
|
|
|
26167
26330
|
const allowed = pullConfig.allowedTools;
|
|
26168
26331
|
return stageToolNames.filter((name) => allowed.length === 0 || allowed.includes(name)).map((name) => PULL_TOOL_REGISTRY[name]).filter((d) => d !== undefined).map((d) => ({ ...d, maxCallsPerSession: pullConfig.maxCallsPerSession ?? d.maxCallsPerSession }));
|
|
26169
26332
|
}
|
|
26170
|
-
async function fetchWithTimeout(provider, request) {
|
|
26333
|
+
async function fetchWithTimeout(provider, request, timeoutMs = PROVIDER_FETCH_TIMEOUT_MS) {
|
|
26334
|
+
const controller = new AbortController;
|
|
26171
26335
|
let handle;
|
|
26336
|
+
let timedOut = false;
|
|
26172
26337
|
const timeout = new Promise((_, reject) => {
|
|
26173
|
-
handle = setTimeout(() =>
|
|
26338
|
+
handle = setTimeout(() => {
|
|
26339
|
+
timedOut = true;
|
|
26340
|
+
controller.abort();
|
|
26341
|
+
reject(new Error(`Provider "${provider.id}" timed out`));
|
|
26342
|
+
}, timeoutMs);
|
|
26343
|
+
});
|
|
26344
|
+
const fetchPromise = provider.fetch(request, controller.signal).then((result) => result, (err) => {
|
|
26345
|
+
if (timedOut) {
|
|
26346
|
+
return new Promise(() => {});
|
|
26347
|
+
}
|
|
26348
|
+
throw err;
|
|
26174
26349
|
});
|
|
26175
26350
|
try {
|
|
26176
|
-
return await Promise.race([
|
|
26351
|
+
return await Promise.race([fetchPromise, timeout]);
|
|
26177
26352
|
} finally {
|
|
26178
26353
|
clearTimeout(handle);
|
|
26179
26354
|
}
|
|
@@ -26812,12 +26987,7 @@ class GitHistoryProvider {
|
|
|
26812
26987
|
return { chunks: [], pullTools: [] };
|
|
26813
26988
|
}
|
|
26814
26989
|
const filesToProcess = touchedFiles.filter(isRelativeAndSafe).slice(0, MAX_FILES2);
|
|
26815
|
-
const sections =
|
|
26816
|
-
for (const file3 of filesToProcess) {
|
|
26817
|
-
const section = await fetchFileHistory(file3, workdir);
|
|
26818
|
-
if (section)
|
|
26819
|
-
sections.push(section);
|
|
26820
|
-
}
|
|
26990
|
+
const sections = (await Promise.all(filesToProcess.map((file3) => fetchFileHistory(file3, workdir)))).filter((section) => section !== null);
|
|
26821
26991
|
if (sections.length === 0) {
|
|
26822
26992
|
return { chunks: [], pullTools: [] };
|
|
26823
26993
|
}
|
|
@@ -27323,6 +27493,75 @@ function isStalled(prd) {
|
|
|
27323
27493
|
return remaining.every((s) => s.status === "blocked" || s.status === "failed" || s.status === "paused" || s.status === "regression-failed" || s.dependencies.some((dep) => blockedIds.has(dep)));
|
|
27324
27494
|
}
|
|
27325
27495
|
|
|
27496
|
+
// src/prd/verbatim-fidelity.ts
|
|
27497
|
+
function normalizeWs(text) {
|
|
27498
|
+
return text.replace(/\s+/g, " ").trim();
|
|
27499
|
+
}
|
|
27500
|
+
function stripBackticks(text) {
|
|
27501
|
+
return text.replace(/`/g, "");
|
|
27502
|
+
}
|
|
27503
|
+
function canonical(text) {
|
|
27504
|
+
return normalizeWs(stripBackticks(text));
|
|
27505
|
+
}
|
|
27506
|
+
function leadingTagGroup(line) {
|
|
27507
|
+
return line.match(LEADING_TAG_GROUP)?.[1] ?? null;
|
|
27508
|
+
}
|
|
27509
|
+
function isVerbatimBullet(line) {
|
|
27510
|
+
const tags = leadingTagGroup(line);
|
|
27511
|
+
return tags !== null && /\[verbatim\]/i.test(tags);
|
|
27512
|
+
}
|
|
27513
|
+
function isContinuation(line) {
|
|
27514
|
+
if (line.trim().length === 0)
|
|
27515
|
+
return false;
|
|
27516
|
+
if (HEADING.test(line))
|
|
27517
|
+
return false;
|
|
27518
|
+
if (LIST_ITEM_START.test(line))
|
|
27519
|
+
return false;
|
|
27520
|
+
return true;
|
|
27521
|
+
}
|
|
27522
|
+
function stripTagPrefix(block) {
|
|
27523
|
+
return block.replace(LEADING_TAG_GROUP, "");
|
|
27524
|
+
}
|
|
27525
|
+
function extractVerbatimAcs(specContent) {
|
|
27526
|
+
const lines = specContent.split(`
|
|
27527
|
+
`);
|
|
27528
|
+
const blocks = [];
|
|
27529
|
+
for (let i = 0;i < lines.length; i++) {
|
|
27530
|
+
if (!isVerbatimBullet(lines[i]))
|
|
27531
|
+
continue;
|
|
27532
|
+
const parts = [lines[i].trim()];
|
|
27533
|
+
let j = i + 1;
|
|
27534
|
+
while (j < lines.length && isContinuation(lines[j])) {
|
|
27535
|
+
parts.push(lines[j].trim());
|
|
27536
|
+
j += 1;
|
|
27537
|
+
}
|
|
27538
|
+
blocks.push(parts.join(" "));
|
|
27539
|
+
i = j - 1;
|
|
27540
|
+
}
|
|
27541
|
+
return blocks;
|
|
27542
|
+
}
|
|
27543
|
+
function prdAcPayloads(prd) {
|
|
27544
|
+
return (prd.userStories ?? []).flatMap((story) => (story.acceptanceCriteria ?? []).map(canonical));
|
|
27545
|
+
}
|
|
27546
|
+
function findMissingVerbatimAcs(specContent, prd) {
|
|
27547
|
+
const prdAcs = prdAcPayloads(prd);
|
|
27548
|
+
const missing = [];
|
|
27549
|
+
for (const block of extractVerbatimAcs(specContent)) {
|
|
27550
|
+
const payload = canonical(stripTagPrefix(block));
|
|
27551
|
+
if (payload.length === 0)
|
|
27552
|
+
continue;
|
|
27553
|
+
if (!prdAcs.some((ac) => ac.includes(payload)))
|
|
27554
|
+
missing.push(block);
|
|
27555
|
+
}
|
|
27556
|
+
return missing;
|
|
27557
|
+
}
|
|
27558
|
+
var LEADING_TAG_GROUP, LIST_ITEM_START, HEADING;
|
|
27559
|
+
var init_verbatim_fidelity = __esm(() => {
|
|
27560
|
+
LEADING_TAG_GROUP = /^\s*(?:[-*]|\d+\.)?\s*((?:\[[a-z][a-z-]*\]\s*)+)/i;
|
|
27561
|
+
LIST_ITEM_START = /^\s*(?:[-*]|\d+\.)\s/;
|
|
27562
|
+
HEADING = /^\s*#/;
|
|
27563
|
+
});
|
|
27564
|
+
|
|
27326
27565
|
// src/prd/validate.ts
|
|
27327
27566
|
function validateStoryId(id) {
|
|
27328
27567
|
if (!id || id.length === 0) {
|
|
@@ -27727,6 +27966,7 @@ function markStoryPaused(prd, storyId) {
|
|
|
27727
27966
|
var PRD_MAX_FILE_SIZE;
|
|
27728
27967
|
var init_prd = __esm(() => {
|
|
27729
27968
|
init_json_file();
|
|
27969
|
+
init_verbatim_fidelity();
|
|
27730
27970
|
init_schema2();
|
|
27731
27971
|
PRD_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
27732
27972
|
});
|
|
@@ -30731,6 +30971,7 @@ Flag issues only when you have confirmed:
|
|
|
30731
30971
|
Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.`, SEMANTIC_OUTPUT_SCHEMA = `Respond with JSON only \u2014 no explanation text before or after:
|
|
30732
30972
|
{
|
|
30733
30973
|
"passed": boolean,
|
|
30974
|
+
"inspectedFiles": ["relative/path/you/actually/opened.ts"],
|
|
30734
30975
|
"findings": [
|
|
30735
30976
|
{
|
|
30736
30977
|
"severity": "error" | "warning" | "info" | "unverifiable",
|
|
@@ -30754,7 +30995,8 @@ Notes:
|
|
|
30754
30995
|
- \`acIndex\` is required when severity is "error" (1-based, into the Acceptance Criteria list above).
|
|
30755
30996
|
- \`acQuote\` is optional advisory metadata for human auditors \u2014 not validated.
|
|
30756
30997
|
- Omit both for "warning", "info", "unverifiable".
|
|
30757
|
-
|
|
30998
|
+
- \`inspectedFiles\` must list the relative paths you actually opened while reviewing. A \`passed:true\` verdict with an empty or absent \`inspectedFiles\` is invalid \u2014 walk each AC against the real files before passing.
|
|
30999
|
+
If all ACs are correctly implemented after inspecting the code, respond with { "passed": true, "inspectedFiles": ["..."], "findings": [] }.`, ReviewPromptBuilder;
|
|
30758
31000
|
var init_review_builder = __esm(() => {
|
|
30759
31001
|
SEMANTIC_ROLE = "You are a semantic code reviewer with access to the repository files. " + "Your job is to walk each acceptance criterion (AC) and judge whether the production code fulfills it \u2014 fully, partially, or not at all. " + "Test coverage gaps and convention/lint issues are out of scope \u2014 adversarial review and lint/typecheck handle those.";
|
|
30760
31002
|
ReviewPromptBuilder = class ReviewPromptBuilder {
|
|
@@ -30807,6 +31049,16 @@ Respond with a condensed summary:
|
|
|
30807
31049
|
- Keep \`verifiedBy\` for every finding. If \`verifiedBy.observed\` is long, abbreviate it to one line \u2014 never drop the field.
|
|
30808
31050
|
Output ONLY a complete, valid JSON object. It must start with { and end with }.
|
|
30809
31051
|
Schema: {"passed": boolean, "findings": [{"severity": string, "category": string, "file": string, "line": number, "issue": string, "suggestion": string, "verifiedBy": {"command": string, "file": string, "line": number, "observed": string}}]}`;
|
|
31052
|
+
}
|
|
31053
|
+
static demandInspection() {
|
|
31054
|
+
return `Your previous review returned \`passed:true\` with no findings and an empty (or absent) \`inspectedFiles\` list. That means you did not open any of the changed files \u2014 a verdict reached without reading the code is not valid.
|
|
31055
|
+
|
|
31056
|
+
Use your file-reading tools NOW to open the changed files and walk each acceptance criterion against the real implementation. Then re-issue your verdict as the same JSON object.
|
|
31057
|
+
|
|
31058
|
+
Rules:
|
|
31059
|
+
- Populate \`inspectedFiles\` with the relative paths you actually opened.
|
|
31060
|
+
- If, after genuinely inspecting the code, every AC is satisfied, you may still return \`passed:true\` \u2014 but \`inspectedFiles\` must list the files you read.
|
|
31061
|
+
- Return ONLY the JSON object \u2014 no markdown fences, no explanation.`;
|
|
30810
31062
|
}
|
|
30811
31063
|
static requoteVerbatim(opts) {
|
|
30812
31064
|
const file3 = opts.finding.verifiedBy?.file ?? opts.finding.file;
|
|
@@ -30981,6 +31233,13 @@ What new exported units lack corresponding test files?
|
|
|
30981
31233
|
- New public functions that only appear in implementation, not in tests
|
|
30982
31234
|
- Acceptance criteria that touch a code path with no test coverage
|
|
30983
31235
|
|
|
31236
|
+
**Placeholder / tautological tests (blocking).** A test that exists but verifies nothing is worse than a missing test \u2014 it manufactures a false green. Treat the following as a **\`severity:"error"\`, \`category:"test-gap"\`** finding whenever the fake test is the only coverage for an acceptance criterion:
|
|
31237
|
+
- Bodies that always pass: \`expect(true).toBe(true)\`, \`expect(x).toBe(x)\`, \`expect(1).toBe(1)\`, an empty test body, or \`assert(true)\`.
|
|
31238
|
+
- Tests skipped/disabled (\`it.skip\`, \`test.todo\`, \`xit\`, commented-out assertions) that an AC depends on.
|
|
31239
|
+
- Assertions that never exercise the implementation (e.g. asserting on a literal, not on a value the production code produced).
|
|
31240
|
+
|
|
31241
|
+
For each such finding: set \`acIndex\` to the AC the fake test purports to cover, \`acQuote\` to a verbatim substring of that AC, and \`verifiedBy.observed\` to the placeholder line itself (e.g. \`expect(true).toBe(true)\`). Do **not** downgrade these to \`warning\` \u2014 a green suite built on placeholder assertions is a failing implementation with hidden evidence.
|
|
31242
|
+
|
|
30984
31243
|
### 5. Convention Breaks
|
|
30985
31244
|
What pattern exists elsewhere that this code does not follow?
|
|
30986
31245
|
- Logger calls missing \`storyId\` as first key in data object
|
|
@@ -31000,6 +31259,7 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
|
|
|
31000
31259
|
\`\`\`json
|
|
31001
31260
|
{
|
|
31002
31261
|
"passed": true | false,
|
|
31262
|
+
"inspectedFiles": ["relative/path/you/actually/opened.ts"],
|
|
31003
31263
|
"findings": [
|
|
31004
31264
|
{
|
|
31005
31265
|
"severity": "error" | "warning" | "info" | "unverifiable",
|
|
@@ -31021,6 +31281,8 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
|
|
|
31021
31281
|
}
|
|
31022
31282
|
\`\`\`
|
|
31023
31283
|
|
|
31284
|
+
**No rubber-stamping:** \`inspectedFiles\` must list the relative paths you actually opened with your tools while reviewing. A \`passed:true\` verdict with an empty or absent \`inspectedFiles\` is invalid \u2014 it means you never looked at the code. Fetch the diff and open the changed files before forming any verdict.
|
|
31285
|
+
|
|
31024
31286
|
Severity guide:
|
|
31025
31287
|
- \`"error"\`: confident this will cause real failure or regression
|
|
31026
31288
|
- \`"warning"\`: fragile or incomplete but may ship without immediate breakage
|
|
@@ -31050,6 +31312,8 @@ Worked example:
|
|
|
31050
31312
|
|
|
31051
31313
|
**Convention / coding-standard violations almost always belong as \`"info"\`** unless an AC specifically names the convention or the symbol it concerns.
|
|
31052
31314
|
|
|
31315
|
+
**Exception \u2014 the trap does NOT apply to \`category:"test-gap"\` findings.** A fake/placeholder/missing test is the *absence* of verification for an AC's behaviour; it cannot name a symbol that is present in the (worthless) test file, because the defect is precisely that the symbol-under-test is never exercised. A \`test-gap\` finding is grounded by the AC whose behaviour goes unverified \u2014 cite that AC's \`acIndex\` and a verbatim \`acQuote\` substring from it, and keep severity \`"error"\`. The symbol-naming requirement is waived for this category.
|
|
31316
|
+
|
|
31053
31317
|
**Scope constraints are not Acceptance Criteria:**
|
|
31054
31318
|
The story description may contain a "Scope" section with "In:" and "Out:" bullets. These are implementation guidelines, not ACs. A finding about code changed outside the stated scope (e.g., a file listed under "Out:") cannot cite a scope constraint as its \`acQuote\`/\`acIndex\` because scope text is not in the numbered AC list. Emit scope-violation findings as \`"warning"\` \u2014 never \`"error"\`. Never use \`acIndex: 0\`; \`acIndex\` is 1-based (first AC bullet = 1).
|
|
31055
31319
|
|
|
@@ -31138,6 +31402,16 @@ Rules:
|
|
|
31138
31402
|
- observed must be a 1-3 line excerpt that proves the claim, taken from at or near line ${line}.
|
|
31139
31403
|
- If after reading the file you cannot find anything that proves the claim, set observed to "".
|
|
31140
31404
|
- Do not return a full review. Do not include markdown fences or explanation.`;
|
|
31405
|
+
}
|
|
31406
|
+
static demandInspection() {
|
|
31407
|
+
return `Your previous review returned \`passed:true\` with no findings and an empty (or absent) \`inspectedFiles\` list. That means you did not open any of the changed files \u2014 a verdict reached without inspecting the code is not valid.
|
|
31408
|
+
|
|
31409
|
+
Use your git and file-reading tools NOW to fetch the diff and open the changed files for this story. Then re-issue your verdict as the same JSON object.
|
|
31410
|
+
|
|
31411
|
+
Rules:
|
|
31412
|
+
- Populate \`inspectedFiles\` with the relative paths you actually opened.
|
|
31413
|
+
- If, after genuinely inspecting the code, there is truly nothing to flag, you may still return \`passed:true\` \u2014 but \`inspectedFiles\` must list the files you read.
|
|
31414
|
+
- Return ONLY the JSON object \u2014 no markdown fences, no explanation.`;
|
|
31141
31415
|
}
|
|
31142
31416
|
static DROP_CODE_MESSAGES_QUOTE = {
|
|
31143
31417
|
missing_ac_quote: "no `acQuote` field was provided \u2014 every blocking finding must cite an AC",
|
|
@@ -31517,7 +31791,7 @@ var init_acceptance_builder = __esm(() => {
|
|
|
31517
31791
|
});
|
|
31518
31792
|
|
|
31519
31793
|
// src/review/ac-quote-validator.ts
|
|
31520
|
-
function
|
|
31794
|
+
function normalizeWs2(s) {
|
|
31521
31795
|
return s.replace(/\s+/g, " ").trim();
|
|
31522
31796
|
}
|
|
31523
31797
|
function stripMarkdownInline(s) {
|
|
@@ -31552,11 +31826,14 @@ function validateAcQuote(finding, acceptanceCriteria) {
|
|
|
31552
31826
|
if (typeof acIndex !== "number" || acIndex < 1 || acIndex > acceptanceCriteria.length) {
|
|
31553
31827
|
return { valid: false, code: "ac_index_out_of_range" };
|
|
31554
31828
|
}
|
|
31555
|
-
const acText =
|
|
31556
|
-
const normalizedQuote =
|
|
31829
|
+
const acText = normalizeWs2(stripMarkdownInline(acceptanceCriteria[acIndex - 1]));
|
|
31830
|
+
const normalizedQuote = normalizeWs2(stripMarkdownInline(acQuote));
|
|
31557
31831
|
if (!acText.toLowerCase().includes(normalizedQuote.toLowerCase())) {
|
|
31558
31832
|
return { valid: false, code: "ac_quote_not_substring" };
|
|
31559
31833
|
}
|
|
31834
|
+
if (finding.category === "test-gap") {
|
|
31835
|
+
return { valid: true };
|
|
31836
|
+
}
|
|
31560
31837
|
const keywords = extractLocusKeywords(finding);
|
|
31561
31838
|
if (keywords.length === 0) {
|
|
31562
31839
|
return { valid: false, code: "ac_quote_does_not_constrain_locus" };
|
|
@@ -31875,6 +32152,10 @@ var init_semantic_evidence = __esm(() => {
|
|
|
31875
32152
|
});
|
|
31876
32153
|
|
|
31877
32154
|
// src/review/finding-filters.ts
|
|
32155
|
+
function hasInspectionTrail(raw) {
|
|
32156
|
+
const files = raw?.inspectedFiles;
|
|
32157
|
+
return Array.isArray(files) && files.some((f) => typeof f === "string" && f.trim().length > 0);
|
|
32158
|
+
}
|
|
31878
32159
|
async function substantiateAdversarialFindings(opts) {
|
|
31879
32160
|
const { findings, workdir, storyId, blockingThreshold } = opts;
|
|
31880
32161
|
return Promise.all(findings.map(async (finding) => {
|
|
@@ -31906,9 +32187,9 @@ function parseRequoteResponse(output) {
|
|
|
31906
32187
|
const parsed = tryParseLLMJson(output);
|
|
31907
32188
|
if (!isRecord(parsed))
|
|
31908
32189
|
return null;
|
|
31909
|
-
const
|
|
31910
|
-
if (
|
|
31911
|
-
return
|
|
32190
|
+
const canonical2 = extractCanonical(parsed);
|
|
32191
|
+
if (canonical2)
|
|
32192
|
+
return canonical2;
|
|
31912
32193
|
const findings = parsed.findings;
|
|
31913
32194
|
if (!Array.isArray(findings) || findings.length !== 1)
|
|
31914
32195
|
return null;
|
|
@@ -32102,6 +32383,23 @@ async function performAdversarialReground(turn, firstParsed, drops, ctx) {
|
|
|
32102
32383
|
output: withRepromptMarker(turn.output, { dropCount, outcome: "still-dropped", costUsd })
|
|
32103
32384
|
};
|
|
32104
32385
|
}
|
|
32386
|
+
async function maybeRepromptForInspection(turn, parsed, rawObject, ctx) {
|
|
32387
|
+
if (ctx.input.adversarialConfig.demandInspectionTrail === false)
|
|
32388
|
+
return null;
|
|
32389
|
+
if (!parsed.passed || parsed.findings.length !== 0)
|
|
32390
|
+
return null;
|
|
32391
|
+
if (hasInspectionTrail(rawObject))
|
|
32392
|
+
return null;
|
|
32393
|
+
const secondTurn = await ctx.send(AdversarialReviewPromptBuilder.demandInspection());
|
|
32394
|
+
const costUsd = (turn.estimatedCostUsd ?? 0) + (secondTurn.estimatedCostUsd ?? 0);
|
|
32395
|
+
const secondParsed = validateAdversarialShape(tryParseLLMJson(secondTurn.output));
|
|
32396
|
+
getSafeLogger()?.warn("review", "Adversarial reviewer returned empty pass with no inspection trail \u2014 re-prompted", {
|
|
32397
|
+
storyId: ctx.input.story.id,
|
|
32398
|
+
event: "review.adversarial.inspection_trail.reprompted",
|
|
32399
|
+
recovered: secondParsed !== null
|
|
32400
|
+
});
|
|
32401
|
+
return secondParsed ? { ...turn, output: secondTurn.output, estimatedCostUsd: costUsd } : { ...turn, estimatedCostUsd: costUsd };
|
|
32402
|
+
}
|
|
32105
32403
|
var FAIL_OPEN, ADVERSARIAL_REQUOTE_RECOVERED_EVENT = "review.adversarial.finding.requote_recovered", ADVERSARIAL_REQUOTE_FAILED_EVENT = "review.adversarial.finding.requote_failed", DEFAULT_MAX_REQUOTES = 5, adversarialParseRetry = (input) => makeParseRetryStrategy({
|
|
32106
32404
|
validate: (parsed) => validateAdversarialShape(parsed) !== null,
|
|
32107
32405
|
reviewerKind: "adversarial",
|
|
@@ -32139,11 +32437,15 @@ var init_adversarial_review = __esm(() => {
|
|
|
32139
32437
|
retry: (input) => adversarialParseRetry(input),
|
|
32140
32438
|
async hopBody(initialPrompt, ctx) {
|
|
32141
32439
|
const turn = await ctx.sendWithParseRetry(initialPrompt);
|
|
32142
|
-
const
|
|
32440
|
+
const rawObject = tryParseLLMJson(turn.output);
|
|
32441
|
+
const parsed = validateAdversarialShape(rawObject);
|
|
32143
32442
|
if (!parsed)
|
|
32144
32443
|
return turn;
|
|
32145
32444
|
if (ctx.input.mode !== "ref")
|
|
32146
32445
|
return turn;
|
|
32446
|
+
const inspectionGuard = await maybeRepromptForInspection(turn, parsed, rawObject, ctx);
|
|
32447
|
+
if (inspectionGuard)
|
|
32448
|
+
return inspectionGuard;
|
|
32147
32449
|
const regroundEnabled = ctx.input.adversarialConfig.acRegroundOnDrop !== false;
|
|
32148
32450
|
if (regroundEnabled) {
|
|
32149
32451
|
const firstShape = { passed: parsed.passed, findings: parsed.findings };
|
|
@@ -33277,20 +33579,23 @@ function createDrainDeadline(deadlineMs) {
|
|
|
33277
33579
|
};
|
|
33278
33580
|
}
|
|
33279
33581
|
async function runQualityCommand(opts) {
|
|
33280
|
-
const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2 } = opts;
|
|
33582
|
+
const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS, env: env2, stripEnvVars } = opts;
|
|
33281
33583
|
const startTime = Date.now();
|
|
33282
33584
|
const logger = getSafeLogger();
|
|
33283
33585
|
logger?.info("quality", `Running ${commandName}`, { storyId, commandName, command, workdir });
|
|
33284
33586
|
try {
|
|
33587
|
+
const baseEnv = {
|
|
33588
|
+
...process.env
|
|
33589
|
+
};
|
|
33590
|
+
for (const key of stripEnvVars ?? []) {
|
|
33591
|
+
delete baseEnv[key];
|
|
33592
|
+
}
|
|
33285
33593
|
const proc = _qualityRunnerDeps.spawn({
|
|
33286
33594
|
cmd: ["/bin/sh", "-c", command],
|
|
33287
33595
|
cwd: workdir,
|
|
33288
33596
|
stdout: "pipe",
|
|
33289
33597
|
stderr: "pipe",
|
|
33290
|
-
env: {
|
|
33291
|
-
...process.env,
|
|
33292
|
-
...env2 ?? {}
|
|
33293
|
-
}
|
|
33598
|
+
env: { ...baseEnv, ...env2 ?? {} }
|
|
33294
33599
|
});
|
|
33295
33600
|
let timedOut = false;
|
|
33296
33601
|
let exitedBeforeSigkill = false;
|
|
@@ -33776,6 +34081,18 @@ var init_adapters = __esm(() => {
|
|
|
33776
34081
|
init_typecheck();
|
|
33777
34082
|
});
|
|
33778
34083
|
|
|
34084
|
+
// src/operations/verbatim-warn.ts
|
|
34085
|
+
function warnOnDroppedVerbatimAcs(prd, specContent, featureName) {
|
|
34086
|
+
const missing = findMissingVerbatimAcs(specContent, prd);
|
|
34087
|
+
if (missing.length > 0) {
|
|
34088
|
+
getSafeLogger()?.warn("plan", "[verbatim] spec acceptance criteria dropped from PRD \u2014 run spec-review --prd before executing", { featureName, missingCount: missing.length, missing });
|
|
34089
|
+
}
|
|
34090
|
+
}
|
|
34091
|
+
var init_verbatim_warn = __esm(() => {
|
|
34092
|
+
init_logger2();
|
|
34093
|
+
init_prd();
|
|
34094
|
+
});
|
|
34095
|
+
|
|
33779
34096
|
// src/operations/plan.ts
|
|
33780
34097
|
var planInteractiveOp;
|
|
33781
34098
|
var init_plan = __esm(() => {
|
|
@@ -33783,6 +34100,7 @@ var init_plan = __esm(() => {
|
|
|
33783
34100
|
init_config();
|
|
33784
34101
|
init_schema2();
|
|
33785
34102
|
init_prompts();
|
|
34103
|
+
init_verbatim_warn();
|
|
33786
34104
|
planInteractiveOp = {
|
|
33787
34105
|
kind: "run",
|
|
33788
34106
|
name: "plan-interactive",
|
|
@@ -33822,9 +34140,10 @@ ${outputFormat}`, overridable: false }
|
|
|
33822
34140
|
parse(output, input, _ctx) {
|
|
33823
34141
|
return validatePlanOutput(output, input.featureName, input.branchName);
|
|
33824
34142
|
},
|
|
33825
|
-
verify: async (parsed,
|
|
34143
|
+
verify: async (parsed, input, _ctx) => {
|
|
33826
34144
|
if (!parsed.userStories || parsed.userStories.length === 0)
|
|
33827
34145
|
return null;
|
|
34146
|
+
warnOnDroppedVerbatimAcs(parsed, input.specContent, input.featureName);
|
|
33828
34147
|
return parsed;
|
|
33829
34148
|
},
|
|
33830
34149
|
recover: async (input, ctx) => {
|
|
@@ -33875,13 +34194,39 @@ function validateRefinedPrd(prd) {
|
|
|
33875
34194
|
validateRefinedStory(story);
|
|
33876
34195
|
return prd;
|
|
33877
34196
|
}
|
|
33878
|
-
|
|
34197
|
+
async function readMissingVerbatimAcs(input) {
|
|
34198
|
+
const content = await _planRefineDeps.readFile(input.outputPath);
|
|
34199
|
+
if (!content)
|
|
34200
|
+
return [];
|
|
34201
|
+
try {
|
|
34202
|
+
const prd = validatePlanOutput(content, input.featureName, input.branchName);
|
|
34203
|
+
return findMissingVerbatimAcs(input.specContent, prd);
|
|
34204
|
+
} catch {
|
|
34205
|
+
getSafeLogger()?.debug("plan", "Skipped [verbatim] self-heal \u2014 draft PRD not yet parseable", {
|
|
34206
|
+
featureName: input.featureName
|
|
34207
|
+
});
|
|
34208
|
+
return [];
|
|
34209
|
+
}
|
|
34210
|
+
}
|
|
34211
|
+
var _planRefineDeps, NEGATIVE_PATH_TOKENS, planRefineOp;
|
|
33879
34212
|
var init_plan_refine = __esm(() => {
|
|
33880
34213
|
init_retry();
|
|
33881
34214
|
init_config();
|
|
33882
34215
|
init_errors();
|
|
34216
|
+
init_logger2();
|
|
34217
|
+
init_prd();
|
|
33883
34218
|
init_schema2();
|
|
33884
34219
|
init_prompts();
|
|
34220
|
+
init_verbatim_warn();
|
|
34221
|
+
_planRefineDeps = {
|
|
34222
|
+
readFile: async (path3) => {
|
|
34223
|
+
try {
|
|
34224
|
+
return await Bun.file(path3).text();
|
|
34225
|
+
} catch {
|
|
34226
|
+
return null;
|
|
34227
|
+
}
|
|
34228
|
+
}
|
|
34229
|
+
};
|
|
33885
34230
|
NEGATIVE_PATH_TOKENS = [
|
|
33886
34231
|
"error",
|
|
33887
34232
|
"fail",
|
|
@@ -33945,19 +34290,30 @@ ${outputFormat}`,
|
|
|
33945
34290
|
};
|
|
33946
34291
|
},
|
|
33947
34292
|
async hopBody(initialPrompt, ctx) {
|
|
34293
|
+
const builder = new PlanPromptBuilder;
|
|
33948
34294
|
const turn1 = await ctx.sendWithParseRetry(initialPrompt);
|
|
33949
|
-
const
|
|
33950
|
-
|
|
33951
|
-
|
|
33952
|
-
|
|
33953
|
-
|
|
33954
|
-
|
|
34295
|
+
const turn2 = await ctx.send(builder.buildRefineContinuation(ctx.input.outputPath));
|
|
34296
|
+
let totalCost = (turn1.estimatedCostUsd ?? 0) + (turn2.estimatedCostUsd ?? 0);
|
|
34297
|
+
let last = turn2;
|
|
34298
|
+
const missing = await readMissingVerbatimAcs(ctx.input);
|
|
34299
|
+
if (missing.length > 0) {
|
|
34300
|
+
getSafeLogger()?.info("plan", "Refine dropped [verbatim] spec ACs \u2014 issuing one repair turn", {
|
|
34301
|
+
featureName: ctx.input.featureName,
|
|
34302
|
+
missingCount: missing.length
|
|
34303
|
+
});
|
|
34304
|
+
const turn3 = await ctx.send(builder.buildVerbatimRepair(missing, ctx.input.outputPath));
|
|
34305
|
+
totalCost += turn3.estimatedCostUsd ?? 0;
|
|
34306
|
+
last = turn3;
|
|
34307
|
+
}
|
|
34308
|
+
return { ...last, estimatedCostUsd: totalCost };
|
|
33955
34309
|
},
|
|
33956
34310
|
parse(output, input) {
|
|
33957
34311
|
return validatePlanOutput(output, input.featureName, input.branchName);
|
|
33958
34312
|
},
|
|
33959
|
-
verify: async (parsed,
|
|
33960
|
-
|
|
34313
|
+
verify: async (parsed, input, _ctx) => {
|
|
34314
|
+
const validated = validateRefinedPrd(parsed);
|
|
34315
|
+
warnOnDroppedVerbatimAcs(validated, input.specContent, input.featureName);
|
|
34316
|
+
return validated;
|
|
33961
34317
|
},
|
|
33962
34318
|
recover: async (input, ctx) => {
|
|
33963
34319
|
const content = await ctx.readFile(input.outputPath);
|
|
@@ -35091,6 +35447,17 @@ function parseRefinementResponse(response, criteria) {
|
|
|
35091
35447
|
return fallbackCriteria(criteria);
|
|
35092
35448
|
}
|
|
35093
35449
|
}
|
|
35450
|
+
function refinementWouldFallback(response) {
|
|
35451
|
+
if (!response || !response.trim())
|
|
35452
|
+
return true;
|
|
35453
|
+
try {
|
|
35454
|
+
const fromFence = extractJsonFromMarkdown(response);
|
|
35455
|
+
const cleaned = stripTrailingCommas(fromFence !== response ? fromFence : response);
|
|
35456
|
+
return !Array.isArray(JSON.parse(cleaned));
|
|
35457
|
+
} catch {
|
|
35458
|
+
return true;
|
|
35459
|
+
}
|
|
35460
|
+
}
|
|
35094
35461
|
function fallbackCriteria(criteria, storyId = "") {
|
|
35095
35462
|
return criteria.map((c) => ({
|
|
35096
35463
|
original: c,
|
|
@@ -35106,6 +35473,7 @@ var acceptanceRefineOp;
|
|
|
35106
35473
|
var init_acceptance_refine = __esm(() => {
|
|
35107
35474
|
init_refinement();
|
|
35108
35475
|
init_config();
|
|
35476
|
+
init_logger2();
|
|
35109
35477
|
init_prompts();
|
|
35110
35478
|
acceptanceRefineOp = {
|
|
35111
35479
|
kind: "complete",
|
|
@@ -35128,6 +35496,9 @@ var init_acceptance_refine = __esm(() => {
|
|
|
35128
35496
|
};
|
|
35129
35497
|
},
|
|
35130
35498
|
parse(output, input, _ctx) {
|
|
35499
|
+
if (refinementWouldFallback(output)) {
|
|
35500
|
+
getSafeLogger()?.warn("acceptance", "AC refinement returned no usable JSON \u2014 falling back to unrefined criteria", { storyId: input.storyId, criteriaCount: input.criteria.length, responseBytes: output?.length ?? 0 });
|
|
35501
|
+
}
|
|
35131
35502
|
const items = parseRefinementResponse(output, input.criteria);
|
|
35132
35503
|
return items.map((item) => ({ ...item, storyId: item.storyId || input.storyId }));
|
|
35133
35504
|
}
|
|
@@ -35279,6 +35650,25 @@ function evaluateRepromptTrigger2(shape, input) {
|
|
|
35279
35650
|
return { shouldReprompt: false };
|
|
35280
35651
|
return { shouldReprompt: true, acDropped: dropped };
|
|
35281
35652
|
}
|
|
35653
|
+
async function maybeRepromptForInspection2(turn, parsed, rawObject, ctx) {
|
|
35654
|
+
if (ctx.input.mode !== "ref")
|
|
35655
|
+
return null;
|
|
35656
|
+
if (ctx.input.semanticConfig.demandInspectionTrail === false)
|
|
35657
|
+
return null;
|
|
35658
|
+
if (!parsed.passed || parsed.findings.length !== 0)
|
|
35659
|
+
return null;
|
|
35660
|
+
if (hasInspectionTrail(rawObject))
|
|
35661
|
+
return null;
|
|
35662
|
+
const secondTurn = await ctx.send(ReviewPromptBuilder.demandInspection());
|
|
35663
|
+
const costUsd = (turn.estimatedCostUsd ?? 0) + (secondTurn.estimatedCostUsd ?? 0);
|
|
35664
|
+
const secondParsed = validateLLMShape(tryParseLLMJson(secondTurn.output));
|
|
35665
|
+
getSafeLogger()?.warn("review", "Semantic reviewer returned empty pass with no inspection trail \u2014 re-prompted", {
|
|
35666
|
+
storyId: ctx.input.story.id,
|
|
35667
|
+
event: "review.semantic.inspection_trail.reprompted",
|
|
35668
|
+
recovered: secondParsed !== null
|
|
35669
|
+
});
|
|
35670
|
+
return secondParsed ? { ...turn, output: secondTurn.output, estimatedCostUsd: costUsd } : { ...turn, estimatedCostUsd: costUsd };
|
|
35671
|
+
}
|
|
35282
35672
|
async function performSemanticReground(turn, firstParsed, drops, ctx) {
|
|
35283
35673
|
const threshold = ctx.input.blockingThreshold ?? "error";
|
|
35284
35674
|
const acceptanceCriteria = ctx.input.story.acceptanceCriteria;
|
|
@@ -35399,9 +35789,13 @@ async function requoteBlockingFindings(findings, ctx) {
|
|
|
35399
35789
|
}
|
|
35400
35790
|
var FAIL_OPEN2, SEMANTIC_REQUOTE_RECOVERED_EVENT = "review.semantic.finding.requote_recovered", SEMANTIC_REQUOTE_FAILED_EVENT = "review.semantic.finding.requote_failed", DEFAULT_MAX_REQUOTES2 = 5, semanticReviewHopBody = async (initialPrompt, ctx) => {
|
|
35401
35791
|
const turn = await ctx.sendWithParseRetry(initialPrompt);
|
|
35402
|
-
const
|
|
35792
|
+
const rawObject = tryParseLLMJson(turn.output);
|
|
35793
|
+
const parsed = validateLLMShape(rawObject);
|
|
35403
35794
|
if (!parsed)
|
|
35404
35795
|
return turn;
|
|
35796
|
+
const inspectionGuard = await maybeRepromptForInspection2(turn, parsed, rawObject, ctx);
|
|
35797
|
+
if (inspectionGuard)
|
|
35798
|
+
return inspectionGuard;
|
|
35405
35799
|
const requoted = await requoteBlockingFindings(parsed.findings, ctx);
|
|
35406
35800
|
if (requoted.changed) {
|
|
35407
35801
|
const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
|
|
@@ -36870,7 +37264,7 @@ function buildVerifierFindings(verdict, categorization) {
|
|
|
36870
37264
|
return [];
|
|
36871
37265
|
}
|
|
36872
37266
|
}
|
|
36873
|
-
function parseVerdictFromStdout(output,
|
|
37267
|
+
function parseVerdictFromStdout(output, input, _ctx) {
|
|
36874
37268
|
if (!output || !output.trim()) {
|
|
36875
37269
|
throw new ParseValidationError("verifier produced no stdout");
|
|
36876
37270
|
}
|
|
@@ -36883,6 +37277,18 @@ function parseVerdictFromStdout(output, _input, _ctx) {
|
|
|
36883
37277
|
throw new ParseValidationError("verifier stdout JSON missing required VerifierVerdict fields");
|
|
36884
37278
|
}
|
|
36885
37279
|
const categorization = categorizeVerdict(verdict, verdict.tests.allPassing === true);
|
|
37280
|
+
getSafeLogger()?.info("verifier", "Verdict categorized", {
|
|
37281
|
+
storyId: input.story.id,
|
|
37282
|
+
approved: verdict.approved,
|
|
37283
|
+
success: categorization.success,
|
|
37284
|
+
advisoryOverride: verdict.approved === false && categorization.success,
|
|
37285
|
+
testsPassing: verdict.tests.allPassing,
|
|
37286
|
+
passCount: verdict.tests.passCount,
|
|
37287
|
+
failCount: verdict.tests.failCount,
|
|
37288
|
+
testModsDetected: verdict.testModifications.detected,
|
|
37289
|
+
testModsLegitimate: verdict.testModifications.legitimate,
|
|
37290
|
+
...categorization.failureCategory && { failureCategory: categorization.failureCategory }
|
|
37291
|
+
});
|
|
36886
37292
|
return {
|
|
36887
37293
|
success: categorization.success,
|
|
36888
37294
|
filesChanged: [],
|
|
@@ -38235,7 +38641,8 @@ var init_mechanical_lintfix_strategy = __esm(() => {
|
|
|
38235
38641
|
commandName: "lintFix",
|
|
38236
38642
|
command,
|
|
38237
38643
|
workdir: input.workdir,
|
|
38238
|
-
storyId: input.storyId
|
|
38644
|
+
storyId: input.storyId,
|
|
38645
|
+
stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
|
|
38239
38646
|
});
|
|
38240
38647
|
return { applied: true, exitCode: result.exitCode };
|
|
38241
38648
|
}
|
|
@@ -38293,7 +38700,8 @@ var init_mechanical_formatfix_strategy = __esm(() => {
|
|
|
38293
38700
|
commandName: "formatFix",
|
|
38294
38701
|
command,
|
|
38295
38702
|
workdir: input.workdir,
|
|
38296
|
-
storyId: input.storyId
|
|
38703
|
+
storyId: input.storyId,
|
|
38704
|
+
stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
|
|
38297
38705
|
});
|
|
38298
38706
|
return { applied: true, exitCode: result.exitCode };
|
|
38299
38707
|
}
|
|
@@ -38326,7 +38734,8 @@ var init_lint_check = __esm(() => {
|
|
|
38326
38734
|
commandName: "lint",
|
|
38327
38735
|
command: command ?? "",
|
|
38328
38736
|
workdir: input.workdir,
|
|
38329
|
-
storyId: input.storyId
|
|
38737
|
+
storyId: input.storyId,
|
|
38738
|
+
stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
|
|
38330
38739
|
});
|
|
38331
38740
|
if (result.exitCode === 0) {
|
|
38332
38741
|
return { success: true, findings: [], durationMs: Date.now() - start };
|
|
@@ -38575,7 +38984,8 @@ var init_typecheck_check = __esm(() => {
|
|
|
38575
38984
|
commandName: "typecheck",
|
|
38576
38985
|
command: command ?? "",
|
|
38577
38986
|
workdir: input.workdir,
|
|
38578
|
-
storyId: input.storyId
|
|
38987
|
+
storyId: input.storyId,
|
|
38988
|
+
stripEnvVars: ctxConfig?.quality?.stripEnvVars ?? []
|
|
38579
38989
|
});
|
|
38580
38990
|
if (result.exitCode === 0) {
|
|
38581
38991
|
return { success: true, findings: [], durationMs: Date.now() - start };
|
|
@@ -38740,6 +39150,7 @@ var init_operations = __esm(() => {
|
|
|
38740
39150
|
init_call();
|
|
38741
39151
|
init_plan();
|
|
38742
39152
|
init_plan_refine();
|
|
39153
|
+
init_verbatim_warn();
|
|
38743
39154
|
init_decompose2();
|
|
38744
39155
|
init_build_hop_callback();
|
|
38745
39156
|
init_classify_route();
|
|
@@ -39550,13 +39961,14 @@ async function resolveLintScope(args) {
|
|
|
39550
39961
|
function resolveScopedTemplate(reviewCommands, qualityCommands) {
|
|
39551
39962
|
return reviewCommands.lintScoped ?? qualityCommands?.lintScoped;
|
|
39552
39963
|
}
|
|
39553
|
-
async function runLintCommand(workdir, storyId, env2, command) {
|
|
39964
|
+
async function runLintCommand(workdir, storyId, env2, command, stripEnvVars) {
|
|
39554
39965
|
return runQualityCommand({
|
|
39555
39966
|
commandName: SCOPED_LINT_CHECK,
|
|
39556
39967
|
command,
|
|
39557
39968
|
workdir,
|
|
39558
39969
|
storyId,
|
|
39559
|
-
env: env2
|
|
39970
|
+
env: env2,
|
|
39971
|
+
stripEnvVars
|
|
39560
39972
|
});
|
|
39561
39973
|
}
|
|
39562
39974
|
function toReviewCheck(result) {
|
|
@@ -39597,7 +40009,7 @@ async function runScopedLintCheck(args) {
|
|
|
39597
40009
|
storyId: args.storyId,
|
|
39598
40010
|
reason: scope.degradedReason
|
|
39599
40011
|
});
|
|
39600
|
-
const fullResult2 = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand);
|
|
40012
|
+
const fullResult2 = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand, args.stripEnvVars);
|
|
39601
40013
|
return withLintScope(attachLintFindings(toReviewCheck(fullResult2), args.lintOutputFormat, args.workdir), scope, "degraded");
|
|
39602
40014
|
}
|
|
39603
40015
|
logger?.info("review", "lint_scope_empty", { storyId: args.storyId });
|
|
@@ -39622,19 +40034,19 @@ async function runScopedLintCheck(args) {
|
|
|
39622
40034
|
}
|
|
39623
40035
|
if (scopedTemplate) {
|
|
39624
40036
|
const scopedCommand = scopedTemplate.replaceAll("{{files}}", scope.files.map(shellQuotePath4).join(" "));
|
|
39625
|
-
const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand);
|
|
40037
|
+
const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand, args.stripEnvVars);
|
|
39626
40038
|
return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
|
|
39627
40039
|
}
|
|
39628
40040
|
if (!scope.degradedReason && isSupportedDerivedScopedCommand(fullLintCommand)) {
|
|
39629
40041
|
const scopedCommand = appendFilesToCommand(fullLintCommand, scope.files);
|
|
39630
|
-
const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand);
|
|
40042
|
+
const scopedResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, scopedCommand, args.stripEnvVars);
|
|
39631
40043
|
return withLintScope(attachLintFindings(toReviewCheck(scopedResult), args.lintOutputFormat, args.workdir), scope);
|
|
39632
40044
|
}
|
|
39633
40045
|
logger?.warn("review", "lint_scope_degraded", {
|
|
39634
40046
|
storyId: args.storyId,
|
|
39635
40047
|
reason: scope.degradedReason ?? "unsupported_scoped_command_shape"
|
|
39636
40048
|
});
|
|
39637
|
-
const fullResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand);
|
|
40049
|
+
const fullResult = await _scopedLintDeps.runLintCommand(args.workdir, args.storyId, args.env, fullLintCommand, args.stripEnvVars);
|
|
39638
40050
|
if (fullResult.exitCode === 0)
|
|
39639
40051
|
return withLintScope(toReviewCheck(fullResult), scope, "degraded");
|
|
39640
40052
|
const parsed = parseLintOutput(fullResult.output, args.lintOutputFormat ?? "auto", {
|
|
@@ -40360,8 +40772,8 @@ async function resolveCommand(check2, config2, executionConfig, workdir, quality
|
|
|
40360
40772
|
}
|
|
40361
40773
|
return null;
|
|
40362
40774
|
}
|
|
40363
|
-
async function runCheck(check2, command, workdir, storyId, env2) {
|
|
40364
|
-
const result = await runQualityCommand({ commandName: check2, command, workdir, storyId, env: env2 });
|
|
40775
|
+
async function runCheck(check2, command, workdir, storyId, env2, stripEnvVars) {
|
|
40776
|
+
const result = await runQualityCommand({ commandName: check2, command, workdir, storyId, env: env2, stripEnvVars });
|
|
40365
40777
|
return {
|
|
40366
40778
|
check: check2,
|
|
40367
40779
|
command: result.command,
|
|
@@ -40567,8 +40979,9 @@ async function runReview(opts) {
|
|
|
40567
40979
|
story,
|
|
40568
40980
|
storyGitRef,
|
|
40569
40981
|
env: env2,
|
|
40982
|
+
stripEnvVars: naxConfig?.quality?.stripEnvVars ?? [],
|
|
40570
40983
|
naxIgnoreIndex
|
|
40571
|
-
}) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2), workdir);
|
|
40984
|
+
}) : normalizeMechanicalFindings(checkName, await runCheck(checkName, command, workdir, storyId, env2, naxConfig?.quality?.stripEnvVars ?? []), workdir);
|
|
40572
40985
|
checks3.push(result);
|
|
40573
40986
|
if (result.success) {
|
|
40574
40987
|
logger?.info("review", `${checkName} passed`, {
|
|
@@ -41611,6 +42024,9 @@ Output ONLY the JSON object. Do not include markdown fences or explanation.`;
|
|
|
41611
42024
|
|
|
41612
42025
|
Review the draft with a strict self-audit mindset. Re-read the codebase context and compare the PRD against it. Focus only on the issues below, then rewrite the PRD if needed.
|
|
41613
42026
|
|
|
42027
|
+
#### spec-ac-preservation
|
|
42028
|
+
Enumerate every acceptance criterion the spec states. Confirm each one appears in some story's acceptanceCriteria \u2014 never drop a spec AC during this audit. If an AC looks unsupported by the current codebase, keep it: the story may be adding that capability. Any AC the spec tags \`[verbatim]\` MUST appear character-for-character in an acceptanceCriteria entry \u2014 preserve every backtick-quoted command, file path, regex, and count exactly. If a \`[verbatim]\` AC is missing or altered, restore it verbatim.
|
|
42029
|
+
|
|
41614
42030
|
#### ac-testable
|
|
41615
42031
|
For each acceptance criterion, ask whether the assertion is observable through a return value, exception, log output, file content, or state change. If any AC is not directly testable, rewrite it so it is observable.
|
|
41616
42032
|
|
|
@@ -41623,7 +42039,7 @@ Check whether any sentence in any description contradicts an acceptance criterio
|
|
|
41623
42039
|
#### codebase-fit
|
|
41624
42040
|
For each story, verify:
|
|
41625
42041
|
1. Proposed files, helpers, tests, dependencies, and implementation notes match the codebase context. Remove invented helpers, files, call sites, or dependencies unless the change clearly requires creating them.
|
|
41626
|
-
2. Each acceptance criterion's semantic meaning matches the spec's actual interface and data flow. Criteria that assert incorrect parameter semantics, wrong data flow, or behavior that contradicts the spec must be corrected
|
|
42042
|
+
2. Each acceptance criterion's semantic meaning matches the spec's actual interface and data flow. Criteria that assert incorrect parameter semantics, wrong data flow, or behavior that contradicts the spec must be corrected. Never delete an AC that restates a spec AC \u2014 correct its wording to match the spec instead. Cross-check each AC against the spec's interface definitions, pseudocode, and design notes.
|
|
41627
42043
|
|
|
41628
42044
|
#### contextfiles-spec-alignment
|
|
41629
42045
|
For each story, compare contextFiles against files the spec explicitly lists as context (e.g., in "Context Files" sections). Ensure the most critical spec-recommended files are included, up to the 5-file limit. If a spec-recommended file is absent, add it (removing the least critical one if already at 5). Files the story will CREATE must not appear here.
|
|
@@ -41641,6 +42057,23 @@ If a story changes existing behavior, extracts a shared helper, extends an exist
|
|
|
41641
42057
|
Check each story's title, description, scope, contextFiles, and acceptance criteria for internal consistency. If the story says a file or command is in scope anywhere else, do not list it as out of scope. If the title or acceptance criteria clearly include CLI, output, tests, or helper extraction work, the Scope section must reflect that accurately.
|
|
41642
42058
|
|
|
41643
42059
|
Write the revised PRD to this file path: ${outputFilePath}
|
|
42060
|
+
Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
|
|
42061
|
+
}
|
|
42062
|
+
buildVerbatimRepair(missingAcs, outputFilePath) {
|
|
42063
|
+
const list = missingAcs.map((ac) => `- ${ac}`).join(`
|
|
42064
|
+
`);
|
|
42065
|
+
return `Your revised PRD dropped or altered acceptance criteria the spec marked \`[verbatim]\`. These are load-bearing executable checks (greps, file-existence checks, regex/count assertions, or architectural invariants) and MUST survive character-for-character \u2014 paraphrasing destroys the verification mechanism.
|
|
42066
|
+
|
|
42067
|
+
The following \`[verbatim]\` spec acceptance criteria are missing or altered in the PRD:
|
|
42068
|
+
|
|
42069
|
+
${list}
|
|
42070
|
+
|
|
42071
|
+
For each one:
|
|
42072
|
+
- Add it to the \`acceptanceCriteria\` array of the single most relevant user story.
|
|
42073
|
+
- Preserve every backtick-quoted command, file path, regex, and count exactly as written in the spec. Do not paraphrase, retag, split, or move them into a description.
|
|
42074
|
+
- Do not remove or weaken any acceptance criteria that are already correct.
|
|
42075
|
+
|
|
42076
|
+
Write the corrected PRD to this file path: ${outputFilePath}
|
|
41644
42077
|
Do not output the PRD in chat. After writing the file, reply with a brief text confirmation only.`;
|
|
41645
42078
|
}
|
|
41646
42079
|
build(specContent, codebaseContext, outputFilePath, packages, packageDetails, projectProfile, proposers) {
|
|
@@ -43916,13 +44349,13 @@ var init_factory = __esm(() => {
|
|
|
43916
44349
|
|
|
43917
44350
|
// src/execution/pid-registry.ts
|
|
43918
44351
|
import { existsSync as existsSync7 } from "fs";
|
|
43919
|
-
import { appendFile as appendFile2 } from "fs/promises";
|
|
43920
44352
|
|
|
43921
44353
|
class PidRegistry {
|
|
43922
44354
|
workdir;
|
|
43923
44355
|
pidsFilePath;
|
|
43924
44356
|
pids = new Set;
|
|
43925
44357
|
frozen = false;
|
|
44358
|
+
writeQueueTail = Promise.resolve();
|
|
43926
44359
|
constructor(workdir, _platform) {
|
|
43927
44360
|
this.workdir = workdir;
|
|
43928
44361
|
this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
|
|
@@ -43943,15 +44376,8 @@ class PidRegistry {
|
|
|
43943
44376
|
return;
|
|
43944
44377
|
}
|
|
43945
44378
|
this.pids.add(pid);
|
|
43946
|
-
const entry = {
|
|
43947
|
-
pid,
|
|
43948
|
-
spawnedAt: new Date().toISOString(),
|
|
43949
|
-
workdir: this.workdir
|
|
43950
|
-
};
|
|
43951
44379
|
try {
|
|
43952
|
-
|
|
43953
|
-
`;
|
|
43954
|
-
await appendFile2(this.pidsFilePath, line);
|
|
44380
|
+
await this.enqueueWrite();
|
|
43955
44381
|
logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
|
|
43956
44382
|
} catch (err) {
|
|
43957
44383
|
logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
|
|
@@ -43963,7 +44389,7 @@ class PidRegistry {
|
|
|
43963
44389
|
const logger = getSafeLogger();
|
|
43964
44390
|
this.pids.delete(pid);
|
|
43965
44391
|
try {
|
|
43966
|
-
await this.
|
|
44392
|
+
await this.enqueueWrite();
|
|
43967
44393
|
logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
|
|
43968
44394
|
} catch (err) {
|
|
43969
44395
|
logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
|
|
@@ -43982,8 +44408,8 @@ class PidRegistry {
|
|
|
43982
44408
|
const killPromises = pids.map((pid) => this.killPidTree(pid));
|
|
43983
44409
|
await Promise.allSettled(killPromises);
|
|
43984
44410
|
try {
|
|
43985
|
-
await Bun.write(this.pidsFilePath, "");
|
|
43986
44411
|
this.pids.clear();
|
|
44412
|
+
await this.enqueueWrite();
|
|
43987
44413
|
logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
|
|
43988
44414
|
} catch (err) {
|
|
43989
44415
|
logger?.warn("pid-registry", "Failed to clear registry file", {
|
|
@@ -44202,6 +44628,37 @@ class PidRegistry {
|
|
|
44202
44628
|
getPids() {
|
|
44203
44629
|
return Array.from(this.pids);
|
|
44204
44630
|
}
|
|
44631
|
+
snapshot() {
|
|
44632
|
+
return Array.from(this.pids);
|
|
44633
|
+
}
|
|
44634
|
+
async flush() {
|
|
44635
|
+
await this.writeQueueTail;
|
|
44636
|
+
}
|
|
44637
|
+
async readPidsFromDisk() {
|
|
44638
|
+
try {
|
|
44639
|
+
if (!existsSync7(this.pidsFilePath))
|
|
44640
|
+
return [];
|
|
44641
|
+
const content = await Bun.file(this.pidsFilePath).text();
|
|
44642
|
+
return content.split(`
|
|
44643
|
+
`).filter((line) => line.trim()).map((line) => {
|
|
44644
|
+
try {
|
|
44645
|
+
return JSON.parse(line).pid;
|
|
44646
|
+
} catch {
|
|
44647
|
+
return null;
|
|
44648
|
+
}
|
|
44649
|
+
}).filter((pid) => pid !== null);
|
|
44650
|
+
} catch {
|
|
44651
|
+
return [];
|
|
44652
|
+
}
|
|
44653
|
+
}
|
|
44654
|
+
enqueueWrite() {
|
|
44655
|
+
this.writeQueueTail = this.writeQueueTail.then(() => this.writePidsFile().catch((err) => {
|
|
44656
|
+
getSafeLogger()?.warn("pid-registry", "Failed to flush PID file \u2014 on-disk registry may be stale", {
|
|
44657
|
+
error: errorMessage(err)
|
|
44658
|
+
});
|
|
44659
|
+
}));
|
|
44660
|
+
return this.writeQueueTail;
|
|
44661
|
+
}
|
|
44205
44662
|
}
|
|
44206
44663
|
var PID_REGISTRY_FILE = ".nax-pids", PID_TREE_KILL_GRACE_MS = 250, _pidRegistryDeps;
|
|
44207
44664
|
var init_pid_registry = __esm(() => {
|
|
@@ -52048,7 +52505,7 @@ var init_effectiveness = __esm(() => {
|
|
|
52048
52505
|
});
|
|
52049
52506
|
|
|
52050
52507
|
// src/execution/progress.ts
|
|
52051
|
-
import { appendFile as
|
|
52508
|
+
import { appendFile as appendFile2, mkdir as mkdir7 } from "fs/promises";
|
|
52052
52509
|
import { join as join44 } from "path";
|
|
52053
52510
|
async function appendProgress(featureDir, storyId, status, message) {
|
|
52054
52511
|
await mkdir7(featureDir, { recursive: true });
|
|
@@ -52056,7 +52513,7 @@ async function appendProgress(featureDir, storyId, status, message) {
|
|
|
52056
52513
|
const timestamp = new Date().toISOString();
|
|
52057
52514
|
const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
|
|
52058
52515
|
`;
|
|
52059
|
-
await
|
|
52516
|
+
await appendFile2(progressPath, entry);
|
|
52060
52517
|
}
|
|
52061
52518
|
var init_progress = () => {};
|
|
52062
52519
|
|
|
@@ -52163,6 +52620,7 @@ var init_completion = __esm(() => {
|
|
|
52163
52620
|
const logger = getLogger();
|
|
52164
52621
|
const isBatch = ctx.stories.length > 1;
|
|
52165
52622
|
const sessionCost = ctx.agentResult?.estimatedCostUsd || 0;
|
|
52623
|
+
const persistPrd = ctx.skipPrdPersistence !== true;
|
|
52166
52624
|
const prdPath = ctx.prdPath ?? (ctx.featureDir ? `${ctx.featureDir}/prd.json` : `${ctx.workdir}/nax/features/unknown/prd.json`);
|
|
52167
52625
|
const storyStartTime = ctx.storyStartTime || new Date().toISOString();
|
|
52168
52626
|
if (isBatch) {
|
|
@@ -52187,7 +52645,9 @@ var init_completion = __esm(() => {
|
|
|
52187
52645
|
}
|
|
52188
52646
|
}
|
|
52189
52647
|
for (const completedStory of ctx.stories) {
|
|
52190
|
-
|
|
52648
|
+
if (persistPrd) {
|
|
52649
|
+
markStoryPassed(ctx.prd, completedStory.id);
|
|
52650
|
+
}
|
|
52191
52651
|
const costPerStory = sessionCost / ctx.stories.length;
|
|
52192
52652
|
logger.info("completion", "Story passed", {
|
|
52193
52653
|
storyId: completedStory.id,
|
|
@@ -52219,7 +52679,9 @@ var init_completion = __esm(() => {
|
|
|
52219
52679
|
}
|
|
52220
52680
|
}
|
|
52221
52681
|
}
|
|
52222
|
-
|
|
52682
|
+
if (persistPrd) {
|
|
52683
|
+
await _completionDeps.savePRD(ctx.prd, prdPath);
|
|
52684
|
+
}
|
|
52223
52685
|
const updatedCounts = countStories(ctx.prd);
|
|
52224
52686
|
logger.info("completion", "Progress update", {
|
|
52225
52687
|
storyId: ctx.story.id,
|
|
@@ -53022,7 +53484,7 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
|
|
|
53022
53484
|
data.reviewReason = r.reviewReason;
|
|
53023
53485
|
return { success: success2, data };
|
|
53024
53486
|
}
|
|
53025
|
-
function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage) {
|
|
53487
|
+
function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTddPhase, stage, progressData = {}) {
|
|
53026
53488
|
if (isTddPhase)
|
|
53027
53489
|
return;
|
|
53028
53490
|
if (opName === "semantic-review" || opName === "adversarial-review")
|
|
@@ -53030,7 +53492,8 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
|
|
|
53030
53492
|
const built = buildPhaseOutcomeLogData(storyId, opName, output, durationMs);
|
|
53031
53493
|
if (!built)
|
|
53032
53494
|
return;
|
|
53033
|
-
const { success: success2
|
|
53495
|
+
const { success: success2 } = built;
|
|
53496
|
+
const data = { ...built.data, ...progressData };
|
|
53034
53497
|
const logger = getSafeLogger();
|
|
53035
53498
|
const message = formatPhaseResultMessage(opName, success2, stage);
|
|
53036
53499
|
if (stage === "rectification") {
|
|
@@ -53093,17 +53556,21 @@ function logUnifiedReviewPhaseResult(storyId, opName, output) {
|
|
|
53093
53556
|
truncated: findingsCount > findingsSummary.length
|
|
53094
53557
|
});
|
|
53095
53558
|
}
|
|
53096
|
-
async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false) {
|
|
53559
|
+
async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = false, progress) {
|
|
53097
53560
|
const logger = getSafeLogger();
|
|
53098
53561
|
const opName = slot.op.name;
|
|
53562
|
+
const progressData = progress ? { phaseIndex: progress.index, totalPhases: progress.total } : {};
|
|
53099
53563
|
const isTddPhase = isThreeSession && TDD_OP_NAMES.has(opName);
|
|
53100
53564
|
const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
|
|
53101
53565
|
let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
|
|
53102
53566
|
dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
|
|
53103
53567
|
if (isTddPhase) {
|
|
53104
|
-
logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName });
|
|
53568
|
+
logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName, ...progressData });
|
|
53105
53569
|
} else if (isThreeSession && opName === "full-suite-gate") {
|
|
53106
|
-
logger?.info("tdd", "-> Running full test suite gate (before Verifier)", {
|
|
53570
|
+
logger?.info("tdd", "-> Running full test suite gate (before Verifier)", {
|
|
53571
|
+
storyId: ctx.storyId,
|
|
53572
|
+
...progressData
|
|
53573
|
+
});
|
|
53107
53574
|
}
|
|
53108
53575
|
logUnifiedReviewPhaseStart(ctx.storyId, opName);
|
|
53109
53576
|
const phaseStartedAt = Date.now();
|
|
@@ -53113,7 +53580,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
53113
53580
|
phaseOutputs[opName] = output;
|
|
53114
53581
|
emitReviewDecision(ctx, opName, output);
|
|
53115
53582
|
logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
|
|
53116
|
-
logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage);
|
|
53583
|
+
logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
|
|
53117
53584
|
if (isTddPhase) {
|
|
53118
53585
|
const durationMs = Date.now() - phaseStartedAt;
|
|
53119
53586
|
logger?.info("tdd", `Session complete: ${opName}`, {
|
|
@@ -53282,9 +53749,13 @@ class ExecutionPlan {
|
|
|
53282
53749
|
const phaseOutputs = {};
|
|
53283
53750
|
const startedAt = Date.now();
|
|
53284
53751
|
const logger = getSafeLogger();
|
|
53285
|
-
|
|
53752
|
+
const orderedPhases = collectOrderedPhases(this.state);
|
|
53753
|
+
for (const [phaseIndex, phase] of orderedPhases.entries()) {
|
|
53286
53754
|
try {
|
|
53287
|
-
await runPhase(this.ctx, phase.slot, phaseCosts, phaseOutputs, this.isThreeSession
|
|
53755
|
+
await runPhase(this.ctx, phase.slot, phaseCosts, phaseOutputs, this.isThreeSession, {
|
|
53756
|
+
index: phaseIndex + 1,
|
|
53757
|
+
total: orderedPhases.length
|
|
53758
|
+
});
|
|
53288
53759
|
} catch (error48) {
|
|
53289
53760
|
logger?.error("story-orchestrator", "Phase threw unexpected error", {
|
|
53290
53761
|
storyId: this.ctx.storyId,
|
|
@@ -56949,7 +57420,7 @@ function renderProposals(proposals, runId, observationCount) {
|
|
|
56949
57420
|
}
|
|
56950
57421
|
|
|
56951
57422
|
// src/plugins/builtin/curator/rollup.ts
|
|
56952
|
-
import { appendFile as
|
|
57423
|
+
import { appendFile as appendFile3, mkdir as mkdir10, writeFile } from "fs/promises";
|
|
56953
57424
|
import * as path14 from "path";
|
|
56954
57425
|
async function appendToRollup(observations, rollupPath) {
|
|
56955
57426
|
try {
|
|
@@ -56965,7 +57436,7 @@ async function appendToRollup(observations, rollupPath) {
|
|
|
56965
57436
|
const newLines = `${observations.map((o) => JSON.stringify(o)).join(`
|
|
56966
57437
|
`)}
|
|
56967
57438
|
`;
|
|
56968
|
-
await
|
|
57439
|
+
await appendFile3(rollupPath, newLines);
|
|
56969
57440
|
} catch {}
|
|
56970
57441
|
}
|
|
56971
57442
|
var init_rollup = () => {};
|
|
@@ -57883,7 +58354,7 @@ var package_default;
|
|
|
57883
58354
|
var init_package = __esm(() => {
|
|
57884
58355
|
package_default = {
|
|
57885
58356
|
name: "@nathapp/nax",
|
|
57886
|
-
version: "0.68.
|
|
58357
|
+
version: "0.68.3",
|
|
57887
58358
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
57888
58359
|
type: "module",
|
|
57889
58360
|
bin: {
|
|
@@ -57978,8 +58449,8 @@ var init_version = __esm(() => {
|
|
|
57978
58449
|
NAX_VERSION = package_default.version;
|
|
57979
58450
|
NAX_COMMIT = (() => {
|
|
57980
58451
|
try {
|
|
57981
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
57982
|
-
return "
|
|
58452
|
+
if (/^[0-9a-f]{6,10}$/.test("a1007103"))
|
|
58453
|
+
return "a1007103";
|
|
57983
58454
|
} catch {}
|
|
57984
58455
|
try {
|
|
57985
58456
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -59395,6 +59866,9 @@ async function handleRunCompletion(options) {
|
|
|
59395
59866
|
if (options.pluginProviderCache) {
|
|
59396
59867
|
await options.pluginProviderCache.disposeAll();
|
|
59397
59868
|
}
|
|
59869
|
+
clearLanguageCache();
|
|
59870
|
+
clearWorkspaceCache();
|
|
59871
|
+
clearGitRootCache();
|
|
59398
59872
|
const finalCounts = countStories(prd);
|
|
59399
59873
|
const fallbackAggregate = deriveRunFallbackAggregates(allStoryMetrics);
|
|
59400
59874
|
pipelineEventBus.emit({
|
|
@@ -59500,7 +59974,10 @@ var init_run_completion = __esm(() => {
|
|
|
59500
59974
|
init_metrics();
|
|
59501
59975
|
init_event_bus();
|
|
59502
59976
|
init_prd();
|
|
59977
|
+
init_detector();
|
|
59503
59978
|
init_scratch_purge();
|
|
59979
|
+
init_workspace();
|
|
59980
|
+
init_smart_runner();
|
|
59504
59981
|
init_run_regression();
|
|
59505
59982
|
_runCompletionDeps = {
|
|
59506
59983
|
runDeferredRegression,
|
|
@@ -59598,7 +60075,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
|
|
|
59598
60075
|
var DEFAULT_MAX_BATCH_SIZE = 4;
|
|
59599
60076
|
|
|
59600
60077
|
// src/pipeline/subscribers/events-writer.ts
|
|
59601
|
-
import { appendFile as
|
|
60078
|
+
import { appendFile as appendFile4, mkdir as mkdir14 } from "fs/promises";
|
|
59602
60079
|
import { basename as basename14, join as join73 } from "path";
|
|
59603
60080
|
function wireEventsWriter(bus, feature, runId, workdir) {
|
|
59604
60081
|
const logger = getSafeLogger();
|
|
@@ -59613,7 +60090,7 @@ function wireEventsWriter(bus, feature, runId, workdir) {
|
|
|
59613
60090
|
await mkdir14(eventsDir, { recursive: true });
|
|
59614
60091
|
dirReady = true;
|
|
59615
60092
|
}
|
|
59616
|
-
await
|
|
60093
|
+
await appendFile4(eventsFile, `${JSON.stringify(line)}
|
|
59617
60094
|
`);
|
|
59618
60095
|
} catch (err) {
|
|
59619
60096
|
logger?.warn("events-writer", "Failed to write event line (non-fatal)", {
|
|
@@ -60623,7 +61100,7 @@ function extractQuoteTriples(reason) {
|
|
|
60623
61100
|
}
|
|
60624
61101
|
return triples;
|
|
60625
61102
|
}
|
|
60626
|
-
function
|
|
61103
|
+
function normalizeWs3(s) {
|
|
60627
61104
|
return s.replace(/\s+/g, " ").trim();
|
|
60628
61105
|
}
|
|
60629
61106
|
async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
|
|
@@ -60637,7 +61114,7 @@ async function verifyQuoteTriple(triple, workdir, deps = _quoteIntegrityDeps) {
|
|
|
60637
61114
|
const end = Math.min(lines.length, triple.line + CONTEXT_LINES);
|
|
60638
61115
|
const window2 = lines.slice(start, end).join(`
|
|
60639
61116
|
`);
|
|
60640
|
-
return
|
|
61117
|
+
return normalizeWs3(window2).toLowerCase().includes(normalizeWs3(triple.quote).toLowerCase());
|
|
60641
61118
|
}
|
|
60642
61119
|
async function verifyEscalationQuotes(reason, workdir, storyId, deps = _quoteIntegrityDeps) {
|
|
60643
61120
|
const triples = extractQuoteTriples(reason);
|
|
@@ -61510,9 +61987,13 @@ var exports_parallel_worker = {};
|
|
|
61510
61987
|
__export(exports_parallel_worker, {
|
|
61511
61988
|
executeStoryInWorktree: () => executeStoryInWorktree,
|
|
61512
61989
|
executeParallelBatch: () => executeParallelBatch,
|
|
61990
|
+
buildWorktreePipelineContext: () => buildWorktreePipelineContext,
|
|
61513
61991
|
_parallelWorkerDeps: () => _parallelWorkerDeps
|
|
61514
61992
|
});
|
|
61515
61993
|
import { join as join79 } from "path";
|
|
61994
|
+
function buildWorktreePipelineContext(base, _story) {
|
|
61995
|
+
return { ...base, prd: structuredClone(base.prd) };
|
|
61996
|
+
}
|
|
61516
61997
|
async function executeStoryInWorktree(story, worktreePath, dependencyContext, context, routing, eventEmitter) {
|
|
61517
61998
|
const logger = getSafeLogger();
|
|
61518
61999
|
try {
|
|
@@ -61526,7 +62007,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
|
|
|
61526
62007
|
}
|
|
61527
62008
|
}
|
|
61528
62009
|
const pipelineContext = {
|
|
61529
|
-
...context,
|
|
62010
|
+
...buildWorktreePipelineContext(context, story),
|
|
61530
62011
|
config: context.config,
|
|
61531
62012
|
rootConfig: context.rootConfig,
|
|
61532
62013
|
story,
|
|
@@ -61826,6 +62307,7 @@ var init_parallel_batch = __esm(() => {
|
|
|
61826
62307
|
// src/execution/unified-executor.ts
|
|
61827
62308
|
var exports_unified_executor = {};
|
|
61828
62309
|
__export(exports_unified_executor, {
|
|
62310
|
+
reconcileBatchOutcome: () => reconcileBatchOutcome,
|
|
61829
62311
|
executeUnified: () => executeUnified,
|
|
61830
62312
|
_unifiedExecutorDeps: () => _unifiedExecutorDeps
|
|
61831
62313
|
});
|
|
@@ -61949,7 +62431,10 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
61949
62431
|
const readyStories = getAllReadyStories(prd);
|
|
61950
62432
|
const batch = _unifiedExecutorDeps.selectIndependentBatch(readyStories, ctx.parallelCount);
|
|
61951
62433
|
if (batch.length > 1) {
|
|
61952
|
-
|
|
62434
|
+
const batchAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
|
|
62435
|
+
const batchCounts = countStories(prd);
|
|
62436
|
+
const batchBaseDone = batchCounts.total - batchCounts.pending;
|
|
62437
|
+
for (const [batchIndex, story] of batch.entries()) {
|
|
61953
62438
|
const modelTier2 = story.routing?.modelTier ?? ctx.config.autoMode.complexityRouting?.[story.routing?.complexity ?? "medium"] ?? "balanced";
|
|
61954
62439
|
pipelineEventBus.emit({
|
|
61955
62440
|
type: "story:started",
|
|
@@ -61957,7 +62442,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
61957
62442
|
story: { id: story.id, title: story.title, status: story.status, attempts: story.attempts },
|
|
61958
62443
|
workdir: ctx.workdir,
|
|
61959
62444
|
modelTier: modelTier2,
|
|
61960
|
-
agent:
|
|
62445
|
+
agent: batchAgent,
|
|
61961
62446
|
iteration: iterations
|
|
61962
62447
|
});
|
|
61963
62448
|
logger?.info("story.start", `${story.title}`, {
|
|
@@ -61965,6 +62450,9 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
61965
62450
|
storyTitle: story.title,
|
|
61966
62451
|
complexity: story.routing?.complexity ?? "unknown",
|
|
61967
62452
|
modelTier: modelTier2,
|
|
62453
|
+
agent: batchAgent,
|
|
62454
|
+
storyNumber: batchBaseDone + batchIndex + 1,
|
|
62455
|
+
storyTotal: batchCounts.total,
|
|
61968
62456
|
attempt: story.attempts + 1
|
|
61969
62457
|
});
|
|
61970
62458
|
}
|
|
@@ -61984,6 +62472,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
61984
62472
|
config: ctx.config,
|
|
61985
62473
|
rootConfig: ctx.config,
|
|
61986
62474
|
prd,
|
|
62475
|
+
skipPrdPersistence: true,
|
|
61987
62476
|
projectDir: ctx.workdir,
|
|
61988
62477
|
naxIgnoreIndex,
|
|
61989
62478
|
hooks: ctx.hooks,
|
|
@@ -62031,6 +62520,8 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62031
62520
|
abortSignal: ctx.abortSignal
|
|
62032
62521
|
}, pipelineResult);
|
|
62033
62522
|
}
|
|
62523
|
+
reconcileBatchOutcome(prd, batchResult);
|
|
62524
|
+
await savePRD(prd, ctx.prdPath);
|
|
62034
62525
|
await pipelineEventBus.drain();
|
|
62035
62526
|
totalCost += batchResult.totalCost;
|
|
62036
62527
|
storiesCompleted += batchResult.completed.length;
|
|
@@ -62121,6 +62612,8 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62121
62612
|
}
|
|
62122
62613
|
}
|
|
62123
62614
|
const modelTier2 = singleSelection.routing.modelTier;
|
|
62615
|
+
const singleAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
|
|
62616
|
+
const singleCounts = countStories(prd);
|
|
62124
62617
|
pipelineEventBus.emit({
|
|
62125
62618
|
type: "story:started",
|
|
62126
62619
|
storyId: singleStory.id,
|
|
@@ -62132,7 +62625,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62132
62625
|
},
|
|
62133
62626
|
workdir: ctx.workdir,
|
|
62134
62627
|
modelTier: modelTier2,
|
|
62135
|
-
agent:
|
|
62628
|
+
agent: singleAgent,
|
|
62136
62629
|
iteration: iterations
|
|
62137
62630
|
});
|
|
62138
62631
|
logger?.info("story.start", `${singleStory.title}`, {
|
|
@@ -62140,6 +62633,9 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62140
62633
|
storyTitle: singleStory.title,
|
|
62141
62634
|
complexity: singleSelection.routing.complexity ?? "unknown",
|
|
62142
62635
|
modelTier: modelTier2,
|
|
62636
|
+
agent: singleAgent,
|
|
62637
|
+
storyNumber: singleCounts.total - singleCounts.pending + 1,
|
|
62638
|
+
storyTotal: singleCounts.total,
|
|
62143
62639
|
attempt: singleStory.attempts + 1
|
|
62144
62640
|
});
|
|
62145
62641
|
const singleIter = await _unifiedExecutorDeps.runIteration(ctx, prd, singleSelection, iterations, totalCost, allStoryMetrics);
|
|
@@ -62191,6 +62687,8 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62191
62687
|
}
|
|
62192
62688
|
}
|
|
62193
62689
|
const modelTier = selection.routing.modelTier;
|
|
62690
|
+
const seqAgent = ctx.agentManager?.getDefault() ?? resolveDefaultAgent(ctx.config);
|
|
62691
|
+
const seqCounts = countStories(prd);
|
|
62194
62692
|
pipelineEventBus.emit({
|
|
62195
62693
|
type: "story:started",
|
|
62196
62694
|
storyId: selection.story.id,
|
|
@@ -62202,7 +62700,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62202
62700
|
},
|
|
62203
62701
|
workdir: ctx.workdir,
|
|
62204
62702
|
modelTier,
|
|
62205
|
-
agent:
|
|
62703
|
+
agent: seqAgent,
|
|
62206
62704
|
iteration: iterations
|
|
62207
62705
|
});
|
|
62208
62706
|
logger?.info("story.start", `${selection.story.title}`, {
|
|
@@ -62210,6 +62708,9 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62210
62708
|
storyTitle: selection.story.title,
|
|
62211
62709
|
complexity: selection.routing.complexity ?? "unknown",
|
|
62212
62710
|
modelTier,
|
|
62711
|
+
agent: seqAgent,
|
|
62712
|
+
storyNumber: seqCounts.total - seqCounts.pending + 1,
|
|
62713
|
+
storyTotal: seqCounts.total,
|
|
62213
62714
|
attempt: selection.story.attempts + 1
|
|
62214
62715
|
});
|
|
62215
62716
|
const iter = await _unifiedExecutorDeps.runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics);
|
|
@@ -62268,6 +62769,18 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
62268
62769
|
return buildResult2("max-iterations");
|
|
62269
62770
|
} finally {}
|
|
62270
62771
|
}
|
|
62772
|
+
function reconcileBatchOutcome(prd, batchResult) {
|
|
62773
|
+
for (const story of batchResult.completed) {
|
|
62774
|
+
markStoryPassed(prd, story.id);
|
|
62775
|
+
}
|
|
62776
|
+
for (const conflict of batchResult.mergeConflicts) {
|
|
62777
|
+
if (conflict.rectified) {
|
|
62778
|
+
markStoryPassed(prd, conflict.story.id);
|
|
62779
|
+
} else {
|
|
62780
|
+
markStoryFailed(prd, conflict.story.id, undefined, "merge-conflict");
|
|
62781
|
+
}
|
|
62782
|
+
}
|
|
62783
|
+
}
|
|
62271
62784
|
var TERMINAL_ACTIONS, _unifiedExecutorDeps;
|
|
62272
62785
|
var init_unified_executor = __esm(() => {
|
|
62273
62786
|
init_agents();
|
|
@@ -94922,6 +95435,7 @@ class DebatePlanStrategy {
|
|
|
94922
95435
|
});
|
|
94923
95436
|
if (debateResult.outcome !== "failed" && debateResult.output) {
|
|
94924
95437
|
const prd2 = validatePlanOutput(debateResult.output, ctx.options.feature, ctx.branchName);
|
|
95438
|
+
warnOnDroppedVerbatimAcs(prd2, ctx.specContent, ctx.options.feature);
|
|
94925
95439
|
const withProject2 = { ...prd2, project: ctx.projectName };
|
|
94926
95440
|
return _debatePlanDeps.writeOrRecoverPrd(ctx, withProject2);
|
|
94927
95441
|
}
|