@nathapp/nax 0.75.1 → 0.75.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 +1492 -548
- package/flows/nax-finish/exec.ts +78 -29
- package/flows/nax-finish/steps/quality.ts +11 -2
- package/flows/nax-finish/steps/result.ts +9 -1
- package/package.json +3 -2
package/dist/nax.js
CHANGED
|
@@ -17294,7 +17294,7 @@ var HeadersSchema, ReporterEventSchema, WebhookReporterConfigSchema, OtelReporte
|
|
|
17294
17294
|
var init_schemas_reporters = __esm(() => {
|
|
17295
17295
|
init_zod();
|
|
17296
17296
|
HeadersSchema = exports_external.record(exports_external.string(), exports_external.string()).default({});
|
|
17297
|
-
ReporterEventSchema = exports_external.enum(["onRunStart", "onStoryComplete", "onRunEnd"]);
|
|
17297
|
+
ReporterEventSchema = exports_external.enum(["onRunStart", "onStoryComplete", "onRunEnd", "onPhaseStart", "onPhaseComplete"]);
|
|
17298
17298
|
WebhookReporterConfigSchema = exports_external.object({
|
|
17299
17299
|
enabled: exports_external.boolean().default(false),
|
|
17300
17300
|
url: exports_external.string().url().optional(),
|
|
@@ -17311,12 +17311,23 @@ var init_schemas_reporters = __esm(() => {
|
|
|
17311
17311
|
endpoint: exports_external.string().url().optional(),
|
|
17312
17312
|
headers: HeadersSchema,
|
|
17313
17313
|
serviceName: exports_external.string().default("nax"),
|
|
17314
|
-
timeoutMs: exports_external.number().int().positive().default(5000)
|
|
17314
|
+
timeoutMs: exports_external.number().int().positive().default(5000),
|
|
17315
|
+
detail: exports_external.enum(["counts", "verbose"]).default("counts"),
|
|
17316
|
+
heartbeatIntervalMs: exports_external.number().int().nonnegative().default(1e4),
|
|
17317
|
+
maxBatchSize: exports_external.number().int().positive().default(64),
|
|
17318
|
+
flushIntervalMs: exports_external.number().int().positive().default(5000),
|
|
17319
|
+
maxQueueSize: exports_external.number().int().positive().default(2048),
|
|
17320
|
+
phases: exports_external.array(exports_external.string()).optional()
|
|
17315
17321
|
}).default({
|
|
17316
17322
|
enabled: false,
|
|
17317
17323
|
headers: {},
|
|
17318
17324
|
serviceName: "nax",
|
|
17319
|
-
timeoutMs: 5000
|
|
17325
|
+
timeoutMs: 5000,
|
|
17326
|
+
detail: "counts",
|
|
17327
|
+
heartbeatIntervalMs: 1e4,
|
|
17328
|
+
maxBatchSize: 64,
|
|
17329
|
+
flushIntervalMs: 5000,
|
|
17330
|
+
maxQueueSize: 2048
|
|
17320
17331
|
});
|
|
17321
17332
|
ReportersConfigSchema = exports_external.object({
|
|
17322
17333
|
webhook: WebhookReporterConfigSchema,
|
|
@@ -17331,7 +17342,12 @@ var init_schemas_reporters = __esm(() => {
|
|
|
17331
17342
|
enabled: false,
|
|
17332
17343
|
headers: {},
|
|
17333
17344
|
serviceName: "nax",
|
|
17334
|
-
timeoutMs: 5000
|
|
17345
|
+
timeoutMs: 5000,
|
|
17346
|
+
detail: "counts",
|
|
17347
|
+
heartbeatIntervalMs: 1e4,
|
|
17348
|
+
maxBatchSize: 64,
|
|
17349
|
+
flushIntervalMs: 5000,
|
|
17350
|
+
maxQueueSize: 2048
|
|
17335
17351
|
}
|
|
17336
17352
|
});
|
|
17337
17353
|
});
|
|
@@ -18222,6 +18238,10 @@ function formatAdvisorySummary(findings, options) {
|
|
|
18222
18238
|
if (coverageGapCount > 0) {
|
|
18223
18239
|
lines.push(c.gray(` ${coverageGapCount} of ${findings.length} were coverage-gap demotions (recurred past the block limit \u2014 candidate for spec/AC review)`));
|
|
18224
18240
|
}
|
|
18241
|
+
const noActionCount = findings.filter((f) => f.actionRequired === false).length;
|
|
18242
|
+
if (noActionCount > 0) {
|
|
18243
|
+
lines.push(c.gray(` ${noActionCount} of ${findings.length} asked for no change (compliance notes \u2014 the best-effort fix pass skipped them)`));
|
|
18244
|
+
}
|
|
18225
18245
|
lines.push(c.yellow("\u2500".repeat(60)));
|
|
18226
18246
|
for (const f of sorted) {
|
|
18227
18247
|
const location = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : undefined;
|
|
@@ -18230,7 +18250,8 @@ function formatAdvisorySummary(findings, options) {
|
|
|
18230
18250
|
f.storyId ?? "unknown",
|
|
18231
18251
|
location,
|
|
18232
18252
|
f.category,
|
|
18233
|
-
f.coverageGap ? "coverage-gap" : undefined
|
|
18253
|
+
f.coverageGap ? "coverage-gap" : undefined,
|
|
18254
|
+
f.actionRequired === false ? "no-action" : undefined
|
|
18234
18255
|
].filter((v) => typeof v === "string" && v.length > 0);
|
|
18235
18256
|
lines.push(` ${c.gray(parts.join(" \xB7 "))}`);
|
|
18236
18257
|
lines.push(` ${f.issue}`);
|
|
@@ -33112,6 +33133,7 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
|
|
|
33112
33133
|
"acIndex": 2,
|
|
33113
33134
|
"scopeQuote": "<out-of-scope findings ONLY: verbatim substring of one Out of Scope entry>",
|
|
33114
33135
|
"scopeIndex": 1,
|
|
33136
|
+
"actionRequired": true,
|
|
33115
33137
|
"verifiedBy": {
|
|
33116
33138
|
"command": "command used to inspect the current codebase",
|
|
33117
33139
|
"file": "relative/path/to/file.ts",
|
|
@@ -33165,6 +33187,11 @@ A finding about code that crossed one of these boundaries must NOT cite an AC \u
|
|
|
33165
33187
|
- When the boundary is only a description "Out:" bullet, quote it in \`issue\` and leave \`scopeQuote\`/\`scopeIndex\` unset.
|
|
33166
33188
|
- Emit scope-violation findings as \`"warning"\` \u2014 never \`"error"\`. Reporting the boundary is the goal; it does not block the story.
|
|
33167
33189
|
|
|
33190
|
+
**Do not report compliance as a finding:**
|
|
33191
|
+
A finding is a request for a change. If you inspected something and the code was CORRECT \u2014 it honoured an out-of-scope boundary, satisfied a convention, handled the edge case \u2014 that is not a finding. Say it in \`passed\`, not in \`findings\`. Emitting "this is correct per Out of Scope #10 / no action needed" as a finding causes an automated fix pass to be dispatched against code that needs no fix; one such report edited working code and broke a test.
|
|
33192
|
+
|
|
33193
|
+
If you judge a no-change note genuinely worth recording, set \`actionRequired: false\` on it. Every finding that asks for a change must leave \`actionRequired\` unset or \`true\`. Never pair \`actionRequired: false\` with a \`suggestion\` that requests an edit \u2014 pick one.
|
|
33194
|
+
|
|
33168
33195
|
Never use \`acIndex: 0\`; \`acIndex\` is 1-based (first AC bullet = 1). The same applies to \`scopeIndex\`.
|
|
33169
33196
|
|
|
33170
33197
|
If you cannot find an AC that names the **specific symbol** in your finding, downgrade to \`"info"\` or \`"warning"\`. A finding dropped by the validator is worse than one correctly classified as advisory.`, AdversarialReviewPromptBuilder;
|
|
@@ -35887,6 +35914,7 @@ function toAdversarialReviewFindings(findings, opts = {}) {
|
|
|
35887
35914
|
message: f.issue,
|
|
35888
35915
|
suggestion: f.suggestion,
|
|
35889
35916
|
fixTarget: resolveFixTarget({ base: categoryToFixTarget(f.category), file: f.file, isTestFile: opts.isTestFile }),
|
|
35917
|
+
...f.actionRequired === false ? { actionRequired: false } : {},
|
|
35890
35918
|
meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
|
|
35891
35919
|
};
|
|
35892
35920
|
});
|
|
@@ -36826,11 +36854,11 @@ var init_adversarial_review = __esm(() => {
|
|
|
36826
36854
|
throw new ParseValidationError("[adversarial-review] parse failed: invalid JSON shape");
|
|
36827
36855
|
},
|
|
36828
36856
|
async verify(parsed, input, _verifyCtx) {
|
|
36857
|
+
const threshold = input.blockingThreshold ?? "error";
|
|
36829
36858
|
if (parsed.failOpen || parsed.looksLikeFail)
|
|
36830
|
-
return parsed;
|
|
36859
|
+
return { ...parsed, blockingThreshold: threshold };
|
|
36831
36860
|
if (parsed.findings.length === 0)
|
|
36832
|
-
return parsed;
|
|
36833
|
-
const threshold = input.blockingThreshold ?? "error";
|
|
36861
|
+
return { ...parsed, blockingThreshold: threshold };
|
|
36834
36862
|
const findings = parsed.findings;
|
|
36835
36863
|
const substantiated = await substantiateAdversarialFindings({
|
|
36836
36864
|
findings,
|
|
@@ -36873,11 +36901,12 @@ var init_adversarial_review = __esm(() => {
|
|
|
36873
36901
|
category: f.category
|
|
36874
36902
|
});
|
|
36875
36903
|
}
|
|
36876
|
-
const
|
|
36877
|
-
const passed = blocking.length === 0 && (parsed.passed || hadBlockingSeverity);
|
|
36904
|
+
const passed = blocking.length === 0 && (parsed.passed || accepted.length > 0);
|
|
36878
36905
|
return {
|
|
36879
36906
|
...parsed,
|
|
36880
36907
|
passed,
|
|
36908
|
+
blockingThreshold: threshold,
|
|
36909
|
+
modelPassed: parsed.passed,
|
|
36881
36910
|
findings: accepted,
|
|
36882
36911
|
normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
|
|
36883
36912
|
advisoryFindings: [
|
|
@@ -39901,9 +39930,29 @@ var init_apply_test_edit_declarations = __esm(() => {
|
|
|
39901
39930
|
});
|
|
39902
39931
|
|
|
39903
39932
|
// src/operations/validate-mock-structure-files.ts
|
|
39904
|
-
import { join as join23 } from "path";
|
|
39905
|
-
|
|
39906
|
-
|
|
39933
|
+
import { isAbsolute as isAbsolute9, join as join23, relative as relative9 } from "path";
|
|
39934
|
+
function resolutionCandidates(file3, packageDir, repoRoot) {
|
|
39935
|
+
if (isAbsolute9(file3))
|
|
39936
|
+
return [file3];
|
|
39937
|
+
const viaPackageDir = join23(packageDir, file3);
|
|
39938
|
+
if (repoRoot === undefined)
|
|
39939
|
+
return [viaPackageDir];
|
|
39940
|
+
const viaRepoRoot = join23(repoRoot, file3);
|
|
39941
|
+
return viaRepoRoot === viaPackageDir ? [viaPackageDir] : [viaPackageDir, viaRepoRoot];
|
|
39942
|
+
}
|
|
39943
|
+
async function resolvePackageRelative(file3, opts) {
|
|
39944
|
+
for (const candidate of resolutionCandidates(file3, opts.packageDir, opts.repoRoot)) {
|
|
39945
|
+
if (!await opts.fileExists(candidate))
|
|
39946
|
+
continue;
|
|
39947
|
+
const rel = relative9(opts.packageDir, candidate);
|
|
39948
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute9(rel))
|
|
39949
|
+
return null;
|
|
39950
|
+
return rel;
|
|
39951
|
+
}
|
|
39952
|
+
return null;
|
|
39953
|
+
}
|
|
39954
|
+
async function validateMockStructureFiles(declarations, resolvedTestPatterns, packageDir, opts) {
|
|
39955
|
+
const fileExists = opts?.fileExists ?? defaultFileExists;
|
|
39907
39956
|
const valid = [];
|
|
39908
39957
|
const invalid = [];
|
|
39909
39958
|
for (const d of declarations) {
|
|
@@ -39914,13 +39963,16 @@ async function validateMockStructureFiles(declarations, resolvedTestPatterns, pa
|
|
|
39914
39963
|
const files = d.files ?? [d.file];
|
|
39915
39964
|
let allValid = true;
|
|
39916
39965
|
for (const file3 of files) {
|
|
39917
|
-
const
|
|
39918
|
-
|
|
39919
|
-
|
|
39966
|
+
const packageRelative = await resolvePackageRelative(file3, {
|
|
39967
|
+
packageDir,
|
|
39968
|
+
repoRoot: opts?.repoRoot,
|
|
39969
|
+
fileExists
|
|
39970
|
+
});
|
|
39971
|
+
if (packageRelative === null) {
|
|
39920
39972
|
allValid = false;
|
|
39921
39973
|
break;
|
|
39922
39974
|
}
|
|
39923
|
-
const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(
|
|
39975
|
+
const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(packageRelative));
|
|
39924
39976
|
if (!matchesPattern) {
|
|
39925
39977
|
allValid = false;
|
|
39926
39978
|
break;
|
|
@@ -41458,7 +41510,7 @@ var init_mutation = __esm(() => {
|
|
|
41458
41510
|
});
|
|
41459
41511
|
|
|
41460
41512
|
// src/operations/mutation-check.ts
|
|
41461
|
-
import { isAbsolute as
|
|
41513
|
+
import { isAbsolute as isAbsolute10, join as join24 } from "path";
|
|
41462
41514
|
var _mutationCheckDeps, mutationCheckOp;
|
|
41463
41515
|
var init_mutation_check = __esm(() => {
|
|
41464
41516
|
init_config();
|
|
@@ -41501,7 +41553,7 @@ var init_mutation_check = __esm(() => {
|
|
|
41501
41553
|
}
|
|
41502
41554
|
const changedFiles = await deps.getChangedNonTestFiles(input.workdir, input.storyGitRef, input.packagePrefix, [...input.resolvedTestPatterns.regex], undefined, input.repoRoot);
|
|
41503
41555
|
const anchor = input.repoRoot ?? input.workdir;
|
|
41504
|
-
const absoluteChangedFiles = changedFiles.map((f) =>
|
|
41556
|
+
const absoluteChangedFiles = changedFiles.map((f) => isAbsolute10(f) ? f : join24(anchor, f));
|
|
41505
41557
|
const survivors = [];
|
|
41506
41558
|
const mutants = [];
|
|
41507
41559
|
for (const file3 of absoluteChangedFiles) {
|
|
@@ -41626,6 +41678,31 @@ var init_operations = __esm(() => {
|
|
|
41626
41678
|
init_mutation_check();
|
|
41627
41679
|
});
|
|
41628
41680
|
|
|
41681
|
+
// src/findings/cycle-retirement.ts
|
|
41682
|
+
function createDeclineLedger() {
|
|
41683
|
+
const declinedByStrategy = new Map;
|
|
41684
|
+
const hasDeclined = (strategyName, finding) => declinedByStrategy.get(strategyName)?.has(findingKey(finding)) === true;
|
|
41685
|
+
const isRetiredFor = (strategy, findings) => {
|
|
41686
|
+
const claimed = findings.filter((f) => strategy.appliesTo(f));
|
|
41687
|
+
return claimed.length > 0 && claimed.every((f) => hasDeclined(strategy.name, f));
|
|
41688
|
+
};
|
|
41689
|
+
return {
|
|
41690
|
+
recordDeclined(strategy, dispatched) {
|
|
41691
|
+
const declined = declinedByStrategy.get(strategy.name) ?? new Set;
|
|
41692
|
+
for (const f of dispatched.filter((x) => strategy.appliesTo(x)))
|
|
41693
|
+
declined.add(findingKey(f));
|
|
41694
|
+
declinedByStrategy.set(strategy.name, declined);
|
|
41695
|
+
},
|
|
41696
|
+
isRetiredFor,
|
|
41697
|
+
retiredNames(strategies, findings) {
|
|
41698
|
+
return strategies.filter((s) => isRetiredFor(s, findings)).map((s) => s.name);
|
|
41699
|
+
}
|
|
41700
|
+
};
|
|
41701
|
+
}
|
|
41702
|
+
var init_cycle_retirement = __esm(() => {
|
|
41703
|
+
init_types6();
|
|
41704
|
+
});
|
|
41705
|
+
|
|
41629
41706
|
// src/findings/cycle.ts
|
|
41630
41707
|
function normalizeValidateResult(r) {
|
|
41631
41708
|
return Array.isArray(r) ? { findings: r, shortCircuited: false } : r;
|
|
@@ -41695,17 +41772,18 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41695
41772
|
const storyId = ctx.storyId;
|
|
41696
41773
|
const packageDir = ctx.packageDir;
|
|
41697
41774
|
let totalCostUsd = 0;
|
|
41698
|
-
const
|
|
41775
|
+
const declines = createDeclineLedger();
|
|
41699
41776
|
let unresolvedDetail;
|
|
41700
41777
|
const finish = (result) => unresolvedDetail !== undefined && result.unresolvedDetail === undefined ? { ...result, unresolvedDetail } : result;
|
|
41701
41778
|
for (;; ) {
|
|
41702
41779
|
if (cycle.findings.length === 0 && cycle.verdict === undefined) {
|
|
41703
41780
|
return { iterations: cycle.iterations, finalFindings: [], exitReason: "resolved", costUsd: totalCostUsd };
|
|
41704
41781
|
}
|
|
41705
|
-
const selectable = cycle.strategies.filter((s) => !
|
|
41782
|
+
const selectable = cycle.strategies.filter((s) => !declines.isRetiredFor(s, cycle.findings));
|
|
41706
41783
|
const active = selectActiveStrategies(selectable, cycle.findings, cycle.verdict);
|
|
41707
41784
|
if (active.length === 0) {
|
|
41708
41785
|
const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
|
|
41786
|
+
const retiredStrategies = declines.retiredNames(cycle.strategies, cycle.findings);
|
|
41709
41787
|
logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
|
|
41710
41788
|
storyId,
|
|
41711
41789
|
packageDir,
|
|
@@ -41713,7 +41791,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41713
41791
|
reason: "no-strategy",
|
|
41714
41792
|
findingsCount: cycle.findings.length,
|
|
41715
41793
|
orphanSources,
|
|
41716
|
-
...
|
|
41794
|
+
...retiredStrategies.length > 0 ? { retiredStrategies } : {}
|
|
41717
41795
|
});
|
|
41718
41796
|
return finish({
|
|
41719
41797
|
iterations: cycle.iterations,
|
|
@@ -41784,7 +41862,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41784
41862
|
for (const strategy of group) {
|
|
41785
41863
|
const relevantFindings = findingsBefore.filter((f) => strategy.appliesTo(f));
|
|
41786
41864
|
const input = strategy.buildInput(relevantFindings, cycle.iterations, ctx);
|
|
41787
|
-
const
|
|
41865
|
+
const fixCtx = {
|
|
41866
|
+
...ctx,
|
|
41867
|
+
fixStrategy: { name: strategy.name, findingsBefore: findingsBefore.length }
|
|
41868
|
+
};
|
|
41869
|
+
const output = await doCallOp(fixCtx, strategy.fixOp, input);
|
|
41788
41870
|
const extracted = await (strategy.extractApplied?.(output, input) ?? {});
|
|
41789
41871
|
fixesApplied.push({
|
|
41790
41872
|
strategyName: strategy.name,
|
|
@@ -41799,8 +41881,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41799
41881
|
if (unresolvedFas.length > 0) {
|
|
41800
41882
|
const firstUnresolved = unresolvedFas[0];
|
|
41801
41883
|
unresolvedDetail = firstUnresolved.unresolved;
|
|
41802
|
-
for (const fa of unresolvedFas)
|
|
41803
|
-
|
|
41884
|
+
for (const fa of unresolvedFas) {
|
|
41885
|
+
const strategy = group.find((s) => s.name === fa.strategyName);
|
|
41886
|
+
if (strategy)
|
|
41887
|
+
declines.recordDeclined(strategy, findingsBefore);
|
|
41888
|
+
}
|
|
41804
41889
|
const allGaveUp = unresolvedFas.length === fixesApplied.length;
|
|
41805
41890
|
if (allGaveUp) {
|
|
41806
41891
|
const finishedAt2 = now();
|
|
@@ -42015,6 +42100,7 @@ var _cycleDeps;
|
|
|
42015
42100
|
var init_cycle = __esm(() => {
|
|
42016
42101
|
init_logger2();
|
|
42017
42102
|
init_operations();
|
|
42103
|
+
init_cycle_retirement();
|
|
42018
42104
|
init_types6();
|
|
42019
42105
|
_cycleDeps = {
|
|
42020
42106
|
callOp,
|
|
@@ -42346,13 +42432,13 @@ var init_finding_projection = __esm(() => {
|
|
|
42346
42432
|
});
|
|
42347
42433
|
|
|
42348
42434
|
// src/review/prepare-inputs.ts
|
|
42349
|
-
import { relative as
|
|
42435
|
+
import { relative as relative10, sep } from "path";
|
|
42350
42436
|
function derivePackageDirs(workdir, projectDir) {
|
|
42351
42437
|
const repoRoot = projectDir ?? workdir;
|
|
42352
42438
|
const packageDir = workdir !== repoRoot ? workdir : undefined;
|
|
42353
42439
|
let packageDirRelative;
|
|
42354
42440
|
if (projectDir && workdir !== projectDir) {
|
|
42355
|
-
const rel =
|
|
42441
|
+
const rel = relative10(projectDir, workdir);
|
|
42356
42442
|
if (rel !== ".." && !rel.startsWith(`..${sep}`)) {
|
|
42357
42443
|
packageDirRelative = rel && rel !== "." ? rel : undefined;
|
|
42358
42444
|
}
|
|
@@ -42493,7 +42579,7 @@ var package_default;
|
|
|
42493
42579
|
var init_package = __esm(() => {
|
|
42494
42580
|
package_default = {
|
|
42495
42581
|
name: "@nathapp/nax",
|
|
42496
|
-
version: "0.75.
|
|
42582
|
+
version: "0.75.3",
|
|
42497
42583
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
42498
42584
|
type: "module",
|
|
42499
42585
|
bin: {
|
|
@@ -42504,9 +42590,10 @@ var init_package = __esm(() => {
|
|
|
42504
42590
|
dev: "bun run bin/nax.ts",
|
|
42505
42591
|
build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
|
|
42506
42592
|
typecheck: "bun x tsc --noEmit && bun x tsc --noEmit -p tsconfig.contracts.json",
|
|
42507
|
-
lint: "bun x biome check src/ bin/ flows/ && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
|
|
42593
|
+
lint: "bun x biome check src/ bin/ flows/ && bun run check:flows-no-bun && bun run check:no-real-global-nax && bun run check:alias-internals && bun run check:deep-relatives && bun run check:nax-error && bun run check:logger-storyid && bun run check:log-format-layering && bun run check:file-sizes",
|
|
42508
42594
|
"lint:json": "bun x biome check src/ bin/ flows/ --reporter json && bun run check:nax-error 1>&2 && bun run check:logger-storyid 1>&2",
|
|
42509
42595
|
"lint:fix": "bun x biome check --write src/ bin/ flows/",
|
|
42596
|
+
"check:flows-no-bun": "bun run scripts/check-flows-no-bun.ts",
|
|
42510
42597
|
"check:no-real-global-nax": "bun run scripts/check-no-real-global-nax.ts",
|
|
42511
42598
|
"check:alias-internals": "bun run scripts/check-alias-internals.ts",
|
|
42512
42599
|
"check:deep-relatives": "bun run scripts/check-deep-relatives.ts",
|
|
@@ -42596,8 +42683,8 @@ var init_version = __esm(() => {
|
|
|
42596
42683
|
NAX_VERSION = package_default.version;
|
|
42597
42684
|
NAX_COMMIT = (() => {
|
|
42598
42685
|
try {
|
|
42599
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
42600
|
-
return "
|
|
42686
|
+
if (/^[0-9a-f]{6,10}$/.test("ae716fd4"))
|
|
42687
|
+
return "ae716fd4";
|
|
42601
42688
|
} catch {}
|
|
42602
42689
|
try {
|
|
42603
42690
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -42686,7 +42773,8 @@ function toAdvisorySummaryEntries(entry) {
|
|
|
42686
42773
|
file: f.file,
|
|
42687
42774
|
line: f.line,
|
|
42688
42775
|
issue: f.issue ?? "(no description)",
|
|
42689
|
-
coverageGap: f.meta?.coverageGap === true ? true : undefined
|
|
42776
|
+
coverageGap: f.meta?.coverageGap === true ? true : undefined,
|
|
42777
|
+
actionRequired: f.actionRequired === false ? false : undefined
|
|
42690
42778
|
};
|
|
42691
42779
|
});
|
|
42692
42780
|
}
|
|
@@ -43110,12 +43198,10 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
|
|
|
43110
43198
|
cost: llmCost
|
|
43111
43199
|
};
|
|
43112
43200
|
}
|
|
43113
|
-
if (!opResult.
|
|
43114
|
-
|
|
43115
|
-
if (allHallucinated) {
|
|
43201
|
+
if (!opResult.modelPassed && acDropped.length > 0) {
|
|
43202
|
+
if (acDropped.every((d) => d.code === "ac_quote_not_substring")) {
|
|
43116
43203
|
const demotedFindings = toAdversarialReviewFindings(acDropped.map((d) => ({ ...d.finding, severity: "warning", acQuote: undefined, acIndex: undefined })), { isTestFile: testFileMatch });
|
|
43117
|
-
const
|
|
43118
|
-
const allAdvisory = [...existingAdvisory, ...demotedFindings];
|
|
43204
|
+
const allAdvisory = [...advisoryFindingsAsFindings, ...demotedFindings];
|
|
43119
43205
|
logger?.warn("review", "Adversarial review passed: all blocking findings discarded as hallucinated AC quotes", {
|
|
43120
43206
|
storyId: story.id,
|
|
43121
43207
|
durationMs,
|
|
@@ -43422,7 +43508,7 @@ var init_language_commands = __esm(() => {
|
|
|
43422
43508
|
});
|
|
43423
43509
|
|
|
43424
43510
|
// src/review/scoped-lint.ts
|
|
43425
|
-
import { join as join27, relative as
|
|
43511
|
+
import { join as join27, relative as relative11 } from "path";
|
|
43426
43512
|
function shellQuotePath4(path6) {
|
|
43427
43513
|
return `'${path6.replaceAll("'", "'\\''")}'`;
|
|
43428
43514
|
}
|
|
@@ -43458,7 +43544,7 @@ async function listChangedFiles(workdir, baseRef) {
|
|
|
43458
43544
|
function inferActivePackageDir(workdir, projectDir) {
|
|
43459
43545
|
if (!projectDir)
|
|
43460
43546
|
return;
|
|
43461
|
-
const rel = normalizePath3(
|
|
43547
|
+
const rel = normalizePath3(relative11(projectDir, workdir));
|
|
43462
43548
|
if (!rel || rel === "." || rel.startsWith(".."))
|
|
43463
43549
|
return;
|
|
43464
43550
|
return rel;
|
|
@@ -44612,6 +44698,7 @@ var init_runner2 = __esm(() => {
|
|
|
44612
44698
|
// src/review/index.ts
|
|
44613
44699
|
var init_review = __esm(() => {
|
|
44614
44700
|
init_semantic_helpers();
|
|
44701
|
+
init_adversarial_helpers();
|
|
44615
44702
|
init_category_fix_target();
|
|
44616
44703
|
init_finding_filters();
|
|
44617
44704
|
init_ac_quote_validator();
|
|
@@ -44896,7 +44983,10 @@ REASON: <one paragraph: which mock is wrong vs which dispatch the new code uses,
|
|
|
44896
44983
|
Rules:
|
|
44897
44984
|
- Do NOT make any edits yourself; the test-writer will fulfill.
|
|
44898
44985
|
- Do NOT also emit \`UNRESOLVED:\` in the same turn \u2014 this declaration IS the handoff.
|
|
44899
|
-
- FILES must list real test files. Each path must exist and be a test file
|
|
44986
|
+
- FILES must list real test files. Each path must exist and be a test file.
|
|
44987
|
+
- Write each path exactly as it appears in the findings above (repository-relative).
|
|
44988
|
+
Paths that resolve under neither the repository root nor the package directory are
|
|
44989
|
+
rejected and the handoff is dropped \u2014 the findings then have no owner.`, SINGLE_SESSION_PERMIT_HEADLINE = "You authored these tests in the same session as the implementation, so you MAY edit test files \u2014 but ONLY to resolve a genuine contradiction between a test and this story's acceptance criteria (or between two acceptance criteria). NEVER weaken, delete, loosen, or skip a test merely to make it pass. See the test-edit guidance appended below.", SINGLE_SESSION_TEST_EDIT_POLICY = `
|
|
44900
44990
|
|
|
44901
44991
|
## Test-edit guidance (single-session implementer)
|
|
44902
44992
|
|
|
@@ -48678,7 +48768,7 @@ var init_pid_registry = __esm(() => {
|
|
|
48678
48768
|
// src/session/manager-deps.ts
|
|
48679
48769
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
48680
48770
|
import { mkdir as mkdir5 } from "fs/promises";
|
|
48681
|
-
import { isAbsolute as
|
|
48771
|
+
import { isAbsolute as isAbsolute11, join as join30, relative as relative12, sep as sep2 } from "path";
|
|
48682
48772
|
function resolveProjectDirFromScratchDir(scratchDir) {
|
|
48683
48773
|
const marker = `${sep2}.nax${sep2}features${sep2}`;
|
|
48684
48774
|
const markerIdx = scratchDir.lastIndexOf(marker);
|
|
@@ -48690,7 +48780,7 @@ function resolveProjectDirFromScratchDir(scratchDir) {
|
|
|
48690
48780
|
return;
|
|
48691
48781
|
}
|
|
48692
48782
|
function toProjectRelativePath(projectDir, pathValue) {
|
|
48693
|
-
const relativePath =
|
|
48783
|
+
const relativePath = isAbsolute11(pathValue) ? relative12(projectDir, pathValue) : pathValue;
|
|
48694
48784
|
return relativePath === "" ? "." : relativePath;
|
|
48695
48785
|
}
|
|
48696
48786
|
var _sessionManagerDeps;
|
|
@@ -50200,7 +50290,7 @@ var init_windsurf = __esm(() => {
|
|
|
50200
50290
|
|
|
50201
50291
|
// src/context/generator.ts
|
|
50202
50292
|
import { existsSync as existsSync9 } from "fs";
|
|
50203
|
-
import { join as join33, relative as
|
|
50293
|
+
import { join as join33, relative as relative13 } from "path";
|
|
50204
50294
|
async function loadContextContent(options, config2) {
|
|
50205
50295
|
if (!_generatorDeps.existsSync(options.contextPath)) {
|
|
50206
50296
|
throw new Error(`Context file not found: ${options.contextPath}`);
|
|
@@ -50328,7 +50418,7 @@ async function discoverWorkspacePackages2(repoRoot) {
|
|
|
50328
50418
|
}
|
|
50329
50419
|
async function generateForPackage(packageDir, config2, dryRun = false, repoRoot) {
|
|
50330
50420
|
const resolvedRepoRoot = repoRoot ?? packageDir;
|
|
50331
|
-
const relativePkgPath =
|
|
50421
|
+
const relativePkgPath = relative13(resolvedRepoRoot, packageDir);
|
|
50332
50422
|
const contextPath = join33(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
|
|
50333
50423
|
if (!_generatorDeps.existsSync(contextPath)) {
|
|
50334
50424
|
return [
|
|
@@ -54175,7 +54265,7 @@ var init_checks_blockers = __esm(() => {
|
|
|
54175
54265
|
|
|
54176
54266
|
// src/precheck/checks-warnings.ts
|
|
54177
54267
|
import { existsSync as existsSync14 } from "fs";
|
|
54178
|
-
import { isAbsolute as
|
|
54268
|
+
import { isAbsolute as isAbsolute12 } from "path";
|
|
54179
54269
|
async function checkClaudeMdExists(workdir) {
|
|
54180
54270
|
const claudeMdPath = `${workdir}/CLAUDE.md`;
|
|
54181
54271
|
const passed = existsSync14(claudeMdPath);
|
|
@@ -54310,7 +54400,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
|
|
|
54310
54400
|
}
|
|
54311
54401
|
async function checkHomeEnvValid() {
|
|
54312
54402
|
const home = process.env.HOME ?? "";
|
|
54313
|
-
const passed = home !== "" &&
|
|
54403
|
+
const passed = home !== "" && isAbsolute12(home);
|
|
54314
54404
|
return {
|
|
54315
54405
|
name: "home-env-valid",
|
|
54316
54406
|
tier: "warning",
|
|
@@ -57014,6 +57104,79 @@ ${stderr}`;
|
|
|
57014
57104
|
};
|
|
57015
57105
|
});
|
|
57016
57106
|
|
|
57107
|
+
// src/pipeline/event-bus.ts
|
|
57108
|
+
class PipelineEventBus {
|
|
57109
|
+
subscribers = new Map;
|
|
57110
|
+
_pending = new Set;
|
|
57111
|
+
on(eventType, subscriber) {
|
|
57112
|
+
const list = this.subscribers.get(eventType) ?? [];
|
|
57113
|
+
list.push(subscriber);
|
|
57114
|
+
this.subscribers.set(eventType, list);
|
|
57115
|
+
return () => {
|
|
57116
|
+
const current = this.subscribers.get(eventType) ?? [];
|
|
57117
|
+
this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
|
|
57118
|
+
};
|
|
57119
|
+
}
|
|
57120
|
+
onAll(subscriber) {
|
|
57121
|
+
const list = this.subscribers.get("*") ?? [];
|
|
57122
|
+
list.push(subscriber);
|
|
57123
|
+
this.subscribers.set("*", list);
|
|
57124
|
+
return () => {
|
|
57125
|
+
const current = this.subscribers.get("*") ?? [];
|
|
57126
|
+
this.subscribers.set("*", current.filter((s) => s !== subscriber));
|
|
57127
|
+
};
|
|
57128
|
+
}
|
|
57129
|
+
emit(event) {
|
|
57130
|
+
const logger = getLogger();
|
|
57131
|
+
const specific = this.subscribers.get(event.type) ?? [];
|
|
57132
|
+
const all = this.subscribers.get("*") ?? [];
|
|
57133
|
+
const targets = [...specific, ...all];
|
|
57134
|
+
for (const sub of targets) {
|
|
57135
|
+
try {
|
|
57136
|
+
const result = sub(event);
|
|
57137
|
+
if (result instanceof Promise) {
|
|
57138
|
+
const tracked = result.catch((err) => {
|
|
57139
|
+
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57140
|
+
});
|
|
57141
|
+
this._pending.add(tracked);
|
|
57142
|
+
tracked.finally(() => this._pending.delete(tracked));
|
|
57143
|
+
}
|
|
57144
|
+
} catch (err) {
|
|
57145
|
+
logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
|
|
57146
|
+
}
|
|
57147
|
+
}
|
|
57148
|
+
}
|
|
57149
|
+
async emitAsync(event) {
|
|
57150
|
+
const logger = getLogger();
|
|
57151
|
+
const specific = this.subscribers.get(event.type) ?? [];
|
|
57152
|
+
const all = this.subscribers.get("*") ?? [];
|
|
57153
|
+
const targets = [...specific, ...all];
|
|
57154
|
+
await Promise.allSettled(targets.map(async (sub) => {
|
|
57155
|
+
try {
|
|
57156
|
+
await sub(event);
|
|
57157
|
+
} catch (err) {
|
|
57158
|
+
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57159
|
+
}
|
|
57160
|
+
}));
|
|
57161
|
+
}
|
|
57162
|
+
async drain() {
|
|
57163
|
+
if (this._pending.size === 0)
|
|
57164
|
+
return;
|
|
57165
|
+
await Promise.allSettled([...this._pending]);
|
|
57166
|
+
}
|
|
57167
|
+
clear() {
|
|
57168
|
+
this.subscribers.clear();
|
|
57169
|
+
}
|
|
57170
|
+
subscriberCount(eventType) {
|
|
57171
|
+
return (this.subscribers.get(eventType) ?? []).length;
|
|
57172
|
+
}
|
|
57173
|
+
}
|
|
57174
|
+
var pipelineEventBus;
|
|
57175
|
+
var init_event_bus = __esm(() => {
|
|
57176
|
+
init_logger2();
|
|
57177
|
+
pipelineEventBus = new PipelineEventBus;
|
|
57178
|
+
});
|
|
57179
|
+
|
|
57017
57180
|
// src/pipeline/stages/acceptance-setup.ts
|
|
57018
57181
|
var exports_acceptance_setup = {};
|
|
57019
57182
|
__export(exports_acceptance_setup, {
|
|
@@ -57029,6 +57192,214 @@ function computeACFingerprint(criteria) {
|
|
|
57029
57192
|
hasher.update(sorted);
|
|
57030
57193
|
return `sha256:${hasher.digest("hex")}`;
|
|
57031
57194
|
}
|
|
57195
|
+
async function runAcceptanceSetup(ctx, featureDir, phaseStartTime) {
|
|
57196
|
+
const language = ctx.config.project?.language;
|
|
57197
|
+
const testPathConfig = ctx.config.acceptance.testPath;
|
|
57198
|
+
const metaPath = path12.join(featureDir, "acceptance-meta.json");
|
|
57199
|
+
const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
|
|
57200
|
+
const featureName = ctx.prd.feature ?? ctx.prd.featureName;
|
|
57201
|
+
const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
|
|
57202
|
+
const nonFixStories = groups.flatMap((g) => g.stories);
|
|
57203
|
+
let totalCriteria = 0;
|
|
57204
|
+
let testableCount = 0;
|
|
57205
|
+
const fingerprint = computeACFingerprint(allCriteria);
|
|
57206
|
+
const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
|
|
57207
|
+
getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
|
|
57208
|
+
currentFingerprint: fingerprint,
|
|
57209
|
+
storedFingerprint: meta3?.acFingerprint ?? "none",
|
|
57210
|
+
match: meta3?.acFingerprint === fingerprint
|
|
57211
|
+
});
|
|
57212
|
+
let shouldGenerate = false;
|
|
57213
|
+
let regenerated = false;
|
|
57214
|
+
if (!meta3 || meta3.acFingerprint !== fingerprint) {
|
|
57215
|
+
if (!meta3) {
|
|
57216
|
+
getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
|
|
57217
|
+
} else {
|
|
57218
|
+
getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
|
|
57219
|
+
reason: "fingerprint mismatch",
|
|
57220
|
+
currentFingerprint: fingerprint,
|
|
57221
|
+
storedFingerprint: meta3.acFingerprint
|
|
57222
|
+
});
|
|
57223
|
+
}
|
|
57224
|
+
for (const { testPath } of groups) {
|
|
57225
|
+
if (await _acceptanceSetupDeps.fileExists(testPath)) {
|
|
57226
|
+
await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
|
|
57227
|
+
await _acceptanceSetupDeps.deleteFile(testPath);
|
|
57228
|
+
}
|
|
57229
|
+
}
|
|
57230
|
+
await _acceptanceSetupDeps.deleteSemanticVerdicts(featureDir);
|
|
57231
|
+
shouldGenerate = true;
|
|
57232
|
+
regenerated = true;
|
|
57233
|
+
} else {
|
|
57234
|
+
getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
|
|
57235
|
+
}
|
|
57236
|
+
if (shouldGenerate) {
|
|
57237
|
+
totalCriteria = allCriteria.length;
|
|
57238
|
+
let allRefinedCriteria;
|
|
57239
|
+
if (ctx.config.acceptance.refinement) {
|
|
57240
|
+
const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
|
|
57241
|
+
const results = new Array(nonFixStories.length);
|
|
57242
|
+
const executing = new Set;
|
|
57243
|
+
for (let i = 0;i < nonFixStories.length; i++) {
|
|
57244
|
+
const story = nonFixStories[i];
|
|
57245
|
+
const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
|
|
57246
|
+
criteria: story.acceptanceCriteria,
|
|
57247
|
+
codebaseContext: "",
|
|
57248
|
+
storyId: story.id,
|
|
57249
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
57250
|
+
testFramework: ctx.config.acceptance.testFramework,
|
|
57251
|
+
storyTitle: story.title,
|
|
57252
|
+
storyDescription: story.description
|
|
57253
|
+
}, story.id).then((refined) => {
|
|
57254
|
+
results[i] = refined;
|
|
57255
|
+
}).catch(() => {
|
|
57256
|
+
getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
|
|
57257
|
+
storyId: story.id
|
|
57258
|
+
});
|
|
57259
|
+
results[i] = story.acceptanceCriteria.map((c) => ({
|
|
57260
|
+
original: c,
|
|
57261
|
+
refined: c,
|
|
57262
|
+
testable: true,
|
|
57263
|
+
storyId: story.id
|
|
57264
|
+
}));
|
|
57265
|
+
}).finally(() => {
|
|
57266
|
+
executing.delete(task);
|
|
57267
|
+
});
|
|
57268
|
+
executing.add(task);
|
|
57269
|
+
if (executing.size >= maxConcurrency) {
|
|
57270
|
+
await Promise.race(executing);
|
|
57271
|
+
}
|
|
57272
|
+
}
|
|
57273
|
+
await Promise.all(executing);
|
|
57274
|
+
allRefinedCriteria = results.flat();
|
|
57275
|
+
} else {
|
|
57276
|
+
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
57277
|
+
original: c,
|
|
57278
|
+
refined: c,
|
|
57279
|
+
testable: true,
|
|
57280
|
+
storyId: story.id
|
|
57281
|
+
})));
|
|
57282
|
+
}
|
|
57283
|
+
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
57284
|
+
for (const group of groups) {
|
|
57285
|
+
const { testPath, packageDir } = group;
|
|
57286
|
+
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
57287
|
+
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
57288
|
+
const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
|
|
57289
|
+
`);
|
|
57290
|
+
const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
|
|
57291
|
+
[FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
|
|
57292
|
+
const groupStoryId = group.stories[0]?.id;
|
|
57293
|
+
const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
|
|
57294
|
+
featureName: featureName ?? "",
|
|
57295
|
+
criteriaList,
|
|
57296
|
+
frameworkOverrideLine,
|
|
57297
|
+
targetTestFilePath: testPath,
|
|
57298
|
+
..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
|
|
57299
|
+
}, groupStoryId);
|
|
57300
|
+
const testCode = genResult.testCode;
|
|
57301
|
+
if (testCode) {
|
|
57302
|
+
await _acceptanceSetupDeps.writeFile(testPath, testCode);
|
|
57303
|
+
} else {
|
|
57304
|
+
const skeletonCriteria = groupRefined.map((c, i) => ({
|
|
57305
|
+
id: `AC-${i + 1}`,
|
|
57306
|
+
text: c.refined,
|
|
57307
|
+
lineNumber: i + 1
|
|
57308
|
+
}));
|
|
57309
|
+
const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
|
|
57310
|
+
await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
|
|
57311
|
+
getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
|
|
57312
|
+
storyId: groupStoryId,
|
|
57313
|
+
testPath
|
|
57314
|
+
});
|
|
57315
|
+
}
|
|
57316
|
+
}
|
|
57317
|
+
if (allRefinedCriteria.length > 0) {
|
|
57318
|
+
const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
|
|
57319
|
+
acId: `AC-${i + 1}`,
|
|
57320
|
+
original: c.original,
|
|
57321
|
+
refined: c.refined,
|
|
57322
|
+
testable: c.testable,
|
|
57323
|
+
storyId: c.storyId
|
|
57324
|
+
})), null, 2);
|
|
57325
|
+
await _acceptanceSetupDeps.writeFile(path12.join(featureDir, "acceptance-refined.json"), refinedJsonContent);
|
|
57326
|
+
}
|
|
57327
|
+
const fingerprint2 = computeACFingerprint(allCriteria);
|
|
57328
|
+
await _acceptanceSetupDeps.writeMeta(metaPath, {
|
|
57329
|
+
generatedAt: new Date().toISOString(),
|
|
57330
|
+
acFingerprint: fingerprint2,
|
|
57331
|
+
storyCount: ctx.prd.userStories.length,
|
|
57332
|
+
acCount: totalCriteria,
|
|
57333
|
+
generator: "nax"
|
|
57334
|
+
});
|
|
57335
|
+
await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
|
|
57336
|
+
}
|
|
57337
|
+
const acceptanceTestPaths = [];
|
|
57338
|
+
for (const g of groups) {
|
|
57339
|
+
const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
|
|
57340
|
+
let groupConfig = ctx.config;
|
|
57341
|
+
if (relativeWorkdir && relativeWorkdir !== ".") {
|
|
57342
|
+
try {
|
|
57343
|
+
groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
|
|
57344
|
+
} catch {
|
|
57345
|
+
groupConfig = ctx.config;
|
|
57346
|
+
}
|
|
57347
|
+
}
|
|
57348
|
+
acceptanceTestPaths.push({
|
|
57349
|
+
testPath: g.testPath,
|
|
57350
|
+
packageDir: g.packageDir,
|
|
57351
|
+
testFramework: groupConfig.project?.testFramework,
|
|
57352
|
+
commandOverride: groupConfig.acceptance.command
|
|
57353
|
+
});
|
|
57354
|
+
}
|
|
57355
|
+
ctx.acceptanceTestPaths = acceptanceTestPaths;
|
|
57356
|
+
if (ctx.config.acceptance.redGate === false) {
|
|
57357
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57358
|
+
pipelineEventBus.emit({
|
|
57359
|
+
type: "postrun:phase:completed",
|
|
57360
|
+
phase: "acceptance-setup",
|
|
57361
|
+
passed: true,
|
|
57362
|
+
durationMs: Date.now() - phaseStartTime,
|
|
57363
|
+
details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
|
|
57364
|
+
});
|
|
57365
|
+
return { action: "continue" };
|
|
57366
|
+
}
|
|
57367
|
+
let redFailCount = 0;
|
|
57368
|
+
for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
|
|
57369
|
+
const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
|
|
57370
|
+
getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
|
|
57371
|
+
cmd: runCmd.join(" "),
|
|
57372
|
+
packageDir
|
|
57373
|
+
});
|
|
57374
|
+
const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
|
|
57375
|
+
if (exitCode !== 0) {
|
|
57376
|
+
redFailCount++;
|
|
57377
|
+
}
|
|
57378
|
+
}
|
|
57379
|
+
if (redFailCount === 0) {
|
|
57380
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57381
|
+
pipelineEventBus.emit({
|
|
57382
|
+
type: "postrun:phase:completed",
|
|
57383
|
+
phase: "acceptance-setup",
|
|
57384
|
+
passed: true,
|
|
57385
|
+
durationMs: Date.now() - phaseStartTime,
|
|
57386
|
+
details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
|
|
57387
|
+
});
|
|
57388
|
+
return {
|
|
57389
|
+
action: "skip",
|
|
57390
|
+
reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
|
|
57391
|
+
};
|
|
57392
|
+
}
|
|
57393
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
|
|
57394
|
+
pipelineEventBus.emit({
|
|
57395
|
+
type: "postrun:phase:completed",
|
|
57396
|
+
phase: "acceptance-setup",
|
|
57397
|
+
passed: true,
|
|
57398
|
+
durationMs: Date.now() - phaseStartTime,
|
|
57399
|
+
details: { totalCriteria, testableCount, redFailCount, regenerated }
|
|
57400
|
+
});
|
|
57401
|
+
return { action: "continue" };
|
|
57402
|
+
}
|
|
57032
57403
|
var _acceptanceSetupDeps, acceptanceSetupStage;
|
|
57033
57404
|
var init_acceptance_setup = __esm(() => {
|
|
57034
57405
|
init_acceptance2();
|
|
@@ -57037,6 +57408,7 @@ var init_acceptance_setup = __esm(() => {
|
|
|
57037
57408
|
init_logger2();
|
|
57038
57409
|
init_operations();
|
|
57039
57410
|
init_git();
|
|
57411
|
+
init_event_bus();
|
|
57040
57412
|
_acceptanceSetupDeps = {
|
|
57041
57413
|
getAgent: (_name) => {
|
|
57042
57414
|
return;
|
|
@@ -57136,189 +57508,19 @@ ${stderr}` };
|
|
|
57136
57508
|
if (!ctx.featureDir) {
|
|
57137
57509
|
return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
|
|
57138
57510
|
}
|
|
57139
|
-
const
|
|
57140
|
-
|
|
57141
|
-
|
|
57142
|
-
|
|
57143
|
-
|
|
57144
|
-
|
|
57145
|
-
|
|
57146
|
-
|
|
57147
|
-
|
|
57148
|
-
|
|
57149
|
-
const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
|
|
57150
|
-
getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
|
|
57151
|
-
currentFingerprint: fingerprint,
|
|
57152
|
-
storedFingerprint: meta3?.acFingerprint ?? "none",
|
|
57153
|
-
match: meta3?.acFingerprint === fingerprint
|
|
57154
|
-
});
|
|
57155
|
-
let shouldGenerate = false;
|
|
57156
|
-
if (!meta3 || meta3.acFingerprint !== fingerprint) {
|
|
57157
|
-
if (!meta3) {
|
|
57158
|
-
getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
|
|
57159
|
-
} else {
|
|
57160
|
-
getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
|
|
57161
|
-
reason: "fingerprint mismatch",
|
|
57162
|
-
currentFingerprint: fingerprint,
|
|
57163
|
-
storedFingerprint: meta3.acFingerprint
|
|
57164
|
-
});
|
|
57165
|
-
}
|
|
57166
|
-
for (const { testPath } of groups) {
|
|
57167
|
-
if (await _acceptanceSetupDeps.fileExists(testPath)) {
|
|
57168
|
-
await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
|
|
57169
|
-
await _acceptanceSetupDeps.deleteFile(testPath);
|
|
57170
|
-
}
|
|
57171
|
-
}
|
|
57172
|
-
await _acceptanceSetupDeps.deleteSemanticVerdicts(ctx.featureDir);
|
|
57173
|
-
shouldGenerate = true;
|
|
57174
|
-
} else {
|
|
57175
|
-
getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
|
|
57176
|
-
}
|
|
57177
|
-
if (shouldGenerate) {
|
|
57178
|
-
totalCriteria = allCriteria.length;
|
|
57179
|
-
let allRefinedCriteria;
|
|
57180
|
-
if (ctx.config.acceptance.refinement) {
|
|
57181
|
-
const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
|
|
57182
|
-
const results = new Array(nonFixStories.length);
|
|
57183
|
-
const executing = new Set;
|
|
57184
|
-
for (let i = 0;i < nonFixStories.length; i++) {
|
|
57185
|
-
const story = nonFixStories[i];
|
|
57186
|
-
const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
|
|
57187
|
-
criteria: story.acceptanceCriteria,
|
|
57188
|
-
codebaseContext: "",
|
|
57189
|
-
storyId: story.id,
|
|
57190
|
-
testStrategy: ctx.config.acceptance.testStrategy,
|
|
57191
|
-
testFramework: ctx.config.acceptance.testFramework,
|
|
57192
|
-
storyTitle: story.title,
|
|
57193
|
-
storyDescription: story.description
|
|
57194
|
-
}, story.id).then((refined) => {
|
|
57195
|
-
results[i] = refined;
|
|
57196
|
-
}).catch(() => {
|
|
57197
|
-
getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
|
|
57198
|
-
storyId: story.id
|
|
57199
|
-
});
|
|
57200
|
-
results[i] = story.acceptanceCriteria.map((c) => ({
|
|
57201
|
-
original: c,
|
|
57202
|
-
refined: c,
|
|
57203
|
-
testable: true,
|
|
57204
|
-
storyId: story.id
|
|
57205
|
-
}));
|
|
57206
|
-
}).finally(() => {
|
|
57207
|
-
executing.delete(task);
|
|
57208
|
-
});
|
|
57209
|
-
executing.add(task);
|
|
57210
|
-
if (executing.size >= maxConcurrency) {
|
|
57211
|
-
await Promise.race(executing);
|
|
57212
|
-
}
|
|
57213
|
-
}
|
|
57214
|
-
await Promise.all(executing);
|
|
57215
|
-
allRefinedCriteria = results.flat();
|
|
57216
|
-
} else {
|
|
57217
|
-
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
57218
|
-
original: c,
|
|
57219
|
-
refined: c,
|
|
57220
|
-
testable: true,
|
|
57221
|
-
storyId: story.id
|
|
57222
|
-
})));
|
|
57223
|
-
}
|
|
57224
|
-
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
57225
|
-
for (const group of groups) {
|
|
57226
|
-
const { testPath, packageDir } = group;
|
|
57227
|
-
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
57228
|
-
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
57229
|
-
const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
|
|
57230
|
-
`);
|
|
57231
|
-
const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
|
|
57232
|
-
[FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
|
|
57233
|
-
const groupStoryId = group.stories[0]?.id;
|
|
57234
|
-
const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
|
|
57235
|
-
featureName: featureName ?? "",
|
|
57236
|
-
criteriaList,
|
|
57237
|
-
frameworkOverrideLine,
|
|
57238
|
-
targetTestFilePath: testPath,
|
|
57239
|
-
..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
|
|
57240
|
-
}, groupStoryId);
|
|
57241
|
-
const testCode = genResult.testCode;
|
|
57242
|
-
if (testCode) {
|
|
57243
|
-
await _acceptanceSetupDeps.writeFile(testPath, testCode);
|
|
57244
|
-
} else {
|
|
57245
|
-
const skeletonCriteria = groupRefined.map((c, i) => ({
|
|
57246
|
-
id: `AC-${i + 1}`,
|
|
57247
|
-
text: c.refined,
|
|
57248
|
-
lineNumber: i + 1
|
|
57249
|
-
}));
|
|
57250
|
-
const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
|
|
57251
|
-
await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
|
|
57252
|
-
getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
|
|
57253
|
-
storyId: groupStoryId,
|
|
57254
|
-
testPath
|
|
57255
|
-
});
|
|
57256
|
-
}
|
|
57257
|
-
}
|
|
57258
|
-
if (allRefinedCriteria.length > 0) {
|
|
57259
|
-
const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
|
|
57260
|
-
acId: `AC-${i + 1}`,
|
|
57261
|
-
original: c.original,
|
|
57262
|
-
refined: c.refined,
|
|
57263
|
-
testable: c.testable,
|
|
57264
|
-
storyId: c.storyId
|
|
57265
|
-
})), null, 2);
|
|
57266
|
-
await _acceptanceSetupDeps.writeFile(path12.join(ctx.featureDir, "acceptance-refined.json"), refinedJsonContent);
|
|
57267
|
-
}
|
|
57268
|
-
const fingerprint2 = computeACFingerprint(allCriteria);
|
|
57269
|
-
await _acceptanceSetupDeps.writeMeta(metaPath, {
|
|
57270
|
-
generatedAt: new Date().toISOString(),
|
|
57271
|
-
acFingerprint: fingerprint2,
|
|
57272
|
-
storyCount: ctx.prd.userStories.length,
|
|
57273
|
-
acCount: totalCriteria,
|
|
57274
|
-
generator: "nax"
|
|
57275
|
-
});
|
|
57276
|
-
await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
|
|
57277
|
-
}
|
|
57278
|
-
const acceptanceTestPaths = [];
|
|
57279
|
-
for (const g of groups) {
|
|
57280
|
-
const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
|
|
57281
|
-
let groupConfig = ctx.config;
|
|
57282
|
-
if (relativeWorkdir && relativeWorkdir !== ".") {
|
|
57283
|
-
try {
|
|
57284
|
-
groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
|
|
57285
|
-
} catch {
|
|
57286
|
-
groupConfig = ctx.config;
|
|
57287
|
-
}
|
|
57288
|
-
}
|
|
57289
|
-
acceptanceTestPaths.push({
|
|
57290
|
-
testPath: g.testPath,
|
|
57291
|
-
packageDir: g.packageDir,
|
|
57292
|
-
testFramework: groupConfig.project?.testFramework,
|
|
57293
|
-
commandOverride: groupConfig.acceptance.command
|
|
57294
|
-
});
|
|
57295
|
-
}
|
|
57296
|
-
ctx.acceptanceTestPaths = acceptanceTestPaths;
|
|
57297
|
-
if (ctx.config.acceptance.redGate === false) {
|
|
57298
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57299
|
-
return { action: "continue" };
|
|
57300
|
-
}
|
|
57301
|
-
let redFailCount = 0;
|
|
57302
|
-
for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
|
|
57303
|
-
const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
|
|
57304
|
-
getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
|
|
57305
|
-
cmd: runCmd.join(" "),
|
|
57306
|
-
packageDir
|
|
57511
|
+
const phaseStartTime = Date.now();
|
|
57512
|
+
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance-setup" });
|
|
57513
|
+
try {
|
|
57514
|
+
return await runAcceptanceSetup(ctx, ctx.featureDir, phaseStartTime);
|
|
57515
|
+
} catch (err) {
|
|
57516
|
+
pipelineEventBus.emit({
|
|
57517
|
+
type: "postrun:phase:completed",
|
|
57518
|
+
phase: "acceptance-setup",
|
|
57519
|
+
passed: false,
|
|
57520
|
+
durationMs: Date.now() - phaseStartTime
|
|
57307
57521
|
});
|
|
57308
|
-
|
|
57309
|
-
if (exitCode !== 0) {
|
|
57310
|
-
redFailCount++;
|
|
57311
|
-
}
|
|
57312
|
-
}
|
|
57313
|
-
if (redFailCount === 0) {
|
|
57314
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57315
|
-
return {
|
|
57316
|
-
action: "skip",
|
|
57317
|
-
reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
|
|
57318
|
-
};
|
|
57522
|
+
throw err;
|
|
57319
57523
|
}
|
|
57320
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
|
|
57321
|
-
return { action: "continue" };
|
|
57322
57524
|
}
|
|
57323
57525
|
};
|
|
57324
57526
|
});
|
|
@@ -57464,79 +57666,6 @@ async function appendProgress(featureDir, storyId, status, message) {
|
|
|
57464
57666
|
}
|
|
57465
57667
|
var init_progress = () => {};
|
|
57466
57668
|
|
|
57467
|
-
// src/pipeline/event-bus.ts
|
|
57468
|
-
class PipelineEventBus {
|
|
57469
|
-
subscribers = new Map;
|
|
57470
|
-
_pending = new Set;
|
|
57471
|
-
on(eventType, subscriber) {
|
|
57472
|
-
const list = this.subscribers.get(eventType) ?? [];
|
|
57473
|
-
list.push(subscriber);
|
|
57474
|
-
this.subscribers.set(eventType, list);
|
|
57475
|
-
return () => {
|
|
57476
|
-
const current = this.subscribers.get(eventType) ?? [];
|
|
57477
|
-
this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
|
|
57478
|
-
};
|
|
57479
|
-
}
|
|
57480
|
-
onAll(subscriber) {
|
|
57481
|
-
const list = this.subscribers.get("*") ?? [];
|
|
57482
|
-
list.push(subscriber);
|
|
57483
|
-
this.subscribers.set("*", list);
|
|
57484
|
-
return () => {
|
|
57485
|
-
const current = this.subscribers.get("*") ?? [];
|
|
57486
|
-
this.subscribers.set("*", current.filter((s) => s !== subscriber));
|
|
57487
|
-
};
|
|
57488
|
-
}
|
|
57489
|
-
emit(event) {
|
|
57490
|
-
const logger = getLogger();
|
|
57491
|
-
const specific = this.subscribers.get(event.type) ?? [];
|
|
57492
|
-
const all = this.subscribers.get("*") ?? [];
|
|
57493
|
-
const targets = [...specific, ...all];
|
|
57494
|
-
for (const sub of targets) {
|
|
57495
|
-
try {
|
|
57496
|
-
const result = sub(event);
|
|
57497
|
-
if (result instanceof Promise) {
|
|
57498
|
-
const tracked = result.catch((err) => {
|
|
57499
|
-
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57500
|
-
});
|
|
57501
|
-
this._pending.add(tracked);
|
|
57502
|
-
tracked.finally(() => this._pending.delete(tracked));
|
|
57503
|
-
}
|
|
57504
|
-
} catch (err) {
|
|
57505
|
-
logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
|
|
57506
|
-
}
|
|
57507
|
-
}
|
|
57508
|
-
}
|
|
57509
|
-
async emitAsync(event) {
|
|
57510
|
-
const logger = getLogger();
|
|
57511
|
-
const specific = this.subscribers.get(event.type) ?? [];
|
|
57512
|
-
const all = this.subscribers.get("*") ?? [];
|
|
57513
|
-
const targets = [...specific, ...all];
|
|
57514
|
-
await Promise.allSettled(targets.map(async (sub) => {
|
|
57515
|
-
try {
|
|
57516
|
-
await sub(event);
|
|
57517
|
-
} catch (err) {
|
|
57518
|
-
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57519
|
-
}
|
|
57520
|
-
}));
|
|
57521
|
-
}
|
|
57522
|
-
async drain() {
|
|
57523
|
-
if (this._pending.size === 0)
|
|
57524
|
-
return;
|
|
57525
|
-
await Promise.allSettled([...this._pending]);
|
|
57526
|
-
}
|
|
57527
|
-
clear() {
|
|
57528
|
-
this.subscribers.clear();
|
|
57529
|
-
}
|
|
57530
|
-
subscriberCount(eventType) {
|
|
57531
|
-
return (this.subscribers.get(eventType) ?? []).length;
|
|
57532
|
-
}
|
|
57533
|
-
}
|
|
57534
|
-
var pipelineEventBus;
|
|
57535
|
-
var init_event_bus = __esm(() => {
|
|
57536
|
-
init_logger2();
|
|
57537
|
-
pipelineEventBus = new PipelineEventBus;
|
|
57538
|
-
});
|
|
57539
|
-
|
|
57540
57669
|
// src/pipeline/stages/completion.ts
|
|
57541
57670
|
async function getDiffText(workdir, baseRef) {
|
|
57542
57671
|
if (!baseRef)
|
|
@@ -58328,11 +58457,11 @@ var init_rollback = __esm(() => {
|
|
|
58328
58457
|
});
|
|
58329
58458
|
|
|
58330
58459
|
// src/utils/paths.ts
|
|
58331
|
-
import { join as join49, relative as
|
|
58460
|
+
import { join as join49, relative as relative14, sep as sep4 } from "path";
|
|
58332
58461
|
function packageDirRelative(projectDir, workdir) {
|
|
58333
58462
|
if (!projectDir || !workdir || workdir === projectDir)
|
|
58334
58463
|
return;
|
|
58335
|
-
const rel =
|
|
58464
|
+
const rel = relative14(projectDir, workdir);
|
|
58336
58465
|
if (rel === ".." || rel.startsWith(`..${sep4}`))
|
|
58337
58466
|
return;
|
|
58338
58467
|
return rel && rel !== "." ? rel : undefined;
|
|
@@ -58348,6 +58477,9 @@ var init_paths3 = __esm(() => {
|
|
|
58348
58477
|
});
|
|
58349
58478
|
|
|
58350
58479
|
// src/execution/non-blocking-fix.ts
|
|
58480
|
+
function actionableAdvisoryFindings(findings) {
|
|
58481
|
+
return findings.filter((f) => f.actionRequired !== false);
|
|
58482
|
+
}
|
|
58351
58483
|
function shouldRunNonBlockingFix(cfg, advisoryCount) {
|
|
58352
58484
|
return cfg?.enabled === true && advisoryCount > 0;
|
|
58353
58485
|
}
|
|
@@ -58420,9 +58552,16 @@ async function runNonBlockingFix(args, overrides = {}) {
|
|
|
58420
58552
|
exhausted = true;
|
|
58421
58553
|
}
|
|
58422
58554
|
if (!exhausted) {
|
|
58423
|
-
|
|
58555
|
+
const gateVerdict = args.keptTreeRegressed?.();
|
|
58556
|
+
if (gateVerdict?.regressed) {
|
|
58424
58557
|
logger?.info("non-blocking-fix", "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", {
|
|
58425
|
-
storyId: args.storyId
|
|
58558
|
+
storyId: args.storyId,
|
|
58559
|
+
regressedKeys: gateVerdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
|
|
58560
|
+
regressedKeyCount: gateVerdict.regressedKeys.length,
|
|
58561
|
+
baselineKeySize: gateVerdict.baselineKeySize,
|
|
58562
|
+
keyless: gateVerdict.keyless,
|
|
58563
|
+
memoExcludedKeyCount: gateVerdict.memoExcludedKeys.length,
|
|
58564
|
+
flakeTriageRan: false
|
|
58426
58565
|
});
|
|
58427
58566
|
return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
|
|
58428
58567
|
}
|
|
@@ -58466,7 +58605,7 @@ async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot,
|
|
|
58466
58605
|
});
|
|
58467
58606
|
return { ran: true, kept: false, restored: true };
|
|
58468
58607
|
}
|
|
58469
|
-
var REVIEW_PHASE_KINDS, _nonBlockingFixDeps, DEFAULT_DEPS;
|
|
58608
|
+
var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
|
|
58470
58609
|
var init_non_blocking_fix = __esm(() => {
|
|
58471
58610
|
init_logger2();
|
|
58472
58611
|
init_rollback();
|
|
@@ -58609,13 +58748,29 @@ function gateFailureKeys(gateOutput) {
|
|
|
58609
58748
|
}
|
|
58610
58749
|
return keys;
|
|
58611
58750
|
}
|
|
58612
|
-
function
|
|
58613
|
-
|
|
58614
|
-
|
|
58615
|
-
|
|
58616
|
-
|
|
58617
|
-
|
|
58618
|
-
|
|
58751
|
+
function describeGateRegression(input) {
|
|
58752
|
+
const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
|
|
58753
|
+
const notRegressed = {
|
|
58754
|
+
regressed: false,
|
|
58755
|
+
regressedKeys: [],
|
|
58756
|
+
memoExcludedKeys: [],
|
|
58757
|
+
baselineKeySize: baselineKeys.size,
|
|
58758
|
+
keyless: false
|
|
58759
|
+
};
|
|
58760
|
+
if (gateName === undefined || phasePassed(gateName, gateOutput, storyId))
|
|
58761
|
+
return notRegressed;
|
|
58762
|
+
const allKeys = gateFailureKeys(gateOutput);
|
|
58763
|
+
const memoExcludedKeys = quarantineMemo ? [...allKeys].filter((k) => quarantineMemo.has(k)) : [];
|
|
58764
|
+
const excluded = new Set(memoExcludedKeys);
|
|
58765
|
+
const regressedKeys = [...allKeys].filter((k) => !baselineKeys.has(k) && !excluded.has(k));
|
|
58766
|
+
const keyless = allKeys.size === 0 || allKeys.has(KEYLESS_GATE_FAILURE_KEY);
|
|
58767
|
+
return {
|
|
58768
|
+
regressed: regressedKeys.length > 0 || keyless,
|
|
58769
|
+
regressedKeys,
|
|
58770
|
+
memoExcludedKeys,
|
|
58771
|
+
baselineKeySize: baselineKeys.size,
|
|
58772
|
+
keyless
|
|
58773
|
+
};
|
|
58619
58774
|
}
|
|
58620
58775
|
function phasesToRevalidate(strategiesRun, allPhases) {
|
|
58621
58776
|
if (!strategiesRun || strategiesRun.length === 0)
|
|
@@ -58762,6 +58917,9 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
|
|
|
58762
58917
|
const data = { storyId, phase: opName, durationMs };
|
|
58763
58918
|
if (findingsCount !== undefined)
|
|
58764
58919
|
data.findingsCount = findingsCount;
|
|
58920
|
+
const identities = extractPhaseFindings(output).slice(0, MAX_LOGGED_FINDING_IDENTITIES).map((f) => `${f.file ?? ""}::${f.rule ?? ""}`);
|
|
58921
|
+
if (identities.length > 0)
|
|
58922
|
+
data.findingIdentities = identities;
|
|
58765
58923
|
if (status !== undefined)
|
|
58766
58924
|
data.status = status;
|
|
58767
58925
|
if (typeof r.failureCategory === "string")
|
|
@@ -58793,8 +58951,10 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
|
|
|
58793
58951
|
logger?.warn("story-orchestrator", message, data);
|
|
58794
58952
|
}
|
|
58795
58953
|
}
|
|
58954
|
+
var MAX_LOGGED_FINDING_IDENTITIES = 10;
|
|
58796
58955
|
var init_story_orchestrator_logging = __esm(() => {
|
|
58797
58956
|
init_logger2();
|
|
58957
|
+
init_phase_eval();
|
|
58798
58958
|
});
|
|
58799
58959
|
// src/verification/flake-baseline-diff.ts
|
|
58800
58960
|
async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
|
|
@@ -59067,10 +59227,13 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59067
59227
|
const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
|
|
59068
59228
|
let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
|
|
59069
59229
|
dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
|
|
59230
|
+
let advIterationBefore = 0;
|
|
59070
59231
|
if (opName === "adversarial-review" && ctx.storyId) {
|
|
59232
|
+
const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
|
|
59233
|
+
advIterationBefore = priorIterations.length;
|
|
59071
59234
|
dispatchInput = {
|
|
59072
59235
|
...dispatchInput,
|
|
59073
|
-
priorAdversarialIterations:
|
|
59236
|
+
priorAdversarialIterations: priorIterations
|
|
59074
59237
|
};
|
|
59075
59238
|
}
|
|
59076
59239
|
if (isTddPhase) {
|
|
@@ -59087,6 +59250,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59087
59250
|
}
|
|
59088
59251
|
const phaseStartedAt = Date.now();
|
|
59089
59252
|
const scope = ctx.runtime.costAggregator.openScope();
|
|
59253
|
+
let outcome = "passed";
|
|
59090
59254
|
try {
|
|
59091
59255
|
const output = await _storyOrchestratorDeps.callOp({ ...ctx, scopeId: scope.scopeId }, slot.op, dispatchInput);
|
|
59092
59256
|
phaseOutputs[opName] = output;
|
|
@@ -59100,6 +59264,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59100
59264
|
}
|
|
59101
59265
|
logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
|
|
59102
59266
|
logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
|
|
59267
|
+
outcome = derivePhaseOutcome(output);
|
|
59103
59268
|
if (isTddPhase) {
|
|
59104
59269
|
const durationMs = Date.now() - phaseStartedAt;
|
|
59105
59270
|
logger?.info("tdd", `Session complete: ${opName}`, {
|
|
@@ -59129,11 +59294,95 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59129
59294
|
}
|
|
59130
59295
|
}
|
|
59131
59296
|
return output;
|
|
59297
|
+
} catch (err) {
|
|
59298
|
+
outcome = "error";
|
|
59299
|
+
throw err;
|
|
59132
59300
|
} finally {
|
|
59133
|
-
|
|
59301
|
+
const snapshot = scope.snapshot();
|
|
59302
|
+
phaseCosts[opName] = (phaseCosts[opName] ?? 0) + snapshot.totalCostUsd;
|
|
59134
59303
|
scope.close();
|
|
59304
|
+
if (ctx.storyId) {
|
|
59305
|
+
const phaseDetails = buildPhaseDetails(opName, phaseOutputs[opName], isThreeSession, ctx.packageView.config, advIterationBefore, ctx.fixStrategy);
|
|
59306
|
+
const event = {
|
|
59307
|
+
type: "story:phase:completed",
|
|
59308
|
+
storyId: ctx.storyId,
|
|
59309
|
+
phase: opName,
|
|
59310
|
+
outcome,
|
|
59311
|
+
durationMs: Date.now() - phaseStartedAt,
|
|
59312
|
+
costUsd: snapshot.totalCostUsd,
|
|
59313
|
+
...ctx.phaseTelemetry ? {
|
|
59314
|
+
tier: ctx.phaseTelemetry.tier,
|
|
59315
|
+
testStrategy: ctx.phaseTelemetry.testStrategy,
|
|
59316
|
+
sessionModel: ctx.phaseTelemetry.sessionModel
|
|
59317
|
+
} : {},
|
|
59318
|
+
...phaseDetails !== undefined ? { details: phaseDetails } : {}
|
|
59319
|
+
};
|
|
59320
|
+
pipelineEventBus.emit(event);
|
|
59321
|
+
}
|
|
59135
59322
|
}
|
|
59136
59323
|
}
|
|
59324
|
+
function buildPhaseDetails(opName, output, isThreeSession, config2, advIterationBefore, fixStrategy) {
|
|
59325
|
+
if (fixStrategy) {
|
|
59326
|
+
return { kind: "fix", strategy: fixStrategy.name, findingsBefore: fixStrategy.findingsBefore };
|
|
59327
|
+
}
|
|
59328
|
+
if (output === null || typeof output !== "object")
|
|
59329
|
+
return;
|
|
59330
|
+
if (opName === "adversarial-review") {
|
|
59331
|
+
const adv = output;
|
|
59332
|
+
const findings = adv?.normalizedFindings ?? [];
|
|
59333
|
+
const threshold = adv?.blockingThreshold ?? "error";
|
|
59334
|
+
const bySeverity = Object.fromEntries(ALL_FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]));
|
|
59335
|
+
const blockingCount = findings.filter((f) => isBlockingSeverity(f.severity, threshold)).length;
|
|
59336
|
+
const advisoryCount = findings.length - blockingCount;
|
|
59337
|
+
return {
|
|
59338
|
+
kind: "review",
|
|
59339
|
+
reviewer: "adversarial",
|
|
59340
|
+
iteration: advIterationBefore,
|
|
59341
|
+
bySeverity,
|
|
59342
|
+
blockingCount,
|
|
59343
|
+
advisoryCount,
|
|
59344
|
+
...config2.reporters?.otel?.detail === "verbose" ? {
|
|
59345
|
+
items: findings.map((f) => ({
|
|
59346
|
+
message: f.message,
|
|
59347
|
+
severity: f.severity,
|
|
59348
|
+
...f.rule ? { rule: f.rule } : {},
|
|
59349
|
+
...f.file ? { file: f.file } : {}
|
|
59350
|
+
}))
|
|
59351
|
+
} : {}
|
|
59352
|
+
};
|
|
59353
|
+
}
|
|
59354
|
+
if (opName === "implementer") {
|
|
59355
|
+
const impl = output;
|
|
59356
|
+
const filesChanged = (impl?.filesChanged ?? []).length;
|
|
59357
|
+
if (isThreeSession) {
|
|
59358
|
+
return { kind: "authoring", role: "implementer", filesChanged, isolationPassed: impl?.isolation?.passed };
|
|
59359
|
+
}
|
|
59360
|
+
return { kind: "authoring", role: "implementer", filesChanged };
|
|
59361
|
+
}
|
|
59362
|
+
if (opName === "test-writer") {
|
|
59363
|
+
const tw = output;
|
|
59364
|
+
return { kind: "authoring", role: "test-writer", filesChanged: (tw?.filesChanged ?? []).length };
|
|
59365
|
+
}
|
|
59366
|
+
if (opName === "full-suite-gate") {
|
|
59367
|
+
const gate = output;
|
|
59368
|
+
return { kind: "gate", gate: "full-suite", failureCount: gate?.failureCount ?? 0 };
|
|
59369
|
+
}
|
|
59370
|
+
if (opName === "verifier" || opName === "verify-scoped") {
|
|
59371
|
+
const verdict = output;
|
|
59372
|
+
return { kind: "verdict", role: opName, passed: verdict.passed ?? false, failureCount: verdict.failureCount ?? 0 };
|
|
59373
|
+
}
|
|
59374
|
+
return;
|
|
59375
|
+
}
|
|
59376
|
+
function derivePhaseOutcome(output) {
|
|
59377
|
+
const built = buildPhaseOutcomeLogData(undefined, "", output, 0);
|
|
59378
|
+
if (!built)
|
|
59379
|
+
return "passed";
|
|
59380
|
+
if (built.success)
|
|
59381
|
+
return "passed";
|
|
59382
|
+
if (built.data.status === "skipped")
|
|
59383
|
+
return "skipped";
|
|
59384
|
+
return "failed";
|
|
59385
|
+
}
|
|
59137
59386
|
function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
|
|
59138
59387
|
if (!enabled)
|
|
59139
59388
|
return strategies;
|
|
@@ -59157,7 +59406,7 @@ function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
|
|
|
59157
59406
|
}
|
|
59158
59407
|
}));
|
|
59159
59408
|
}
|
|
59160
|
-
var _storyOrchestratorDeps;
|
|
59409
|
+
var _storyOrchestratorDeps, ALL_FINDING_SEVERITIES;
|
|
59161
59410
|
var init_run_phase = __esm(() => {
|
|
59162
59411
|
init_findings();
|
|
59163
59412
|
init_logger2();
|
|
@@ -59187,6 +59436,14 @@ var init_run_phase = __esm(() => {
|
|
|
59187
59436
|
},
|
|
59188
59437
|
loadCheckpoints: async (_featureDir) => new Map
|
|
59189
59438
|
};
|
|
59439
|
+
ALL_FINDING_SEVERITIES = [
|
|
59440
|
+
"critical",
|
|
59441
|
+
"error",
|
|
59442
|
+
"warning",
|
|
59443
|
+
"info",
|
|
59444
|
+
"low",
|
|
59445
|
+
"unverifiable"
|
|
59446
|
+
];
|
|
59190
59447
|
});
|
|
59191
59448
|
|
|
59192
59449
|
// src/execution/story-orchestrator/rectification.ts
|
|
@@ -59410,6 +59667,15 @@ class ExecutionPlan {
|
|
|
59410
59667
|
this.state = state;
|
|
59411
59668
|
this.isThreeSession = isThreeSession;
|
|
59412
59669
|
}
|
|
59670
|
+
describeGateRegressionNow(phaseOutputs, gateName, baselineKeys) {
|
|
59671
|
+
return describeGateRegression({
|
|
59672
|
+
gateOutput: gateName === undefined ? undefined : phaseOutputs[gateName],
|
|
59673
|
+
baselineKeys,
|
|
59674
|
+
gateName,
|
|
59675
|
+
storyId: this.ctx.storyId,
|
|
59676
|
+
quarantineMemo: this.ctx.runtime.quarantineMemo
|
|
59677
|
+
});
|
|
59678
|
+
}
|
|
59413
59679
|
phaseNames() {
|
|
59414
59680
|
const names = collectOrderedPhases(this.state).map((p) => p.slot.op.name);
|
|
59415
59681
|
if (this.state.rectification) {
|
|
@@ -59546,7 +59812,7 @@ class ExecutionPlan {
|
|
|
59546
59812
|
const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
|
|
59547
59813
|
const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
|
|
59548
59814
|
const advisoryOut = phaseOutputs["adversarial-review"];
|
|
59549
|
-
const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
|
|
59815
|
+
const advisoryFindings = actionableAdvisoryFindings(advisoryOut?.advisoryFindings ?? []);
|
|
59550
59816
|
if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
|
|
59551
59817
|
await _storyOrchestratorDeps.runNonBlockingFix({
|
|
59552
59818
|
workdir: this.ctx.packageDir,
|
|
@@ -59563,7 +59829,7 @@ class ExecutionPlan {
|
|
|
59563
59829
|
maxAttempts,
|
|
59564
59830
|
postValidate: this.state.nonBlockingFixPostValidate
|
|
59565
59831
|
}),
|
|
59566
|
-
keptTreeRegressed: () =>
|
|
59832
|
+
keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
|
|
59567
59833
|
}, {
|
|
59568
59834
|
measureSourceDiff: createMeasureSourceDiff({
|
|
59569
59835
|
config: this.ctx.runtime.configLoader.current(),
|
|
@@ -59574,7 +59840,7 @@ class ExecutionPlan {
|
|
|
59574
59840
|
}
|
|
59575
59841
|
const verifierName = this.state.verifier?.slot.op.name;
|
|
59576
59842
|
const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
|
|
59577
|
-
const gateRegressedDuringRect =
|
|
59843
|
+
const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
|
|
59578
59844
|
const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
|
|
59579
59845
|
if (verifierExplicitlyPassed && gateRegressedDuringRect) {
|
|
59580
59846
|
logger?.warn("story-orchestrator", "Gate regressed during rectification after verifier passed \u2014 verifier verdict is stale, failing story", { storyId: this.ctx.storyId, packageDir: this.ctx.packageDir });
|
|
@@ -59794,8 +60060,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
59794
60060
|
if (inputs.adversarialReview) {
|
|
59795
60061
|
builder.addAdversarialReview(inputs.adversarialReview);
|
|
59796
60062
|
}
|
|
59797
|
-
const
|
|
59798
|
-
const
|
|
60063
|
+
const repoRoot = ctx.packageDir;
|
|
60064
|
+
const packageDir = join50(repoRoot, story.workdir ?? "");
|
|
60065
|
+
const resolvedTestPatterns = await resolveTestFilePatterns(config2, repoRoot, story.workdir);
|
|
59799
60066
|
if (shouldRunRectification(config2) && inputs.rectification) {
|
|
59800
60067
|
const sink = makeDeclarationSink();
|
|
59801
60068
|
const strategies = [];
|
|
@@ -59831,7 +60098,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
59831
60098
|
files: h.files,
|
|
59832
60099
|
reasonDetail: h.reasonDetail
|
|
59833
60100
|
}));
|
|
59834
|
-
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir
|
|
60101
|
+
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
|
|
60102
|
+
repoRoot
|
|
60103
|
+
});
|
|
59835
60104
|
sink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
|
|
59836
60105
|
const allDeclarations = [...sink.testEdits, ...valid];
|
|
59837
60106
|
sink.testEdits = [];
|
|
@@ -59890,7 +60159,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
59890
60159
|
files: h.files,
|
|
59891
60160
|
reasonDetail: h.reasonDetail
|
|
59892
60161
|
}));
|
|
59893
|
-
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir
|
|
60162
|
+
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
|
|
60163
|
+
repoRoot
|
|
60164
|
+
});
|
|
59894
60165
|
nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
|
|
59895
60166
|
const allDeclarations = [...nbSink.testEdits, ...valid];
|
|
59896
60167
|
nbSink.testEdits = [];
|
|
@@ -60829,7 +61100,12 @@ var init_execution = __esm(() => {
|
|
|
60829
61100
|
featureName: ctx.prd.feature,
|
|
60830
61101
|
story: ctx.story,
|
|
60831
61102
|
...ctx.featureDir ? { featureDir: ctx.featureDir } : {},
|
|
60832
|
-
...interactionBridge ? { interactionBridge } : {}
|
|
61103
|
+
...interactionBridge ? { interactionBridge } : {},
|
|
61104
|
+
phaseTelemetry: {
|
|
61105
|
+
testStrategy: ctx.routing.testStrategy,
|
|
61106
|
+
sessionModel: isThreeSessionStrategy(ctx.routing.testStrategy) ? "three-session" : "single-session",
|
|
61107
|
+
tier: effectiveTier
|
|
61108
|
+
}
|
|
60833
61109
|
};
|
|
60834
61110
|
let capturedTokenUsage;
|
|
60835
61111
|
let capturedResponse = "";
|
|
@@ -61592,6 +61868,196 @@ var init_stages = __esm(() => {
|
|
|
61592
61868
|
preRunPipeline = [acceptanceSetupStage];
|
|
61593
61869
|
});
|
|
61594
61870
|
|
|
61871
|
+
// src/pipeline/subscribers/reporters.ts
|
|
61872
|
+
async function fanOutReporters(reporters, hook, invoke) {
|
|
61873
|
+
const logger = getSafeLogger();
|
|
61874
|
+
for (const reporter of reporters) {
|
|
61875
|
+
try {
|
|
61876
|
+
await invoke(reporter);
|
|
61877
|
+
} catch (err) {
|
|
61878
|
+
try {
|
|
61879
|
+
logger?.warn("plugins", `Reporter '${reporter.name}' ${hook} failed`, { error: err });
|
|
61880
|
+
} catch {}
|
|
61881
|
+
}
|
|
61882
|
+
}
|
|
61883
|
+
}
|
|
61884
|
+
function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
61885
|
+
const logger = getSafeLogger();
|
|
61886
|
+
const safe = (name, fn) => {
|
|
61887
|
+
return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
|
|
61888
|
+
};
|
|
61889
|
+
const unsubs = [];
|
|
61890
|
+
const phaseStart = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseStart", (reporter) => reporter.onPhaseStart?.(event));
|
|
61891
|
+
const phaseComplete = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseComplete", (reporter) => reporter.onPhaseComplete?.(event));
|
|
61892
|
+
unsubs.push(bus.on("story:step", (ev) => phaseStart({
|
|
61893
|
+
runId,
|
|
61894
|
+
scope: "story",
|
|
61895
|
+
storyId: ev.storyId,
|
|
61896
|
+
phase: ev.step,
|
|
61897
|
+
startTime: new Date().toISOString()
|
|
61898
|
+
})), bus.on("story:phase:completed", (phaseEvent) => phaseComplete({
|
|
61899
|
+
runId,
|
|
61900
|
+
scope: "story",
|
|
61901
|
+
storyId: phaseEvent.storyId,
|
|
61902
|
+
phase: phaseEvent.phase,
|
|
61903
|
+
outcome: phaseEvent.outcome,
|
|
61904
|
+
durationMs: phaseEvent.durationMs,
|
|
61905
|
+
costUsd: phaseEvent.costUsd,
|
|
61906
|
+
tier: phaseEvent.tier,
|
|
61907
|
+
testStrategy: phaseEvent.testStrategy,
|
|
61908
|
+
sessionModel: phaseEvent.sessionModel,
|
|
61909
|
+
details: phaseEvent.details
|
|
61910
|
+
})), bus.on("postrun:phase:started", (ev) => phaseStart({
|
|
61911
|
+
runId,
|
|
61912
|
+
scope: "run",
|
|
61913
|
+
phase: ev.phase,
|
|
61914
|
+
startTime: new Date().toISOString()
|
|
61915
|
+
})), bus.on("postrun:phase:completed", (phaseEvent) => phaseComplete({
|
|
61916
|
+
runId,
|
|
61917
|
+
scope: "run",
|
|
61918
|
+
phase: phaseEvent.phase,
|
|
61919
|
+
outcome: phaseEvent.passed ? "passed" : "failed",
|
|
61920
|
+
durationMs: phaseEvent.durationMs ?? 0,
|
|
61921
|
+
costUsd: phaseEvent.costUsd,
|
|
61922
|
+
details: phaseEvent.details
|
|
61923
|
+
})));
|
|
61924
|
+
unsubs.push(bus.on("run:started", (ev) => {
|
|
61925
|
+
return safe("onRunStart", async () => {
|
|
61926
|
+
const reporters = pluginRegistry.getReporters();
|
|
61927
|
+
for (const r of reporters) {
|
|
61928
|
+
if (r.onRunStart) {
|
|
61929
|
+
try {
|
|
61930
|
+
await r.onRunStart({
|
|
61931
|
+
runId,
|
|
61932
|
+
feature: ev.feature,
|
|
61933
|
+
totalStories: ev.totalStories,
|
|
61934
|
+
startTime: new Date(startTime).toISOString()
|
|
61935
|
+
});
|
|
61936
|
+
} catch (err) {
|
|
61937
|
+
logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
|
|
61938
|
+
}
|
|
61939
|
+
}
|
|
61940
|
+
}
|
|
61941
|
+
});
|
|
61942
|
+
}));
|
|
61943
|
+
unsubs.push(bus.on("story:completed", (ev) => {
|
|
61944
|
+
return safe("onStoryComplete(completed)", async () => {
|
|
61945
|
+
const reporters = pluginRegistry.getReporters();
|
|
61946
|
+
for (const r of reporters) {
|
|
61947
|
+
if (r.onStoryComplete) {
|
|
61948
|
+
try {
|
|
61949
|
+
await r.onStoryComplete({
|
|
61950
|
+
runId,
|
|
61951
|
+
storyId: ev.storyId,
|
|
61952
|
+
status: "completed",
|
|
61953
|
+
runElapsedMs: ev.runElapsedMs,
|
|
61954
|
+
cost: ev.cost ?? 0,
|
|
61955
|
+
tier: ev.modelTier ?? "balanced",
|
|
61956
|
+
testStrategy: ev.testStrategy ?? "test-after"
|
|
61957
|
+
});
|
|
61958
|
+
} catch (err) {
|
|
61959
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
61960
|
+
}
|
|
61961
|
+
}
|
|
61962
|
+
}
|
|
61963
|
+
});
|
|
61964
|
+
}));
|
|
61965
|
+
unsubs.push(bus.on("story:failed", (ev) => {
|
|
61966
|
+
return safe("onStoryComplete(failed)", async () => {
|
|
61967
|
+
const reporters = pluginRegistry.getReporters();
|
|
61968
|
+
for (const r of reporters) {
|
|
61969
|
+
if (r.onStoryComplete) {
|
|
61970
|
+
try {
|
|
61971
|
+
await r.onStoryComplete({
|
|
61972
|
+
runId,
|
|
61973
|
+
storyId: ev.storyId,
|
|
61974
|
+
status: "failed",
|
|
61975
|
+
runElapsedMs: Date.now() - startTime,
|
|
61976
|
+
cost: 0,
|
|
61977
|
+
tier: "balanced",
|
|
61978
|
+
testStrategy: "test-after"
|
|
61979
|
+
});
|
|
61980
|
+
} catch (err) {
|
|
61981
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
61982
|
+
}
|
|
61983
|
+
}
|
|
61984
|
+
}
|
|
61985
|
+
});
|
|
61986
|
+
}));
|
|
61987
|
+
unsubs.push(bus.on("story:paused", (ev) => {
|
|
61988
|
+
return safe("onStoryComplete(paused)", async () => {
|
|
61989
|
+
const reporters = pluginRegistry.getReporters();
|
|
61990
|
+
for (const r of reporters) {
|
|
61991
|
+
if (r.onStoryComplete) {
|
|
61992
|
+
try {
|
|
61993
|
+
await r.onStoryComplete({
|
|
61994
|
+
runId,
|
|
61995
|
+
storyId: ev.storyId,
|
|
61996
|
+
status: "paused",
|
|
61997
|
+
runElapsedMs: Date.now() - startTime,
|
|
61998
|
+
cost: 0,
|
|
61999
|
+
tier: "balanced",
|
|
62000
|
+
testStrategy: "test-after"
|
|
62001
|
+
});
|
|
62002
|
+
} catch (err) {
|
|
62003
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
62004
|
+
}
|
|
62005
|
+
}
|
|
62006
|
+
}
|
|
62007
|
+
});
|
|
62008
|
+
}));
|
|
62009
|
+
unsubs.push(bus.on("story:escalated", (ev) => {
|
|
62010
|
+
return safe("onEscalation", async () => {
|
|
62011
|
+
const reporters = pluginRegistry.getReporters();
|
|
62012
|
+
for (const r of reporters) {
|
|
62013
|
+
if (r.onEscalation) {
|
|
62014
|
+
try {
|
|
62015
|
+
await r.onEscalation({
|
|
62016
|
+
runId,
|
|
62017
|
+
storyId: ev.storyId,
|
|
62018
|
+
fromTier: ev.fromTier,
|
|
62019
|
+
toTier: ev.toTier
|
|
62020
|
+
});
|
|
62021
|
+
} catch (err) {
|
|
62022
|
+
logger?.warn("plugins", `Reporter '${r.name}' onEscalation failed`, { error: err });
|
|
62023
|
+
}
|
|
62024
|
+
}
|
|
62025
|
+
}
|
|
62026
|
+
});
|
|
62027
|
+
}));
|
|
62028
|
+
unsubs.push(bus.on("run:completed", (ev) => {
|
|
62029
|
+
return safe("onRunEnd", async () => {
|
|
62030
|
+
const reporters = pluginRegistry.getReporters();
|
|
62031
|
+
for (const r of reporters) {
|
|
62032
|
+
if (r.onRunEnd) {
|
|
62033
|
+
try {
|
|
62034
|
+
await r.onRunEnd({
|
|
62035
|
+
runId,
|
|
62036
|
+
totalDurationMs: Date.now() - startTime,
|
|
62037
|
+
totalCost: ev.totalCost ?? 0,
|
|
62038
|
+
storySummary: {
|
|
62039
|
+
completed: ev.passedStories,
|
|
62040
|
+
failed: ev.failedStories,
|
|
62041
|
+
skipped: ev.skippedStories,
|
|
62042
|
+
paused: ev.pausedStories
|
|
62043
|
+
}
|
|
62044
|
+
});
|
|
62045
|
+
} catch (err) {
|
|
62046
|
+
logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
|
|
62047
|
+
}
|
|
62048
|
+
}
|
|
62049
|
+
}
|
|
62050
|
+
});
|
|
62051
|
+
}));
|
|
62052
|
+
return () => {
|
|
62053
|
+
for (const u of unsubs)
|
|
62054
|
+
u();
|
|
62055
|
+
};
|
|
62056
|
+
}
|
|
62057
|
+
var init_reporters = __esm(() => {
|
|
62058
|
+
init_logger2();
|
|
62059
|
+
});
|
|
62060
|
+
|
|
61595
62061
|
// src/pipeline/index.ts
|
|
61596
62062
|
var init_pipeline = __esm(() => {
|
|
61597
62063
|
init_runner4();
|
|
@@ -61599,7 +62065,9 @@ var init_pipeline = __esm(() => {
|
|
|
61599
62065
|
init_stages();
|
|
61600
62066
|
init_queue_check();
|
|
61601
62067
|
init_execution_helpers();
|
|
62068
|
+
init_acceptance_setup();
|
|
61602
62069
|
init_event_bus();
|
|
62070
|
+
init_reporters();
|
|
61603
62071
|
});
|
|
61604
62072
|
|
|
61605
62073
|
// src/cli/prompts-shared.ts
|
|
@@ -64354,6 +64822,19 @@ var init_telegram2 = __esm(() => {
|
|
|
64354
64822
|
|
|
64355
64823
|
// src/plugins/builtin/nax-finish/index.ts
|
|
64356
64824
|
import * as path21 from "path";
|
|
64825
|
+
function logTail(stream) {
|
|
64826
|
+
if (stream.length <= LOG_TAIL_CHARS)
|
|
64827
|
+
return stream;
|
|
64828
|
+
return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
|
|
64829
|
+
${stream.slice(-LOG_TAIL_CHARS)}`;
|
|
64830
|
+
}
|
|
64831
|
+
function stderrTail(stderr) {
|
|
64832
|
+
const trimmed = stderr.trim();
|
|
64833
|
+
if (!trimmed)
|
|
64834
|
+
return "";
|
|
64835
|
+
const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
|
|
64836
|
+
return tail.replace(/\s+/g, " ");
|
|
64837
|
+
}
|
|
64357
64838
|
async function defaultRun2(cmd, opts) {
|
|
64358
64839
|
const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
|
|
64359
64840
|
let timedOut = false;
|
|
@@ -64427,7 +64908,7 @@ function buildFlowEnv(cfg) {
|
|
|
64427
64908
|
env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
|
|
64428
64909
|
return env2;
|
|
64429
64910
|
}
|
|
64430
|
-
var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
|
|
64911
|
+
var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, STDERR_TAIL_CHARS = 400, LOG_TAIL_CHARS = 20000, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
|
|
64431
64912
|
var init_nax_finish = __esm(() => {
|
|
64432
64913
|
init_config2();
|
|
64433
64914
|
init_telegram2();
|
|
@@ -64478,7 +64959,16 @@ var init_nax_finish = __esm(() => {
|
|
|
64478
64959
|
});
|
|
64479
64960
|
const result = await _naxFinishDeps.readResult(ctx.workdir);
|
|
64480
64961
|
if (!result) {
|
|
64481
|
-
|
|
64962
|
+
ctx.logger.warn("nax-finish flow produced no result file", {
|
|
64963
|
+
exitCode: res.exitCode,
|
|
64964
|
+
stdout: logTail(res.stdout),
|
|
64965
|
+
stderr: logTail(res.stderr)
|
|
64966
|
+
});
|
|
64967
|
+
const tail = stderrTail(res.stderr);
|
|
64968
|
+
return {
|
|
64969
|
+
success: false,
|
|
64970
|
+
message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
|
|
64971
|
+
};
|
|
64482
64972
|
}
|
|
64483
64973
|
if (result.status === "escalated" && escalateTelegram && creds) {
|
|
64484
64974
|
await _naxFinishDeps.notify(creds, `nax-finish escalated *${result.feature}*: ${result.escalationReason ?? ""}`);
|
|
@@ -64556,16 +65046,72 @@ var init_reporter_shared = __esm(() => {
|
|
|
64556
65046
|
init_post_json();
|
|
64557
65047
|
});
|
|
64558
65048
|
|
|
64559
|
-
// src/plugins/builtin/otel-reporter/
|
|
64560
|
-
function
|
|
64561
|
-
const
|
|
64562
|
-
|
|
64563
|
-
let
|
|
64564
|
-
|
|
64565
|
-
|
|
64566
|
-
|
|
65049
|
+
// src/plugins/builtin/otel-reporter/batch-queue.ts
|
|
65050
|
+
function createBatchQueue(opts) {
|
|
65051
|
+
const { maxBatchSize, flushIntervalMs, maxQueueSize, send } = opts;
|
|
65052
|
+
let queue = [];
|
|
65053
|
+
let dropCount = 0;
|
|
65054
|
+
let overflowing = false;
|
|
65055
|
+
let tornDown = false;
|
|
65056
|
+
let timer;
|
|
65057
|
+
const armTimer = () => {
|
|
65058
|
+
timer = setTimeout(() => {
|
|
65059
|
+
doFlush();
|
|
65060
|
+
if (!tornDown)
|
|
65061
|
+
armTimer();
|
|
65062
|
+
}, flushIntervalMs);
|
|
65063
|
+
};
|
|
65064
|
+
const sendWithRetry = async (batch) => {
|
|
65065
|
+
for (let attempt = 0;attempt < RETRY_ATTEMPTS; attempt++) {
|
|
65066
|
+
try {
|
|
65067
|
+
const ok = await send(batch);
|
|
65068
|
+
if (ok)
|
|
65069
|
+
return;
|
|
65070
|
+
} catch (err) {
|
|
65071
|
+
getSafeLogger()?.warn(STAGE, "Batch export threw", { error: err instanceof Error ? err.message : String(err) });
|
|
65072
|
+
}
|
|
65073
|
+
}
|
|
65074
|
+
};
|
|
65075
|
+
const doFlush = () => {
|
|
65076
|
+
if (tornDown || queue.length === 0)
|
|
65077
|
+
return Promise.resolve();
|
|
65078
|
+
const batch = queue;
|
|
65079
|
+
queue = [];
|
|
65080
|
+
sendWithRetry(batch);
|
|
65081
|
+
return Promise.resolve();
|
|
65082
|
+
};
|
|
65083
|
+
const enqueue = (item) => {
|
|
65084
|
+
queue.push(item);
|
|
65085
|
+
if (queue.length > maxQueueSize) {
|
|
65086
|
+
queue.shift();
|
|
65087
|
+
dropCount++;
|
|
65088
|
+
if (!overflowing) {
|
|
65089
|
+
overflowing = true;
|
|
65090
|
+
getSafeLogger()?.warn(STAGE, "Batch queue overflow \u2014 dropping oldest entries", { maxQueueSize });
|
|
65091
|
+
}
|
|
65092
|
+
} else {
|
|
65093
|
+
overflowing = false;
|
|
65094
|
+
}
|
|
65095
|
+
if (queue.length >= maxBatchSize) {
|
|
65096
|
+
doFlush();
|
|
65097
|
+
}
|
|
65098
|
+
};
|
|
65099
|
+
armTimer();
|
|
65100
|
+
return {
|
|
65101
|
+
enqueue,
|
|
65102
|
+
flushNow: () => doFlush(),
|
|
65103
|
+
teardown: () => {
|
|
65104
|
+
tornDown = true;
|
|
65105
|
+
if (timer !== undefined)
|
|
65106
|
+
clearTimeout(timer);
|
|
65107
|
+
},
|
|
65108
|
+
getMetrics: () => ({ size: queue.length, dropCount })
|
|
65109
|
+
};
|
|
64567
65110
|
}
|
|
64568
|
-
var
|
|
65111
|
+
var STAGE = "otel-batch-queue", RETRY_ATTEMPTS = 2;
|
|
65112
|
+
var init_batch_queue = __esm(() => {
|
|
65113
|
+
init_logger2();
|
|
65114
|
+
});
|
|
64569
65115
|
|
|
64570
65116
|
// src/plugins/builtin/otel-reporter/otlp.ts
|
|
64571
65117
|
function attr(key, value) {
|
|
@@ -64574,10 +65120,27 @@ function attr(key, value) {
|
|
|
64574
65120
|
function msToUnixNano(ms) {
|
|
64575
65121
|
return (BigInt(Math.round(ms)) * 1000000n).toString();
|
|
64576
65122
|
}
|
|
65123
|
+
function buildHistogramPoint(values, bounds, attributes, timeUnixNano) {
|
|
65124
|
+
const bucketCounts = new Array(bounds.length + 1).fill(0);
|
|
65125
|
+
let sum = 0;
|
|
65126
|
+
for (const value of values) {
|
|
65127
|
+
sum += value;
|
|
65128
|
+
const bucketIndex = bounds.findIndex((bound) => value <= bound);
|
|
65129
|
+
bucketCounts[bucketIndex === -1 ? bounds.length : bucketIndex]++;
|
|
65130
|
+
}
|
|
65131
|
+
return { attributes, timeUnixNano, count: values.length, sum, bucketCounts, explicitBounds: bounds };
|
|
65132
|
+
}
|
|
65133
|
+
function buildCounterPoint(count, attributes, timeUnixNano) {
|
|
65134
|
+
return { attributes, timeUnixNano, asInt: String(count) };
|
|
65135
|
+
}
|
|
65136
|
+
function buildResourceAttributes(serviceName, runId) {
|
|
65137
|
+
return [attr("service.name", serviceName), attr("nax.run_id", runId)];
|
|
65138
|
+
}
|
|
64577
65139
|
function buildTracesPayload(p) {
|
|
64578
65140
|
const span = {
|
|
64579
65141
|
traceId: p.traceId,
|
|
64580
65142
|
spanId: p.spanId,
|
|
65143
|
+
...p.parentSpanId ? { parentSpanId: p.parentSpanId } : {},
|
|
64581
65144
|
name: "nax.run",
|
|
64582
65145
|
kind: 1,
|
|
64583
65146
|
startTimeUnixNano: p.startUnixNano,
|
|
@@ -64598,7 +65161,7 @@ function buildTracesPayload(p) {
|
|
|
64598
65161
|
resourceSpans: [
|
|
64599
65162
|
{
|
|
64600
65163
|
resource: { attributes: [attr("service.name", p.serviceName)] },
|
|
64601
|
-
scopeSpans: [{ scope: { name: "nax" }, spans: [span] }]
|
|
65164
|
+
scopeSpans: [{ scope: { name: "nax" }, spans: [span, ...p.extraSpans ?? []] }]
|
|
64602
65165
|
}
|
|
64603
65166
|
]
|
|
64604
65167
|
};
|
|
@@ -64636,16 +65199,356 @@ function buildMetricsPayload(p) {
|
|
|
64636
65199
|
};
|
|
64637
65200
|
}
|
|
64638
65201
|
|
|
65202
|
+
// src/plugins/builtin/otel-reporter/heartbeat.ts
|
|
65203
|
+
function startHeartbeat(opts) {
|
|
65204
|
+
const { intervalMs, getSnapshot, onTick } = opts;
|
|
65205
|
+
if (intervalMs <= 0)
|
|
65206
|
+
return { stop() {} };
|
|
65207
|
+
let stopped = false;
|
|
65208
|
+
let timer;
|
|
65209
|
+
const armTimer = () => {
|
|
65210
|
+
timer = setTimeout(() => {
|
|
65211
|
+
try {
|
|
65212
|
+
Promise.resolve(onTick(getSnapshot())).catch((err) => getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
|
|
65213
|
+
error: err instanceof Error ? err.message : String(err)
|
|
65214
|
+
}));
|
|
65215
|
+
} catch (err) {
|
|
65216
|
+
getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
|
|
65217
|
+
error: err instanceof Error ? err.message : String(err)
|
|
65218
|
+
});
|
|
65219
|
+
}
|
|
65220
|
+
if (!stopped)
|
|
65221
|
+
armTimer();
|
|
65222
|
+
}, intervalMs);
|
|
65223
|
+
};
|
|
65224
|
+
armTimer();
|
|
65225
|
+
return {
|
|
65226
|
+
stop() {
|
|
65227
|
+
stopped = true;
|
|
65228
|
+
if (timer !== undefined)
|
|
65229
|
+
clearTimeout(timer);
|
|
65230
|
+
}
|
|
65231
|
+
};
|
|
65232
|
+
}
|
|
65233
|
+
function heartbeatAttributes(a) {
|
|
65234
|
+
return [
|
|
65235
|
+
attr("run_id", a.runId),
|
|
65236
|
+
attr("feature", a.feature),
|
|
65237
|
+
attr("project", a.project),
|
|
65238
|
+
attr("story_id", a.storyId),
|
|
65239
|
+
attr("phase", a.phase),
|
|
65240
|
+
attr("tier", a.tier),
|
|
65241
|
+
attr("test_strategy", a.testStrategy)
|
|
65242
|
+
];
|
|
65243
|
+
}
|
|
65244
|
+
function buildHeartbeatMetricsPayload(p) {
|
|
65245
|
+
const attributes = heartbeatAttributes(p.snapshot.attributes);
|
|
65246
|
+
const gauge = (name, value) => ({
|
|
65247
|
+
name,
|
|
65248
|
+
gauge: { dataPoints: [{ asDouble: value, timeUnixNano: p.timeUnixNano, attributes }] }
|
|
65249
|
+
});
|
|
65250
|
+
return {
|
|
65251
|
+
resourceMetrics: [
|
|
65252
|
+
{
|
|
65253
|
+
resource: { attributes: [attr("service.name", p.serviceName)] },
|
|
65254
|
+
scopeMetrics: [
|
|
65255
|
+
{
|
|
65256
|
+
scope: { name: "nax" },
|
|
65257
|
+
metrics: [
|
|
65258
|
+
gauge("nax.run.active", 1),
|
|
65259
|
+
gauge("nax.run.phase_elapsed_ms", p.snapshot.phaseElapsedMs),
|
|
65260
|
+
gauge("nax.run.cost_usd", p.snapshot.costUsd)
|
|
65261
|
+
]
|
|
65262
|
+
}
|
|
65263
|
+
]
|
|
65264
|
+
}
|
|
65265
|
+
]
|
|
65266
|
+
};
|
|
65267
|
+
}
|
|
65268
|
+
var STAGE2 = "otel-reporter-heartbeat";
|
|
65269
|
+
var init_heartbeat = __esm(() => {
|
|
65270
|
+
init_logger2();
|
|
65271
|
+
});
|
|
65272
|
+
|
|
65273
|
+
// src/plugins/builtin/otel-reporter/ids.ts
|
|
65274
|
+
function randomHex(bytes) {
|
|
65275
|
+
const arr = new Uint8Array(bytes);
|
|
65276
|
+
crypto.getRandomValues(arr);
|
|
65277
|
+
let out = "";
|
|
65278
|
+
for (const b of arr)
|
|
65279
|
+
out += b.toString(16).padStart(2, "0");
|
|
65280
|
+
return out;
|
|
65281
|
+
}
|
|
65282
|
+
var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
|
|
65283
|
+
|
|
65284
|
+
// src/plugins/builtin/otel-reporter/span-tree.ts
|
|
65285
|
+
function createSpanTree(traceId, runSpanId) {
|
|
65286
|
+
const storySpanIds = new Map;
|
|
65287
|
+
function storySpanId(storyId) {
|
|
65288
|
+
let spanId = storySpanIds.get(storyId);
|
|
65289
|
+
if (!spanId) {
|
|
65290
|
+
spanId = newSpanId();
|
|
65291
|
+
storySpanIds.set(storyId, spanId);
|
|
65292
|
+
}
|
|
65293
|
+
return spanId;
|
|
65294
|
+
}
|
|
65295
|
+
function buildStorySpan(storyId, startUnixNano, endUnixNano) {
|
|
65296
|
+
return {
|
|
65297
|
+
traceId,
|
|
65298
|
+
spanId: storySpanId(storyId),
|
|
65299
|
+
parentSpanId: runSpanId,
|
|
65300
|
+
name: "nax.story",
|
|
65301
|
+
startTimeUnixNano: startUnixNano,
|
|
65302
|
+
endTimeUnixNano: endUnixNano,
|
|
65303
|
+
attributes: [attr("nax.story_id", storyId)]
|
|
65304
|
+
};
|
|
65305
|
+
}
|
|
65306
|
+
function buildPhaseSpan({ event, traceId: spanTraceId, startUnixNano, endUnixNano }) {
|
|
65307
|
+
const parentSpanId = event.scope === "story" && event.storyId !== undefined ? storySpanId(event.storyId) : runSpanId;
|
|
65308
|
+
const attributes = [attr("phase", event.phase), attr("outcome", event.outcome)];
|
|
65309
|
+
if (event.testStrategy)
|
|
65310
|
+
attributes.push(attr("nax.test_strategy", event.testStrategy));
|
|
65311
|
+
return {
|
|
65312
|
+
traceId: spanTraceId,
|
|
65313
|
+
spanId: newSpanId(),
|
|
65314
|
+
parentSpanId,
|
|
65315
|
+
name: "nax.phase",
|
|
65316
|
+
startTimeUnixNano: startUnixNano,
|
|
65317
|
+
endTimeUnixNano: endUnixNano,
|
|
65318
|
+
attributes
|
|
65319
|
+
};
|
|
65320
|
+
}
|
|
65321
|
+
return { traceId, runSpanId, storySpanId, buildStorySpan, buildPhaseSpan };
|
|
65322
|
+
}
|
|
65323
|
+
function counterKey(attributes) {
|
|
65324
|
+
return attributes.map((a) => `${a.key}=${a.value.stringValue ?? a.value.doubleValue}`).join("|");
|
|
65325
|
+
}
|
|
65326
|
+
function bumpCounter(groups, attributes, count) {
|
|
65327
|
+
const key = counterKey(attributes);
|
|
65328
|
+
const existing = groups.get(key);
|
|
65329
|
+
if (existing) {
|
|
65330
|
+
existing.count += count;
|
|
65331
|
+
} else {
|
|
65332
|
+
groups.set(key, { attributes, count });
|
|
65333
|
+
}
|
|
65334
|
+
}
|
|
65335
|
+
function createPhaseMetricsAggregator() {
|
|
65336
|
+
const phaseGroups = new Map;
|
|
65337
|
+
const reviewFindings = new Map;
|
|
65338
|
+
const fixIterations = new Map;
|
|
65339
|
+
const escalations = new Map;
|
|
65340
|
+
function recordPhase(event) {
|
|
65341
|
+
const attributes = [
|
|
65342
|
+
attr("phase", event.phase),
|
|
65343
|
+
attr("outcome", event.outcome),
|
|
65344
|
+
attr("tier", event.tier ?? "unknown"),
|
|
65345
|
+
attr("test_strategy", event.testStrategy ?? "unknown"),
|
|
65346
|
+
attr("session_model", event.sessionModel ?? "unknown")
|
|
65347
|
+
];
|
|
65348
|
+
const key = counterKey(attributes);
|
|
65349
|
+
let group = phaseGroups.get(key);
|
|
65350
|
+
if (!group) {
|
|
65351
|
+
group = { attributes, durations: [], costs: [] };
|
|
65352
|
+
phaseGroups.set(key, group);
|
|
65353
|
+
}
|
|
65354
|
+
group.durations.push(event.durationMs);
|
|
65355
|
+
if (event.costUsd !== undefined)
|
|
65356
|
+
group.costs.push(event.costUsd);
|
|
65357
|
+
}
|
|
65358
|
+
function recordReviewFindings(phase, severity2, count) {
|
|
65359
|
+
bumpCounter(reviewFindings, [attr("phase", phase), attr("severity", severity2)], count);
|
|
65360
|
+
}
|
|
65361
|
+
function recordFixIterations(phase, strategy, count) {
|
|
65362
|
+
bumpCounter(fixIterations, [attr("phase", phase), attr("strategy", strategy)], count);
|
|
65363
|
+
}
|
|
65364
|
+
function recordEscalation(toTier, count) {
|
|
65365
|
+
bumpCounter(escalations, [attr("to_tier", toTier)], count);
|
|
65366
|
+
}
|
|
65367
|
+
function buildMetricsPayload2(serviceName, runId, timeUnixNano) {
|
|
65368
|
+
const groups = [...phaseGroups.values()];
|
|
65369
|
+
const counterMetric = (name, source) => ({
|
|
65370
|
+
name,
|
|
65371
|
+
sum: {
|
|
65372
|
+
aggregationTemporality: 2,
|
|
65373
|
+
isMonotonic: true,
|
|
65374
|
+
dataPoints: [...source.values()].map((g) => buildCounterPoint(g.count, g.attributes, timeUnixNano))
|
|
65375
|
+
}
|
|
65376
|
+
});
|
|
65377
|
+
const metrics = [];
|
|
65378
|
+
if (groups.length > 0) {
|
|
65379
|
+
metrics.push({
|
|
65380
|
+
name: "nax.phase.duration",
|
|
65381
|
+
histogram: {
|
|
65382
|
+
aggregationTemporality: 2,
|
|
65383
|
+
dataPoints: groups.map((g) => buildHistogramPoint(g.durations, PHASE_DURATION_BOUNDS, g.attributes, timeUnixNano))
|
|
65384
|
+
}
|
|
65385
|
+
});
|
|
65386
|
+
metrics.push({
|
|
65387
|
+
name: "nax.phase.cost_usd",
|
|
65388
|
+
histogram: {
|
|
65389
|
+
aggregationTemporality: 2,
|
|
65390
|
+
dataPoints: groups.map((g) => buildHistogramPoint(g.costs, PHASE_COST_BOUNDS, g.attributes, timeUnixNano))
|
|
65391
|
+
}
|
|
65392
|
+
});
|
|
65393
|
+
}
|
|
65394
|
+
if (reviewFindings.size > 0)
|
|
65395
|
+
metrics.push(counterMetric("nax.review.findings", reviewFindings));
|
|
65396
|
+
if (fixIterations.size > 0)
|
|
65397
|
+
metrics.push(counterMetric("nax.fix.iterations", fixIterations));
|
|
65398
|
+
if (escalations.size > 0)
|
|
65399
|
+
metrics.push(counterMetric("nax.escalations", escalations));
|
|
65400
|
+
return {
|
|
65401
|
+
resourceMetrics: [
|
|
65402
|
+
{
|
|
65403
|
+
resource: { attributes: buildResourceAttributes(serviceName, runId) },
|
|
65404
|
+
scopeMetrics: [{ scope: { name: "nax" }, metrics }]
|
|
65405
|
+
}
|
|
65406
|
+
]
|
|
65407
|
+
};
|
|
65408
|
+
}
|
|
65409
|
+
return { recordPhase, recordReviewFindings, recordFixIterations, recordEscalation, buildMetricsPayload: buildMetricsPayload2 };
|
|
65410
|
+
}
|
|
65411
|
+
var PHASE_DURATION_BOUNDS, PHASE_COST_BOUNDS;
|
|
65412
|
+
var init_span_tree = __esm(() => {
|
|
65413
|
+
PHASE_DURATION_BOUNDS = [100, 500, 1000, 5000, 15000, 60000, 300000, 900000];
|
|
65414
|
+
PHASE_COST_BOUNDS = [0.001, 0.01, 0.05, 0.1, 0.5, 1, 5];
|
|
65415
|
+
});
|
|
65416
|
+
|
|
65417
|
+
// src/plugins/builtin/otel-reporter/traceparent.ts
|
|
65418
|
+
function parseTraceparent(value) {
|
|
65419
|
+
if (!value)
|
|
65420
|
+
return null;
|
|
65421
|
+
const match = TRACEPARENT_PATTERN.exec(value);
|
|
65422
|
+
if (!match)
|
|
65423
|
+
return null;
|
|
65424
|
+
const [, traceId, spanId] = match;
|
|
65425
|
+
if (!traceId || !spanId || ALL_ZERO.test(traceId))
|
|
65426
|
+
return null;
|
|
65427
|
+
return { traceId, spanId };
|
|
65428
|
+
}
|
|
65429
|
+
var TRACEPARENT_PATTERN, ALL_ZERO;
|
|
65430
|
+
var init_traceparent = __esm(() => {
|
|
65431
|
+
TRACEPARENT_PATTERN = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
|
|
65432
|
+
ALL_ZERO = /^0+$/;
|
|
65433
|
+
});
|
|
65434
|
+
|
|
64639
65435
|
// src/plugins/builtin/otel-reporter/index.ts
|
|
65436
|
+
function rootSpanIdentity() {
|
|
65437
|
+
const adopted = parseTraceparent(process.env.TRACEPARENT);
|
|
65438
|
+
if (!adopted)
|
|
65439
|
+
return { traceId: newTraceId(), spanId: newSpanId() };
|
|
65440
|
+
return { traceId: adopted.traceId, spanId: newSpanId(), parentSpanId: adopted.spanId };
|
|
65441
|
+
}
|
|
65442
|
+
function heartbeatSnapshotOf(runId, st) {
|
|
65443
|
+
const last = st.lastPhase;
|
|
65444
|
+
return {
|
|
65445
|
+
attributes: {
|
|
65446
|
+
runId,
|
|
65447
|
+
feature: st.feature,
|
|
65448
|
+
project: st.project,
|
|
65449
|
+
storyId: last?.storyId ?? "",
|
|
65450
|
+
phase: last?.phase ?? "",
|
|
65451
|
+
tier: last?.tier ?? "",
|
|
65452
|
+
testStrategy: last?.testStrategy ?? ""
|
|
65453
|
+
},
|
|
65454
|
+
phaseElapsedMs: last ? Date.now() - last.atMs : 0,
|
|
65455
|
+
costUsd: st.costUsd
|
|
65456
|
+
};
|
|
65457
|
+
}
|
|
65458
|
+
function recordDetailMetrics(metrics, phase, details) {
|
|
65459
|
+
if (typeof details !== "object" || details === null)
|
|
65460
|
+
return;
|
|
65461
|
+
const record2 = details;
|
|
65462
|
+
if (record2.kind === "review" && typeof record2.bySeverity === "object" && record2.bySeverity !== null) {
|
|
65463
|
+
for (const [severity2, count] of Object.entries(record2.bySeverity)) {
|
|
65464
|
+
if (count > 0)
|
|
65465
|
+
metrics.recordReviewFindings(phase, severity2, count);
|
|
65466
|
+
}
|
|
65467
|
+
} else if (record2.kind === "fix" && typeof record2.strategy === "string") {
|
|
65468
|
+
metrics.recordFixIterations(phase, record2.strategy, 1);
|
|
65469
|
+
}
|
|
65470
|
+
}
|
|
65471
|
+
function reviewSpanEvents(details, timeUnixNano, verbose) {
|
|
65472
|
+
if (!verbose)
|
|
65473
|
+
return [];
|
|
65474
|
+
if (typeof details !== "object" || details === null)
|
|
65475
|
+
return [];
|
|
65476
|
+
const record2 = details;
|
|
65477
|
+
if (record2.kind !== "review" || !Array.isArray(record2.items))
|
|
65478
|
+
return [];
|
|
65479
|
+
return record2.items.map((item) => {
|
|
65480
|
+
const finding = typeof item === "object" && item !== null ? item : {};
|
|
65481
|
+
const attributes = [attr("message", String(finding.message ?? ""))];
|
|
65482
|
+
if (typeof finding.file === "string")
|
|
65483
|
+
attributes.push(attr("file", finding.file));
|
|
65484
|
+
return { timeUnixNano, name: "review.finding", attributes };
|
|
65485
|
+
});
|
|
65486
|
+
}
|
|
64640
65487
|
function createOtelReporterPlugin(cfg, deps) {
|
|
64641
65488
|
const states = new Map;
|
|
64642
65489
|
const base = cfg.endpoint?.replace(/\/$/, "");
|
|
65490
|
+
let tornDown = false;
|
|
65491
|
+
const sendSpanBatch = async (batch) => {
|
|
65492
|
+
if (!base || batch.length === 0)
|
|
65493
|
+
return true;
|
|
65494
|
+
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
65495
|
+
if (missing.length > 0) {
|
|
65496
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
65497
|
+
return true;
|
|
65498
|
+
}
|
|
65499
|
+
const payload = {
|
|
65500
|
+
resourceSpans: [
|
|
65501
|
+
{
|
|
65502
|
+
resource: { attributes: [attr("service.name", cfg.serviceName)] },
|
|
65503
|
+
scopeSpans: [{ scope: { name: "nax" }, spans: batch }]
|
|
65504
|
+
}
|
|
65505
|
+
]
|
|
65506
|
+
};
|
|
65507
|
+
return postJson(`${base}/v1/traces`, payload, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
|
|
65508
|
+
};
|
|
65509
|
+
const makeSpanQueue = () => createBatchQueue({
|
|
65510
|
+
maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
|
|
65511
|
+
flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
65512
|
+
maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
65513
|
+
send: sendSpanBatch
|
|
65514
|
+
});
|
|
65515
|
+
const buildOrphanState = (startMs) => {
|
|
65516
|
+
const identity = rootSpanIdentity();
|
|
65517
|
+
return {
|
|
65518
|
+
...identity,
|
|
65519
|
+
startMs,
|
|
65520
|
+
feature: "",
|
|
65521
|
+
project: "",
|
|
65522
|
+
events: [],
|
|
65523
|
+
spanTree: createSpanTree(identity.traceId, identity.spanId),
|
|
65524
|
+
spanQueue: makeSpanQueue(),
|
|
65525
|
+
metrics: createPhaseMetricsAggregator(),
|
|
65526
|
+
storyBounds: new Map,
|
|
65527
|
+
costUsd: 0,
|
|
65528
|
+
heartbeat: { stop() {} }
|
|
65529
|
+
};
|
|
65530
|
+
};
|
|
65531
|
+
const exportHeartbeat = async (snapshot) => {
|
|
65532
|
+
if (!base)
|
|
65533
|
+
return;
|
|
65534
|
+
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
65535
|
+
if (missing.length > 0) {
|
|
65536
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
65537
|
+
return;
|
|
65538
|
+
}
|
|
65539
|
+
const metrics = buildHeartbeatMetricsPayload({
|
|
65540
|
+
serviceName: cfg.serviceName,
|
|
65541
|
+
timeUnixNano: msToUnixNano(Date.now()),
|
|
65542
|
+
snapshot
|
|
65543
|
+
});
|
|
65544
|
+
await postJson(`${base}/v1/metrics`, metrics, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
|
|
65545
|
+
};
|
|
64643
65546
|
const flush = async (st, endMs, e) => {
|
|
64644
65547
|
if (!base)
|
|
64645
65548
|
return;
|
|
64646
65549
|
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
64647
65550
|
if (missing.length > 0) {
|
|
64648
|
-
getSafeLogger()?.warn(
|
|
65551
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
64649
65552
|
return;
|
|
64650
65553
|
}
|
|
64651
65554
|
const startUnixNano = msToUnixNano(st.startMs);
|
|
@@ -64654,6 +65557,7 @@ function createOtelReporterPlugin(cfg, deps) {
|
|
|
64654
65557
|
serviceName: cfg.serviceName,
|
|
64655
65558
|
traceId: st.traceId,
|
|
64656
65559
|
spanId: st.spanId,
|
|
65560
|
+
parentSpanId: st.parentSpanId,
|
|
64657
65561
|
startUnixNano,
|
|
64658
65562
|
endUnixNano,
|
|
64659
65563
|
feature: st.feature,
|
|
@@ -64670,20 +65574,35 @@ function createOtelReporterPlugin(cfg, deps) {
|
|
|
64670
65574
|
totalCost: e.totalCost,
|
|
64671
65575
|
totalDurationMs: e.totalDurationMs
|
|
64672
65576
|
});
|
|
64673
|
-
const
|
|
65577
|
+
const aggMetrics = st.metrics.buildMetricsPayload(cfg.serviceName, e.runId, endUnixNano);
|
|
65578
|
+
metrics.resourceMetrics[0].scopeMetrics[0].metrics.push(...aggMetrics.resourceMetrics[0].scopeMetrics[0].metrics);
|
|
65579
|
+
const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps };
|
|
64674
65580
|
await postJson(`${base}/v1/traces`, traces, opts);
|
|
64675
65581
|
await postJson(`${base}/v1/metrics`, metrics, opts);
|
|
64676
65582
|
};
|
|
64677
65583
|
const reporter = {
|
|
64678
|
-
name:
|
|
65584
|
+
name: STAGE3,
|
|
64679
65585
|
async onRunStart(event) {
|
|
64680
|
-
|
|
64681
|
-
|
|
64682
|
-
|
|
65586
|
+
const identity = rootSpanIdentity();
|
|
65587
|
+
const runId = event.runId;
|
|
65588
|
+
const state = {
|
|
65589
|
+
...identity,
|
|
64683
65590
|
startMs: Date.parse(event.startTime),
|
|
64684
65591
|
feature: event.feature,
|
|
64685
|
-
|
|
64686
|
-
|
|
65592
|
+
project: event.project ?? "",
|
|
65593
|
+
events: [],
|
|
65594
|
+
spanTree: createSpanTree(identity.traceId, identity.spanId),
|
|
65595
|
+
spanQueue: makeSpanQueue(),
|
|
65596
|
+
metrics: createPhaseMetricsAggregator(),
|
|
65597
|
+
storyBounds: new Map,
|
|
65598
|
+
costUsd: 0,
|
|
65599
|
+
heartbeat: startHeartbeat({
|
|
65600
|
+
intervalMs: cfg.heartbeatIntervalMs ?? 0,
|
|
65601
|
+
getSnapshot: () => heartbeatSnapshotOf(runId, state),
|
|
65602
|
+
onTick: (snapshot) => exportHeartbeat(snapshot)
|
|
65603
|
+
})
|
|
65604
|
+
};
|
|
65605
|
+
states.set(runId, state);
|
|
64687
65606
|
},
|
|
64688
65607
|
async onStoryComplete(event) {
|
|
64689
65608
|
const st = states.get(event.runId);
|
|
@@ -64700,32 +65619,98 @@ function createOtelReporterPlugin(cfg, deps) {
|
|
|
64700
65619
|
attr("testStrategy", event.testStrategy)
|
|
64701
65620
|
]
|
|
64702
65621
|
});
|
|
65622
|
+
const bounds = st.storyBounds.get(event.storyId);
|
|
65623
|
+
if (bounds) {
|
|
65624
|
+
st.spanQueue.enqueue(st.spanTree.buildStorySpan(event.storyId, msToUnixNano(bounds.startMs), msToUnixNano(bounds.endMs)));
|
|
65625
|
+
st.storyBounds.delete(event.storyId);
|
|
65626
|
+
}
|
|
65627
|
+
},
|
|
65628
|
+
async onPhaseComplete(event) {
|
|
65629
|
+
const st = states.get(event.runId);
|
|
65630
|
+
if (!st)
|
|
65631
|
+
return;
|
|
65632
|
+
st.costUsd += event.costUsd ?? 0;
|
|
65633
|
+
st.lastPhase = {
|
|
65634
|
+
phase: event.phase,
|
|
65635
|
+
storyId: event.storyId ?? "",
|
|
65636
|
+
tier: event.tier ?? "",
|
|
65637
|
+
testStrategy: event.testStrategy ?? "",
|
|
65638
|
+
atMs: Date.now()
|
|
65639
|
+
};
|
|
65640
|
+
const endMs = Date.now();
|
|
65641
|
+
const startMs = endMs - event.durationMs;
|
|
65642
|
+
const endUnixNano = msToUnixNano(endMs);
|
|
65643
|
+
const span = st.spanTree.buildPhaseSpan({
|
|
65644
|
+
event,
|
|
65645
|
+
traceId: st.traceId,
|
|
65646
|
+
startUnixNano: msToUnixNano(startMs),
|
|
65647
|
+
endUnixNano
|
|
65648
|
+
});
|
|
65649
|
+
const events = reviewSpanEvents(event.details, endUnixNano, cfg.detail === "verbose");
|
|
65650
|
+
if (events.length > 0)
|
|
65651
|
+
span.events = events;
|
|
65652
|
+
st.spanQueue.enqueue(span);
|
|
65653
|
+
st.metrics.recordPhase(event);
|
|
65654
|
+
recordDetailMetrics(st.metrics, event.phase, event.details);
|
|
65655
|
+
if (event.scope === "story" && event.storyId !== undefined) {
|
|
65656
|
+
const bounds = st.storyBounds.get(event.storyId);
|
|
65657
|
+
st.storyBounds.set(event.storyId, {
|
|
65658
|
+
startMs: bounds ? Math.min(bounds.startMs, startMs) : startMs,
|
|
65659
|
+
endMs: bounds ? Math.max(bounds.endMs, endMs) : endMs
|
|
65660
|
+
});
|
|
65661
|
+
}
|
|
65662
|
+
},
|
|
65663
|
+
async onEscalation(event) {
|
|
65664
|
+
const st = states.get(event.runId);
|
|
65665
|
+
if (!st)
|
|
65666
|
+
return;
|
|
65667
|
+
st.metrics.recordEscalation(event.toTier, 1);
|
|
64703
65668
|
},
|
|
64704
65669
|
async onRunEnd(event) {
|
|
64705
65670
|
const existing = states.get(event.runId);
|
|
65671
|
+
existing?.heartbeat.stop();
|
|
64706
65672
|
const startMs = existing?.startMs ?? Date.now() - event.totalDurationMs;
|
|
64707
|
-
const st = existing ??
|
|
64708
|
-
traceId: newTraceId(),
|
|
64709
|
-
spanId: newSpanId(),
|
|
64710
|
-
startMs,
|
|
64711
|
-
feature: "",
|
|
64712
|
-
events: []
|
|
64713
|
-
};
|
|
65673
|
+
const st = existing ?? buildOrphanState(startMs);
|
|
64714
65674
|
states.delete(event.runId);
|
|
65675
|
+
await st.spanQueue.flushNow();
|
|
65676
|
+
st.spanQueue.teardown();
|
|
64715
65677
|
await flush(st, startMs + event.totalDurationMs, event);
|
|
64716
65678
|
}
|
|
64717
65679
|
};
|
|
64718
65680
|
return {
|
|
64719
|
-
name:
|
|
65681
|
+
name: STAGE3,
|
|
64720
65682
|
version: "1.0.0",
|
|
64721
65683
|
provides: ["reporter"],
|
|
65684
|
+
async teardown() {
|
|
65685
|
+
if (tornDown)
|
|
65686
|
+
return;
|
|
65687
|
+
tornDown = true;
|
|
65688
|
+
const entries = [...states.entries()];
|
|
65689
|
+
states.clear();
|
|
65690
|
+
for (const [runId, st] of entries) {
|
|
65691
|
+
st.heartbeat.stop();
|
|
65692
|
+
await st.spanQueue.flushNow();
|
|
65693
|
+
st.spanQueue.teardown();
|
|
65694
|
+
const endMs = Date.now();
|
|
65695
|
+
await flush(st, endMs, {
|
|
65696
|
+
runId,
|
|
65697
|
+
totalDurationMs: endMs - st.startMs,
|
|
65698
|
+
totalCost: st.costUsd,
|
|
65699
|
+
storySummary: { completed: 0, failed: 0, skipped: 0, paused: 0 }
|
|
65700
|
+
});
|
|
65701
|
+
}
|
|
65702
|
+
},
|
|
64722
65703
|
extensions: { reporter }
|
|
64723
65704
|
};
|
|
64724
65705
|
}
|
|
64725
|
-
var
|
|
65706
|
+
var STAGE3 = "otel-reporter", DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048;
|
|
64726
65707
|
var init_otel_reporter = __esm(() => {
|
|
64727
65708
|
init_logger2();
|
|
64728
65709
|
init_reporter_shared();
|
|
65710
|
+
init_batch_queue();
|
|
65711
|
+
init_heartbeat();
|
|
65712
|
+
init_span_tree();
|
|
65713
|
+
init_traceparent();
|
|
64729
65714
|
});
|
|
64730
65715
|
|
|
64731
65716
|
// src/plugins/builtin/webhook-reporter/index.ts
|
|
@@ -64736,25 +65721,27 @@ function createWebhookReporterPlugin(cfg, deps) {
|
|
|
64736
65721
|
return;
|
|
64737
65722
|
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
64738
65723
|
if (missing.length > 0) {
|
|
64739
|
-
getSafeLogger()?.warn(
|
|
65724
|
+
getSafeLogger()?.warn(STAGE4, "Skipping webhook \u2014 unresolved env vars", { missing });
|
|
64740
65725
|
return;
|
|
64741
65726
|
}
|
|
64742
|
-
await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage:
|
|
65727
|
+
await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE4, deps });
|
|
64743
65728
|
};
|
|
64744
65729
|
const reporter = {
|
|
64745
|
-
name:
|
|
65730
|
+
name: STAGE4,
|
|
64746
65731
|
onRunStart: (event) => emit("onRunStart", event),
|
|
64747
65732
|
onStoryComplete: (event) => emit("onStoryComplete", event),
|
|
64748
|
-
onRunEnd: (event) => emit("onRunEnd", event)
|
|
65733
|
+
onRunEnd: (event) => emit("onRunEnd", event),
|
|
65734
|
+
onPhaseStart: (event) => emit("onPhaseStart", event),
|
|
65735
|
+
onPhaseComplete: (event) => emit("onPhaseComplete", event)
|
|
64749
65736
|
};
|
|
64750
65737
|
return {
|
|
64751
|
-
name:
|
|
65738
|
+
name: STAGE4,
|
|
64752
65739
|
version: "1.0.0",
|
|
64753
65740
|
provides: ["reporter"],
|
|
64754
65741
|
extensions: { reporter }
|
|
64755
65742
|
};
|
|
64756
65743
|
}
|
|
64757
|
-
var
|
|
65744
|
+
var STAGE4 = "webhook-reporter";
|
|
64758
65745
|
var init_webhook_reporter = __esm(() => {
|
|
64759
65746
|
init_logger2();
|
|
64760
65747
|
init_reporter_shared();
|
|
@@ -65789,7 +66776,7 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
|
|
|
65789
66776
|
}
|
|
65790
66777
|
}
|
|
65791
66778
|
}
|
|
65792
|
-
function
|
|
66779
|
+
function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
|
|
65793
66780
|
const logger = _heartbeatDeps.getSafeLogger();
|
|
65794
66781
|
_heartbeatActive = true;
|
|
65795
66782
|
const gen = ++_heartbeatGen;
|
|
@@ -67383,28 +68370,47 @@ async function handleRunCompletion(options) {
|
|
|
67383
68370
|
const regressionMode = config2.execution.regressionGate?.mode;
|
|
67384
68371
|
if (options.skipRegression) {} else if ((regressionMode === "deferred" || regressionMode === "per-story") && config2.quality.commands.test) {
|
|
67385
68372
|
statusWriter.setPostRunPhase("regression", { status: "running" });
|
|
68373
|
+
const regressionStartTime = Date.now();
|
|
67386
68374
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "regression" });
|
|
67387
|
-
|
|
67388
|
-
|
|
67389
|
-
|
|
67390
|
-
|
|
67391
|
-
|
|
67392
|
-
|
|
67393
|
-
|
|
67394
|
-
|
|
67395
|
-
|
|
67396
|
-
|
|
67397
|
-
|
|
67398
|
-
|
|
68375
|
+
let regressionResult;
|
|
68376
|
+
try {
|
|
68377
|
+
regressionResult = await _runCompletionDeps.runDeferredRegression({
|
|
68378
|
+
config: config2,
|
|
68379
|
+
prd,
|
|
68380
|
+
workdir,
|
|
68381
|
+
runtime: options.runtime,
|
|
68382
|
+
quarantineMemo: options.runtime.quarantineMemo,
|
|
68383
|
+
storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
|
|
68384
|
+
storyId: m.storyId,
|
|
68385
|
+
completedAt: m.completedAt,
|
|
68386
|
+
failingTestFiles: m.failingTestFiles
|
|
68387
|
+
}))
|
|
68388
|
+
});
|
|
68389
|
+
} catch (err) {
|
|
68390
|
+
pipelineEventBus.emit({
|
|
68391
|
+
type: "postrun:phase:completed",
|
|
68392
|
+
phase: "regression",
|
|
68393
|
+
passed: false,
|
|
68394
|
+
durationMs: Date.now() - regressionStartTime
|
|
68395
|
+
});
|
|
68396
|
+
throw err;
|
|
68397
|
+
}
|
|
67399
68398
|
const lastRunAt = new Date().toISOString();
|
|
67400
68399
|
logger?.info("regression", "Deferred regression gate completed", {
|
|
67401
68400
|
success: regressionResult.success,
|
|
67402
68401
|
failedTests: regressionResult.failedTests,
|
|
67403
68402
|
affectedStories: regressionResult.affectedStories
|
|
67404
68403
|
});
|
|
68404
|
+
const regressionDurationMs = Date.now() - regressionStartTime;
|
|
67405
68405
|
if (regressionResult.success) {
|
|
67406
68406
|
statusWriter.setPostRunPhase("regression", { status: "passed", lastRunAt });
|
|
67407
|
-
pipelineEventBus.emit({
|
|
68407
|
+
pipelineEventBus.emit({
|
|
68408
|
+
type: "postrun:phase:completed",
|
|
68409
|
+
phase: "regression",
|
|
68410
|
+
passed: true,
|
|
68411
|
+
durationMs: regressionDurationMs,
|
|
68412
|
+
details: { mode: regressionMode, failedTests: 0 }
|
|
68413
|
+
});
|
|
67408
68414
|
} else {
|
|
67409
68415
|
statusWriter.setPostRunPhase("regression", {
|
|
67410
68416
|
status: "failed",
|
|
@@ -67412,7 +68418,16 @@ async function handleRunCompletion(options) {
|
|
|
67412
68418
|
affectedStories: regressionResult.affectedStories,
|
|
67413
68419
|
lastRunAt
|
|
67414
68420
|
});
|
|
67415
|
-
pipelineEventBus.emit({
|
|
68421
|
+
pipelineEventBus.emit({
|
|
68422
|
+
type: "postrun:phase:completed",
|
|
68423
|
+
phase: "regression",
|
|
68424
|
+
passed: false,
|
|
68425
|
+
durationMs: regressionDurationMs,
|
|
68426
|
+
details: {
|
|
68427
|
+
mode: regressionMode,
|
|
68428
|
+
failedTests: regressionResult.failedTests
|
|
68429
|
+
}
|
|
68430
|
+
});
|
|
67416
68431
|
for (const storyId of regressionResult.affectedStories) {
|
|
67417
68432
|
const story = prd.userStories.find((s) => s.id === storyId);
|
|
67418
68433
|
if (story) {
|
|
@@ -67480,7 +68495,15 @@ async function handleRunCompletion(options) {
|
|
|
67480
68495
|
let pluginGateFailed = false;
|
|
67481
68496
|
const deferredReview = options.deferredReview;
|
|
67482
68497
|
if (deferredReview !== undefined) {
|
|
67483
|
-
|
|
68498
|
+
const findingCount = deferredReview.reviewerResults.filter((r) => !r.passed).length;
|
|
68499
|
+
const reviewDurationMs = Date.now() - (options.deferredReviewStartedAt ?? Date.now());
|
|
68500
|
+
pipelineEventBus.emit({
|
|
68501
|
+
type: "postrun:phase:completed",
|
|
68502
|
+
phase: "review",
|
|
68503
|
+
passed: !deferredReview.anyFailed,
|
|
68504
|
+
durationMs: reviewDurationMs,
|
|
68505
|
+
details: { findingCount, anyFailed: deferredReview.anyFailed }
|
|
68506
|
+
});
|
|
67484
68507
|
}
|
|
67485
68508
|
if (deferredReview?.anyFailed) {
|
|
67486
68509
|
const failedReviewers = deferredReview.reviewerResults.filter((r) => !r.passed).map((r) => r.name);
|
|
@@ -67729,6 +68752,7 @@ async function runCompletionPhase(options) {
|
|
|
67729
68752
|
logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
|
|
67730
68753
|
} else if (options.config.acceptance.enabled && isComplete(options.prd)) {
|
|
67731
68754
|
options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
|
|
68755
|
+
const acceptanceStartTime = Date.now();
|
|
67732
68756
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance" });
|
|
67733
68757
|
const acceptanceTestPaths = options.featureDir ? await Promise.all((await groupStoriesByPackage(options.prd, options.workdir, options.feature, options.config.acceptance.testPath, options.config.project?.language)).map(async (g) => {
|
|
67734
68758
|
const relativeWorkdir = path25.relative(options.workdir, g.packageDir);
|
|
@@ -67751,32 +68775,54 @@ async function runCompletionPhase(options) {
|
|
|
67751
68775
|
commandOverride: groupConfig.acceptance.command
|
|
67752
68776
|
};
|
|
67753
68777
|
})) : undefined;
|
|
67754
|
-
|
|
67755
|
-
|
|
67756
|
-
|
|
67757
|
-
|
|
67758
|
-
|
|
67759
|
-
|
|
67760
|
-
|
|
67761
|
-
|
|
67762
|
-
|
|
67763
|
-
|
|
67764
|
-
|
|
67765
|
-
|
|
67766
|
-
|
|
67767
|
-
|
|
67768
|
-
|
|
67769
|
-
|
|
67770
|
-
|
|
67771
|
-
|
|
67772
|
-
|
|
67773
|
-
|
|
67774
|
-
|
|
67775
|
-
|
|
68778
|
+
let acceptanceResult;
|
|
68779
|
+
try {
|
|
68780
|
+
acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
|
|
68781
|
+
config: options.config,
|
|
68782
|
+
prd: options.prd,
|
|
68783
|
+
prdPath: options.prdPath,
|
|
68784
|
+
workdir: options.workdir,
|
|
68785
|
+
featureDir: options.featureDir,
|
|
68786
|
+
hooks: options.hooks,
|
|
68787
|
+
feature: options.feature,
|
|
68788
|
+
totalCost: options.totalCost,
|
|
68789
|
+
iterations: options.iterations,
|
|
68790
|
+
storiesCompleted: options.storiesCompleted,
|
|
68791
|
+
allStoryMetrics: options.allStoryMetrics,
|
|
68792
|
+
pluginRegistry: options.pluginRegistry,
|
|
68793
|
+
eventEmitter: options.eventEmitter,
|
|
68794
|
+
statusWriter: options.statusWriter,
|
|
68795
|
+
agentGetFn: options.agentGetFn,
|
|
68796
|
+
agentManager: options.agentManager,
|
|
68797
|
+
sessionManager: options.sessionManager,
|
|
68798
|
+
runtime: options.runtime,
|
|
68799
|
+
abortSignal: options.abortSignal,
|
|
68800
|
+
acceptanceTestPaths
|
|
68801
|
+
});
|
|
68802
|
+
} catch (err) {
|
|
68803
|
+
pipelineEventBus.emit({
|
|
68804
|
+
type: "postrun:phase:completed",
|
|
68805
|
+
phase: "acceptance",
|
|
68806
|
+
passed: false,
|
|
68807
|
+
durationMs: Date.now() - acceptanceStartTime
|
|
68808
|
+
});
|
|
68809
|
+
throw err;
|
|
68810
|
+
}
|
|
67776
68811
|
const lastRunAt = new Date().toISOString();
|
|
68812
|
+
const acceptanceDurationMs = Date.now() - acceptanceStartTime;
|
|
67777
68813
|
if (acceptanceResult.success) {
|
|
67778
68814
|
options.statusWriter.setPostRunPhase("acceptance", { status: "passed", lastRunAt });
|
|
67779
|
-
pipelineEventBus.emit({
|
|
68815
|
+
pipelineEventBus.emit({
|
|
68816
|
+
type: "postrun:phase:completed",
|
|
68817
|
+
phase: "acceptance",
|
|
68818
|
+
passed: true,
|
|
68819
|
+
durationMs: acceptanceDurationMs,
|
|
68820
|
+
details: {
|
|
68821
|
+
retries: acceptanceResult.retries ?? 0,
|
|
68822
|
+
failedACCount: acceptanceResult.failedACs?.length ?? 0,
|
|
68823
|
+
fixStoriesCreated: 0
|
|
68824
|
+
}
|
|
68825
|
+
});
|
|
67780
68826
|
} else {
|
|
67781
68827
|
acceptancePassed = false;
|
|
67782
68828
|
options.statusWriter.setPostRunPhase("acceptance", {
|
|
@@ -67785,7 +68831,17 @@ async function runCompletionPhase(options) {
|
|
|
67785
68831
|
retries: acceptanceResult.retries ?? 0,
|
|
67786
68832
|
lastRunAt
|
|
67787
68833
|
});
|
|
67788
|
-
pipelineEventBus.emit({
|
|
68834
|
+
pipelineEventBus.emit({
|
|
68835
|
+
type: "postrun:phase:completed",
|
|
68836
|
+
phase: "acceptance",
|
|
68837
|
+
passed: false,
|
|
68838
|
+
durationMs: acceptanceDurationMs,
|
|
68839
|
+
details: {
|
|
68840
|
+
retries: acceptanceResult.retries ?? 0,
|
|
68841
|
+
failedACCount: acceptanceResult.failedACs?.length ?? 0,
|
|
68842
|
+
fixStoriesCreated: 0
|
|
68843
|
+
}
|
|
68844
|
+
});
|
|
67789
68845
|
}
|
|
67790
68846
|
Object.assign(options, {
|
|
67791
68847
|
prd: acceptanceResult.prd,
|
|
@@ -67816,6 +68872,7 @@ async function runCompletionPhase(options) {
|
|
|
67816
68872
|
sessionManager: options.sessionManager,
|
|
67817
68873
|
pluginProviderCache: options.pluginProviderCache,
|
|
67818
68874
|
deferredReview: options.deferredReview,
|
|
68875
|
+
deferredReviewStartedAt: options.deferredReviewStartedAt,
|
|
67819
68876
|
exitReason: options.exitReason,
|
|
67820
68877
|
runtime: options.runtime,
|
|
67821
68878
|
abortSignal: options.abortSignal
|
|
@@ -67892,7 +68949,7 @@ var init_runner_completion = __esm(() => {
|
|
|
67892
68949
|
});
|
|
67893
68950
|
|
|
67894
68951
|
// src/execution/batching.ts
|
|
67895
|
-
function groupStoriesIntoBatches(stories, maxBatchSize =
|
|
68952
|
+
function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
|
|
67896
68953
|
const batches = [];
|
|
67897
68954
|
let currentBatch = [];
|
|
67898
68955
|
for (const story of stories) {
|
|
@@ -67925,7 +68982,7 @@ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE)
|
|
|
67925
68982
|
}
|
|
67926
68983
|
return batches;
|
|
67927
68984
|
}
|
|
67928
|
-
function precomputeBatchPlan(stories, maxBatchSize =
|
|
68985
|
+
function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
|
|
67929
68986
|
const batches = [];
|
|
67930
68987
|
let currentBatch = [];
|
|
67931
68988
|
for (const story of stories) {
|
|
@@ -67962,7 +69019,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
|
|
|
67962
69019
|
}
|
|
67963
69020
|
return batches;
|
|
67964
69021
|
}
|
|
67965
|
-
var
|
|
69022
|
+
var DEFAULT_MAX_BATCH_SIZE2 = 4;
|
|
67966
69023
|
|
|
67967
69024
|
// src/execution/ensure-package-dirs.ts
|
|
67968
69025
|
import path26 from "path";
|
|
@@ -68242,131 +69299,6 @@ var init_registry6 = __esm(() => {
|
|
|
68242
69299
|
init_paths3();
|
|
68243
69300
|
});
|
|
68244
69301
|
|
|
68245
|
-
// src/pipeline/subscribers/reporters.ts
|
|
68246
|
-
function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
68247
|
-
const logger = getSafeLogger();
|
|
68248
|
-
const safe = (name, fn) => {
|
|
68249
|
-
return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
|
|
68250
|
-
};
|
|
68251
|
-
const unsubs = [];
|
|
68252
|
-
unsubs.push(bus.on("run:started", (ev) => {
|
|
68253
|
-
return safe("onRunStart", async () => {
|
|
68254
|
-
const reporters = pluginRegistry.getReporters();
|
|
68255
|
-
for (const r of reporters) {
|
|
68256
|
-
if (r.onRunStart) {
|
|
68257
|
-
try {
|
|
68258
|
-
await r.onRunStart({
|
|
68259
|
-
runId,
|
|
68260
|
-
feature: ev.feature,
|
|
68261
|
-
totalStories: ev.totalStories,
|
|
68262
|
-
startTime: new Date(startTime).toISOString()
|
|
68263
|
-
});
|
|
68264
|
-
} catch (err) {
|
|
68265
|
-
logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
|
|
68266
|
-
}
|
|
68267
|
-
}
|
|
68268
|
-
}
|
|
68269
|
-
});
|
|
68270
|
-
}));
|
|
68271
|
-
unsubs.push(bus.on("story:completed", (ev) => {
|
|
68272
|
-
return safe("onStoryComplete(completed)", async () => {
|
|
68273
|
-
const reporters = pluginRegistry.getReporters();
|
|
68274
|
-
for (const r of reporters) {
|
|
68275
|
-
if (r.onStoryComplete) {
|
|
68276
|
-
try {
|
|
68277
|
-
await r.onStoryComplete({
|
|
68278
|
-
runId,
|
|
68279
|
-
storyId: ev.storyId,
|
|
68280
|
-
status: "completed",
|
|
68281
|
-
runElapsedMs: ev.runElapsedMs,
|
|
68282
|
-
cost: ev.cost ?? 0,
|
|
68283
|
-
tier: ev.modelTier ?? "balanced",
|
|
68284
|
-
testStrategy: ev.testStrategy ?? "test-after"
|
|
68285
|
-
});
|
|
68286
|
-
} catch (err) {
|
|
68287
|
-
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
68288
|
-
}
|
|
68289
|
-
}
|
|
68290
|
-
}
|
|
68291
|
-
});
|
|
68292
|
-
}));
|
|
68293
|
-
unsubs.push(bus.on("story:failed", (ev) => {
|
|
68294
|
-
return safe("onStoryComplete(failed)", async () => {
|
|
68295
|
-
const reporters = pluginRegistry.getReporters();
|
|
68296
|
-
for (const r of reporters) {
|
|
68297
|
-
if (r.onStoryComplete) {
|
|
68298
|
-
try {
|
|
68299
|
-
await r.onStoryComplete({
|
|
68300
|
-
runId,
|
|
68301
|
-
storyId: ev.storyId,
|
|
68302
|
-
status: "failed",
|
|
68303
|
-
runElapsedMs: Date.now() - startTime,
|
|
68304
|
-
cost: 0,
|
|
68305
|
-
tier: "balanced",
|
|
68306
|
-
testStrategy: "test-after"
|
|
68307
|
-
});
|
|
68308
|
-
} catch (err) {
|
|
68309
|
-
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
68310
|
-
}
|
|
68311
|
-
}
|
|
68312
|
-
}
|
|
68313
|
-
});
|
|
68314
|
-
}));
|
|
68315
|
-
unsubs.push(bus.on("story:paused", (ev) => {
|
|
68316
|
-
return safe("onStoryComplete(paused)", async () => {
|
|
68317
|
-
const reporters = pluginRegistry.getReporters();
|
|
68318
|
-
for (const r of reporters) {
|
|
68319
|
-
if (r.onStoryComplete) {
|
|
68320
|
-
try {
|
|
68321
|
-
await r.onStoryComplete({
|
|
68322
|
-
runId,
|
|
68323
|
-
storyId: ev.storyId,
|
|
68324
|
-
status: "paused",
|
|
68325
|
-
runElapsedMs: Date.now() - startTime,
|
|
68326
|
-
cost: 0,
|
|
68327
|
-
tier: "balanced",
|
|
68328
|
-
testStrategy: "test-after"
|
|
68329
|
-
});
|
|
68330
|
-
} catch (err) {
|
|
68331
|
-
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
68332
|
-
}
|
|
68333
|
-
}
|
|
68334
|
-
}
|
|
68335
|
-
});
|
|
68336
|
-
}));
|
|
68337
|
-
unsubs.push(bus.on("run:completed", (ev) => {
|
|
68338
|
-
return safe("onRunEnd", async () => {
|
|
68339
|
-
const reporters = pluginRegistry.getReporters();
|
|
68340
|
-
for (const r of reporters) {
|
|
68341
|
-
if (r.onRunEnd) {
|
|
68342
|
-
try {
|
|
68343
|
-
await r.onRunEnd({
|
|
68344
|
-
runId,
|
|
68345
|
-
totalDurationMs: Date.now() - startTime,
|
|
68346
|
-
totalCost: ev.totalCost ?? 0,
|
|
68347
|
-
storySummary: {
|
|
68348
|
-
completed: ev.passedStories,
|
|
68349
|
-
failed: ev.failedStories,
|
|
68350
|
-
skipped: ev.skippedStories,
|
|
68351
|
-
paused: ev.pausedStories
|
|
68352
|
-
}
|
|
68353
|
-
});
|
|
68354
|
-
} catch (err) {
|
|
68355
|
-
logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
|
|
68356
|
-
}
|
|
68357
|
-
}
|
|
68358
|
-
}
|
|
68359
|
-
});
|
|
68360
|
-
}));
|
|
68361
|
-
return () => {
|
|
68362
|
-
for (const u of unsubs)
|
|
68363
|
-
u();
|
|
68364
|
-
};
|
|
68365
|
-
}
|
|
68366
|
-
var init_reporters = __esm(() => {
|
|
68367
|
-
init_logger2();
|
|
68368
|
-
});
|
|
68369
|
-
|
|
68370
69302
|
// src/execution/deferred-review.ts
|
|
68371
69303
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
68372
69304
|
async function captureRunStartRef(workdir) {
|
|
@@ -70380,6 +71312,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
70380
71312
|
const allStoryMetrics = [];
|
|
70381
71313
|
let warningSent = false;
|
|
70382
71314
|
let deferredReview;
|
|
71315
|
+
let deferredReviewStartedAt;
|
|
70383
71316
|
const runStartRef = await captureRunStartRef(ctx.workdir);
|
|
70384
71317
|
let cachedNaxIgnoreKey;
|
|
70385
71318
|
const getRunNaxIgnoreIndex = async (currentPrd) => {
|
|
@@ -70418,14 +71351,16 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
70418
71351
|
totalCost: totalCost2,
|
|
70419
71352
|
allStoryMetrics,
|
|
70420
71353
|
exitReason,
|
|
70421
|
-
deferredReview
|
|
71354
|
+
deferredReview,
|
|
71355
|
+
deferredReviewStartedAt
|
|
70422
71356
|
});
|
|
70423
|
-
|
|
71357
|
+
startHeartbeat2(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
|
|
70424
71358
|
let _executeThrew = false;
|
|
70425
71359
|
try {
|
|
70426
71360
|
if (isComplete(prd)) {
|
|
70427
71361
|
logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
|
|
70428
71362
|
const naxIgnoreIndex = await getRunNaxIgnoreIndex(prd);
|
|
71363
|
+
deferredReviewStartedAt = Date.now();
|
|
70429
71364
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
|
|
70430
71365
|
deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
|
|
70431
71366
|
return buildResult2("completed");
|
|
@@ -70480,6 +71415,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
70480
71415
|
return buildResult2("pre-merge-aborted");
|
|
70481
71416
|
}
|
|
70482
71417
|
logger?.debug("execution", "Running deferred review");
|
|
71418
|
+
deferredReviewStartedAt = Date.now();
|
|
70483
71419
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
|
|
70484
71420
|
deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
|
|
70485
71421
|
logger?.debug("execution", "Deferred review done \u2014 returning completed");
|
|
@@ -70990,6 +71926,7 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
|
|
|
70990
71926
|
totalCost: totalCost2,
|
|
70991
71927
|
allStoryMetrics,
|
|
70992
71928
|
deferredReview: unifiedResult.deferredReview,
|
|
71929
|
+
deferredReviewStartedAt: unifiedResult.deferredReviewStartedAt,
|
|
70993
71930
|
exitReason: unifiedResult.exitReason
|
|
70994
71931
|
};
|
|
70995
71932
|
}
|
|
@@ -72243,10 +73180,10 @@ async function cleanupRun(options) {
|
|
|
72243
73180
|
}
|
|
72244
73181
|
const actions = pluginRegistry.getPostRunActions();
|
|
72245
73182
|
const pluginLogger = {
|
|
72246
|
-
debug: (msg) => logger?.debug("post-run", msg),
|
|
72247
|
-
info: (msg) => logger?.info("post-run", msg),
|
|
72248
|
-
warn: (msg) => logger?.warn("post-run", msg),
|
|
72249
|
-
error: (msg) => logger?.error("post-run", msg)
|
|
73183
|
+
debug: (msg, data) => logger?.debug("post-run", msg, data),
|
|
73184
|
+
info: (msg, data) => logger?.info("post-run", msg, data),
|
|
73185
|
+
warn: (msg, data) => logger?.warn("post-run", msg, data),
|
|
73186
|
+
error: (msg, data) => logger?.error("post-run", msg, data)
|
|
72250
73187
|
};
|
|
72251
73188
|
const ctx = buildPostRunContext(options, durationMs, pluginLogger);
|
|
72252
73189
|
for (const action of actions) {
|
|
@@ -72442,6 +73379,7 @@ async function run(options) {
|
|
|
72442
73379
|
agentManager,
|
|
72443
73380
|
pluginProviderCache,
|
|
72444
73381
|
deferredReview: executionResult.deferredReview,
|
|
73382
|
+
deferredReviewStartedAt: executionResult.deferredReviewStartedAt,
|
|
72445
73383
|
exitReason: executionResult.exitReason,
|
|
72446
73384
|
runtime,
|
|
72447
73385
|
abortSignal: shutdownController.signal
|
|
@@ -72540,8 +73478,11 @@ __export(exports_execution, {
|
|
|
72540
73478
|
withIncreasingFailuresBail: () => withIncreasingFailuresBail,
|
|
72541
73479
|
synthesizeBackfillMetric: () => synthesizeBackfillMetric,
|
|
72542
73480
|
stopHeartbeat: () => stopHeartbeat,
|
|
72543
|
-
startHeartbeat: () =>
|
|
73481
|
+
startHeartbeat: () => startHeartbeat2,
|
|
73482
|
+
runRectification: () => runRectification,
|
|
73483
|
+
runPhase: () => runPhase,
|
|
72544
73484
|
runDeferredRegression: () => runDeferredRegression,
|
|
73485
|
+
runCompletionPhase: () => runCompletionPhase,
|
|
72545
73486
|
run: () => run,
|
|
72546
73487
|
resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
|
|
72547
73488
|
resetCrashHandlers: () => resetCrashHandlers,
|
|
@@ -72567,7 +73508,6 @@ __export(exports_execution, {
|
|
|
72567
73508
|
getTierConfig: () => getTierConfig,
|
|
72568
73509
|
getOscillations: () => getOscillations,
|
|
72569
73510
|
getAllReadyStories: () => getAllReadyStories,
|
|
72570
|
-
gateRegressedAfterRectification: () => gateRegressedAfterRectification,
|
|
72571
73511
|
gateFailureKeys: () => gateFailureKeys,
|
|
72572
73512
|
formatProgress: () => formatProgress,
|
|
72573
73513
|
formatPhaseResultMessage: () => formatPhaseResultMessage,
|
|
@@ -72576,11 +73516,13 @@ __export(exports_execution, {
|
|
|
72576
73516
|
extractPauseReason: () => extractPauseReason,
|
|
72577
73517
|
escalateTier: () => escalateTier,
|
|
72578
73518
|
ensureStoryPackageDirs: () => ensureStoryPackageDirs,
|
|
73519
|
+
describeGateRegression: () => describeGateRegression,
|
|
72579
73520
|
deriveTddFailureCategory: () => deriveTddFailureCategory,
|
|
72580
73521
|
decideStageAction: () => decideStageAction,
|
|
72581
73522
|
createCheckpointWriter: () => createCheckpointWriter,
|
|
72582
73523
|
countOscillationOutcomes: () => countOscillationOutcomes,
|
|
72583
73524
|
clearQueueFile: () => clearQueueFile,
|
|
73525
|
+
cleanupRun: () => cleanupRun,
|
|
72584
73526
|
captureTreeState: () => captureTreeState,
|
|
72585
73527
|
calculateMaxIterations: () => calculateMaxIterations,
|
|
72586
73528
|
buildStoryContext: () => buildStoryContext,
|
|
@@ -72597,6 +73539,7 @@ __export(exports_execution, {
|
|
|
72597
73539
|
_storyOrchestratorDeps: () => _storyOrchestratorDeps,
|
|
72598
73540
|
_runnerReentrancyGuard: () => _runnerReentrancyGuard,
|
|
72599
73541
|
_runnerDeps: () => _runnerDeps,
|
|
73542
|
+
_runnerCompletionDeps: () => _runnerCompletionDeps,
|
|
72600
73543
|
_runCompletionDeps: () => _runCompletionDeps,
|
|
72601
73544
|
_regressionDeps: () => _regressionDeps,
|
|
72602
73545
|
_postRunDeps: () => _postRunDeps,
|
|
@@ -72628,6 +73571,7 @@ var init_execution2 = __esm(() => {
|
|
|
72628
73571
|
init_plan_inputs();
|
|
72629
73572
|
init_build_plan_for_strategy();
|
|
72630
73573
|
init_checkpoint();
|
|
73574
|
+
init_runner_completion();
|
|
72631
73575
|
init_post_run();
|
|
72632
73576
|
});
|
|
72633
73577
|
|
|
@@ -104407,7 +105351,7 @@ async function resolveRunProfileOverride(opts) {
|
|
|
104407
105351
|
// src/cli/features-resolve.ts
|
|
104408
105352
|
init_config();
|
|
104409
105353
|
import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
|
|
104410
|
-
import { join as join74, relative as
|
|
105354
|
+
import { join as join74, relative as relative17 } from "path";
|
|
104411
105355
|
|
|
104412
105356
|
// src/cli/features-acceptance.ts
|
|
104413
105357
|
init_acceptance2();
|
|
@@ -104415,7 +105359,7 @@ init_config();
|
|
|
104415
105359
|
init_logger2();
|
|
104416
105360
|
init_prd();
|
|
104417
105361
|
import { existsSync as existsSync27 } from "fs";
|
|
104418
|
-
import { join as join73, relative as
|
|
105362
|
+
import { join as join73, relative as relative16 } from "path";
|
|
104419
105363
|
async function resolveFeatureAcceptance(featureName, workdir) {
|
|
104420
105364
|
let enabled = true;
|
|
104421
105365
|
try {
|
|
@@ -104436,11 +105380,11 @@ async function resolveFeatureAcceptance(featureName, workdir) {
|
|
|
104436
105380
|
const prd = await loadPRD(prdPath);
|
|
104437
105381
|
const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
|
|
104438
105382
|
const groups = await Promise.all(testGroups.map(async (g) => {
|
|
104439
|
-
const packageDir =
|
|
105383
|
+
const packageDir = relative16(repoRoot, g.packageDir);
|
|
104440
105384
|
const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
|
|
104441
105385
|
return {
|
|
104442
105386
|
packageDir,
|
|
104443
|
-
testPath:
|
|
105387
|
+
testPath: relative16(repoRoot, g.testPath),
|
|
104444
105388
|
exists: await Bun.file(g.testPath).exists(),
|
|
104445
105389
|
command,
|
|
104446
105390
|
cwd: packageDir,
|
|
@@ -104477,17 +105421,17 @@ async function searchSpecSource(naxDir, repoRoot, name) {
|
|
|
104477
105421
|
];
|
|
104478
105422
|
const docsSpecExact = join74(repoRoot, "docs", "specs", `SPEC-${name}.md`);
|
|
104479
105423
|
candidates.push({ abs: docsSpecExact, kind: "markdown" });
|
|
104480
|
-
const checked = candidates.map((c) =>
|
|
105424
|
+
const checked = candidates.map((c) => relative17(repoRoot, c.abs));
|
|
104481
105425
|
for (const { abs, kind } of candidates.slice(0, 2)) {
|
|
104482
105426
|
if (kind === "markdown") {
|
|
104483
105427
|
const nonEmpty = await isNonEmptyFile(abs);
|
|
104484
105428
|
if (nonEmpty) {
|
|
104485
|
-
return { source: { kind, path:
|
|
105429
|
+
return { source: { kind, path: relative17(repoRoot, abs) }, checked };
|
|
104486
105430
|
}
|
|
104487
105431
|
}
|
|
104488
105432
|
}
|
|
104489
105433
|
if (await isNonEmptyFile(docsSpecExact)) {
|
|
104490
|
-
return { source: { kind: "markdown", path:
|
|
105434
|
+
return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
|
|
104491
105435
|
}
|
|
104492
105436
|
const docsSpecsDir = join74(repoRoot, "docs", "specs");
|
|
104493
105437
|
if (existsSync28(docsSpecsDir)) {
|
|
@@ -104495,7 +105439,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
|
|
|
104495
105439
|
for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
|
|
104496
105440
|
const abs = join74(docsSpecsDir, match);
|
|
104497
105441
|
if (await isNonEmptyFile(abs)) {
|
|
104498
|
-
const relPath =
|
|
105442
|
+
const relPath = relative17(repoRoot, abs);
|
|
104499
105443
|
if (!checked.includes(relPath))
|
|
104500
105444
|
checked.push(relPath);
|
|
104501
105445
|
return { source: { kind: "markdown", path: relPath }, checked };
|
|
@@ -104503,7 +105447,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
|
|
|
104503
105447
|
}
|
|
104504
105448
|
}
|
|
104505
105449
|
const prdAbs = join74(naxDir, "features", name, "prd.json");
|
|
104506
|
-
const prdRel =
|
|
105450
|
+
const prdRel = relative17(repoRoot, prdAbs);
|
|
104507
105451
|
if (!checked.includes(prdRel))
|
|
104508
105452
|
checked.push(prdRel);
|
|
104509
105453
|
if (existsSync28(prdAbs)) {
|
|
@@ -104555,8 +105499,8 @@ async function resolveFeatureSpec(name, workdir) {
|
|
|
104555
105499
|
return {
|
|
104556
105500
|
status: "ok",
|
|
104557
105501
|
featureName: null,
|
|
104558
|
-
specSource: { kind: "markdown", path:
|
|
104559
|
-
message: `resolved spec: ${
|
|
105502
|
+
specSource: { kind: "markdown", path: relative17(repoRoot, abs) },
|
|
105503
|
+
message: `resolved spec: ${relative17(repoRoot, abs)}`
|
|
104560
105504
|
};
|
|
104561
105505
|
}
|
|
104562
105506
|
if (name !== undefined && name.trim() !== "") {
|
|
@@ -106054,9 +106998,9 @@ function parseSchedule(input, now2) {
|
|
|
106054
106998
|
const trimmed = input.trim();
|
|
106055
106999
|
if (trimmed === "")
|
|
106056
107000
|
return { ok: false, error: `Empty schedule value. ${ACCEPTED}` };
|
|
106057
|
-
const
|
|
106058
|
-
if (
|
|
106059
|
-
return
|
|
107001
|
+
const relative18 = parseRelative(trimmed, now2);
|
|
107002
|
+
if (relative18)
|
|
107003
|
+
return relative18;
|
|
106060
107004
|
const timeOfDay = parseTimeOfDay(trimmed, now2);
|
|
106061
107005
|
if (timeOfDay)
|
|
106062
107006
|
return timeOfDay;
|