@nathapp/nax 0.75.2 → 0.75.4
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 +1861 -556
- package/flows/nax-finish/exec.ts +78 -29
- package/flows/nax-finish/nax-finish.flow.ts +155 -16
- package/flows/nax-finish/steps/acceptance.ts +28 -5
- package/flows/nax-finish/steps/forge.ts +55 -6
- package/flows/nax-finish/steps/git.ts +61 -27
- package/flows/nax-finish/steps/quality.ts +11 -2
- package/flows/nax-finish/steps/result.ts +9 -1
- package/flows/nax-finish/types.ts +14 -0
- 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,28 @@ 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(),
|
|
17321
|
+
logs: exports_external.object({
|
|
17322
|
+
enabled: exports_external.boolean().default(false),
|
|
17323
|
+
level: exports_external.enum(["silent", "error", "warn", "info", "debug"]).default("info")
|
|
17324
|
+
}).default({ enabled: false, level: "info" })
|
|
17315
17325
|
}).default({
|
|
17316
17326
|
enabled: false,
|
|
17317
17327
|
headers: {},
|
|
17318
17328
|
serviceName: "nax",
|
|
17319
|
-
timeoutMs: 5000
|
|
17329
|
+
timeoutMs: 5000,
|
|
17330
|
+
detail: "counts",
|
|
17331
|
+
heartbeatIntervalMs: 1e4,
|
|
17332
|
+
maxBatchSize: 64,
|
|
17333
|
+
flushIntervalMs: 5000,
|
|
17334
|
+
maxQueueSize: 2048,
|
|
17335
|
+
logs: { enabled: false, level: "info" }
|
|
17320
17336
|
});
|
|
17321
17337
|
ReportersConfigSchema = exports_external.object({
|
|
17322
17338
|
webhook: WebhookReporterConfigSchema,
|
|
@@ -17331,7 +17347,13 @@ var init_schemas_reporters = __esm(() => {
|
|
|
17331
17347
|
enabled: false,
|
|
17332
17348
|
headers: {},
|
|
17333
17349
|
serviceName: "nax",
|
|
17334
|
-
timeoutMs: 5000
|
|
17350
|
+
timeoutMs: 5000,
|
|
17351
|
+
detail: "counts",
|
|
17352
|
+
heartbeatIntervalMs: 1e4,
|
|
17353
|
+
maxBatchSize: 64,
|
|
17354
|
+
flushIntervalMs: 5000,
|
|
17355
|
+
maxQueueSize: 2048,
|
|
17356
|
+
logs: { enabled: false, level: "info" }
|
|
17335
17357
|
}
|
|
17336
17358
|
});
|
|
17337
17359
|
});
|
|
@@ -18222,6 +18244,10 @@ function formatAdvisorySummary(findings, options) {
|
|
|
18222
18244
|
if (coverageGapCount > 0) {
|
|
18223
18245
|
lines.push(c.gray(` ${coverageGapCount} of ${findings.length} were coverage-gap demotions (recurred past the block limit \u2014 candidate for spec/AC review)`));
|
|
18224
18246
|
}
|
|
18247
|
+
const noActionCount = findings.filter((f) => f.actionRequired === false).length;
|
|
18248
|
+
if (noActionCount > 0) {
|
|
18249
|
+
lines.push(c.gray(` ${noActionCount} of ${findings.length} asked for no change (compliance notes \u2014 the best-effort fix pass skipped them)`));
|
|
18250
|
+
}
|
|
18225
18251
|
lines.push(c.yellow("\u2500".repeat(60)));
|
|
18226
18252
|
for (const f of sorted) {
|
|
18227
18253
|
const location = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : undefined;
|
|
@@ -18230,7 +18256,8 @@ function formatAdvisorySummary(findings, options) {
|
|
|
18230
18256
|
f.storyId ?? "unknown",
|
|
18231
18257
|
location,
|
|
18232
18258
|
f.category,
|
|
18233
|
-
f.coverageGap ? "coverage-gap" : undefined
|
|
18259
|
+
f.coverageGap ? "coverage-gap" : undefined,
|
|
18260
|
+
f.actionRequired === false ? "no-action" : undefined
|
|
18234
18261
|
].filter((v) => typeof v === "string" && v.length > 0);
|
|
18235
18262
|
lines.push(` ${c.gray(parts.join(" \xB7 "))}`);
|
|
18236
18263
|
lines.push(` ${f.issue}`);
|
|
@@ -18367,6 +18394,30 @@ var init_redact = __esm(() => {
|
|
|
18367
18394
|
];
|
|
18368
18395
|
});
|
|
18369
18396
|
|
|
18397
|
+
// src/logger/sink-registry.ts
|
|
18398
|
+
class SinkRegistry {
|
|
18399
|
+
sinks = [];
|
|
18400
|
+
add(sink) {
|
|
18401
|
+
this.sinks.push(sink);
|
|
18402
|
+
return () => {
|
|
18403
|
+
const idx = this.sinks.indexOf(sink);
|
|
18404
|
+
if (idx !== -1) {
|
|
18405
|
+
this.sinks.splice(idx, 1);
|
|
18406
|
+
}
|
|
18407
|
+
};
|
|
18408
|
+
}
|
|
18409
|
+
dispatch(entry) {
|
|
18410
|
+
for (const sink of this.sinks) {
|
|
18411
|
+
try {
|
|
18412
|
+
sink({ ...entry });
|
|
18413
|
+
} catch (error48) {
|
|
18414
|
+
process.stderr.write(`[logger] Sink threw: ${error48}
|
|
18415
|
+
`);
|
|
18416
|
+
}
|
|
18417
|
+
}
|
|
18418
|
+
}
|
|
18419
|
+
}
|
|
18420
|
+
|
|
18370
18421
|
// src/logger/logger.ts
|
|
18371
18422
|
import { mkdirSync } from "fs";
|
|
18372
18423
|
import { appendFile } from "fs/promises";
|
|
@@ -18379,6 +18430,7 @@ class Logger {
|
|
|
18379
18430
|
suppressConsole;
|
|
18380
18431
|
writeQueueTail = Promise.resolve();
|
|
18381
18432
|
pendingLines = [];
|
|
18433
|
+
sinkRegistry = new SinkRegistry;
|
|
18382
18434
|
constructor(options) {
|
|
18383
18435
|
this.level = options.level;
|
|
18384
18436
|
this.filePath = options.filePath;
|
|
@@ -18422,10 +18474,11 @@ class Logger {
|
|
|
18422
18474
|
...sessionRole && { sessionRole },
|
|
18423
18475
|
...strippedData && { data: strippedData }
|
|
18424
18476
|
};
|
|
18477
|
+
const entry = redactEntry(rawEntry);
|
|
18478
|
+
this.sinkRegistry.dispatch(entry);
|
|
18425
18479
|
const consoleEnabled = this.shouldLog(level) && !this.suppressConsole;
|
|
18426
18480
|
if (!consoleEnabled && !this.filePath)
|
|
18427
18481
|
return;
|
|
18428
|
-
const entry = redactEntry(rawEntry);
|
|
18429
18482
|
if (consoleEnabled) {
|
|
18430
18483
|
let consoleOutput = null;
|
|
18431
18484
|
if (this.formatterMode) {
|
|
@@ -18509,8 +18562,19 @@ ${JSON.stringify(entry.data, null, 2)}`;
|
|
|
18509
18562
|
debug: (stage, message, data) => this.log("debug", stage, message, data, storyId)
|
|
18510
18563
|
};
|
|
18511
18564
|
}
|
|
18565
|
+
addSink(sink) {
|
|
18566
|
+
return this.sinkRegistry.add(sink);
|
|
18567
|
+
}
|
|
18512
18568
|
close() {}
|
|
18513
18569
|
}
|
|
18570
|
+
function addSink(sink) {
|
|
18571
|
+
if (!instance) {
|
|
18572
|
+
throw new NaxError("Logger not initialized. Call initLogger() before addSink().", "LOGGER_NOT_INITIALIZED", {
|
|
18573
|
+
stage: "logger"
|
|
18574
|
+
});
|
|
18575
|
+
}
|
|
18576
|
+
return instance.addSink(sink);
|
|
18577
|
+
}
|
|
18514
18578
|
function initLogger(options = { level: "silent" }) {
|
|
18515
18579
|
if (instance) {
|
|
18516
18580
|
throw new Error("Logger already initialized. Call getLogger() to access existing instance.");
|
|
@@ -18539,6 +18603,7 @@ function resetLogger() {
|
|
|
18539
18603
|
}
|
|
18540
18604
|
var LOG_LEVEL_PRIORITY, MAX_BATCH_BYTES, instance = null, noopLogger;
|
|
18541
18605
|
var init_logger = __esm(() => {
|
|
18606
|
+
init_errors();
|
|
18542
18607
|
init_log_format();
|
|
18543
18608
|
init_formatters();
|
|
18544
18609
|
init_redact();
|
|
@@ -33112,6 +33177,7 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
|
|
|
33112
33177
|
"acIndex": 2,
|
|
33113
33178
|
"scopeQuote": "<out-of-scope findings ONLY: verbatim substring of one Out of Scope entry>",
|
|
33114
33179
|
"scopeIndex": 1,
|
|
33180
|
+
"actionRequired": true,
|
|
33115
33181
|
"verifiedBy": {
|
|
33116
33182
|
"command": "command used to inspect the current codebase",
|
|
33117
33183
|
"file": "relative/path/to/file.ts",
|
|
@@ -33165,6 +33231,11 @@ A finding about code that crossed one of these boundaries must NOT cite an AC \u
|
|
|
33165
33231
|
- When the boundary is only a description "Out:" bullet, quote it in \`issue\` and leave \`scopeQuote\`/\`scopeIndex\` unset.
|
|
33166
33232
|
- Emit scope-violation findings as \`"warning"\` \u2014 never \`"error"\`. Reporting the boundary is the goal; it does not block the story.
|
|
33167
33233
|
|
|
33234
|
+
**Do not report compliance as a finding:**
|
|
33235
|
+
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.
|
|
33236
|
+
|
|
33237
|
+
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.
|
|
33238
|
+
|
|
33168
33239
|
Never use \`acIndex: 0\`; \`acIndex\` is 1-based (first AC bullet = 1). The same applies to \`scopeIndex\`.
|
|
33169
33240
|
|
|
33170
33241
|
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 +35958,7 @@ function toAdversarialReviewFindings(findings, opts = {}) {
|
|
|
35887
35958
|
message: f.issue,
|
|
35888
35959
|
suggestion: f.suggestion,
|
|
35889
35960
|
fixTarget: resolveFixTarget({ base: categoryToFixTarget(f.category), file: f.file, isTestFile: opts.isTestFile }),
|
|
35961
|
+
...f.actionRequired === false ? { actionRequired: false } : {},
|
|
35890
35962
|
meta: Object.keys(metaExtras).length > 0 ? metaExtras : undefined
|
|
35891
35963
|
};
|
|
35892
35964
|
});
|
|
@@ -36826,11 +36898,11 @@ var init_adversarial_review = __esm(() => {
|
|
|
36826
36898
|
throw new ParseValidationError("[adversarial-review] parse failed: invalid JSON shape");
|
|
36827
36899
|
},
|
|
36828
36900
|
async verify(parsed, input, _verifyCtx) {
|
|
36901
|
+
const threshold = input.blockingThreshold ?? "error";
|
|
36829
36902
|
if (parsed.failOpen || parsed.looksLikeFail)
|
|
36830
|
-
return parsed;
|
|
36903
|
+
return { ...parsed, blockingThreshold: threshold };
|
|
36831
36904
|
if (parsed.findings.length === 0)
|
|
36832
|
-
return parsed;
|
|
36833
|
-
const threshold = input.blockingThreshold ?? "error";
|
|
36905
|
+
return { ...parsed, blockingThreshold: threshold };
|
|
36834
36906
|
const findings = parsed.findings;
|
|
36835
36907
|
const substantiated = await substantiateAdversarialFindings({
|
|
36836
36908
|
findings,
|
|
@@ -36877,6 +36949,7 @@ var init_adversarial_review = __esm(() => {
|
|
|
36877
36949
|
return {
|
|
36878
36950
|
...parsed,
|
|
36879
36951
|
passed,
|
|
36952
|
+
blockingThreshold: threshold,
|
|
36880
36953
|
modelPassed: parsed.passed,
|
|
36881
36954
|
findings: accepted,
|
|
36882
36955
|
normalizedFindings: toAdversarialReviewFindings(blocking, { isTestFile: testFileMatch }),
|
|
@@ -39901,9 +39974,29 @@ var init_apply_test_edit_declarations = __esm(() => {
|
|
|
39901
39974
|
});
|
|
39902
39975
|
|
|
39903
39976
|
// src/operations/validate-mock-structure-files.ts
|
|
39904
|
-
import { join as join23 } from "path";
|
|
39905
|
-
|
|
39906
|
-
|
|
39977
|
+
import { isAbsolute as isAbsolute9, join as join23, relative as relative9 } from "path";
|
|
39978
|
+
function resolutionCandidates(file3, packageDir, repoRoot) {
|
|
39979
|
+
if (isAbsolute9(file3))
|
|
39980
|
+
return [file3];
|
|
39981
|
+
const viaPackageDir = join23(packageDir, file3);
|
|
39982
|
+
if (repoRoot === undefined)
|
|
39983
|
+
return [viaPackageDir];
|
|
39984
|
+
const viaRepoRoot = join23(repoRoot, file3);
|
|
39985
|
+
return viaRepoRoot === viaPackageDir ? [viaPackageDir] : [viaPackageDir, viaRepoRoot];
|
|
39986
|
+
}
|
|
39987
|
+
async function resolvePackageRelative(file3, opts) {
|
|
39988
|
+
for (const candidate of resolutionCandidates(file3, opts.packageDir, opts.repoRoot)) {
|
|
39989
|
+
if (!await opts.fileExists(candidate))
|
|
39990
|
+
continue;
|
|
39991
|
+
const rel = relative9(opts.packageDir, candidate);
|
|
39992
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute9(rel))
|
|
39993
|
+
return null;
|
|
39994
|
+
return rel;
|
|
39995
|
+
}
|
|
39996
|
+
return null;
|
|
39997
|
+
}
|
|
39998
|
+
async function validateMockStructureFiles(declarations, resolvedTestPatterns, packageDir, opts) {
|
|
39999
|
+
const fileExists = opts?.fileExists ?? defaultFileExists;
|
|
39907
40000
|
const valid = [];
|
|
39908
40001
|
const invalid = [];
|
|
39909
40002
|
for (const d of declarations) {
|
|
@@ -39914,13 +40007,16 @@ async function validateMockStructureFiles(declarations, resolvedTestPatterns, pa
|
|
|
39914
40007
|
const files = d.files ?? [d.file];
|
|
39915
40008
|
let allValid = true;
|
|
39916
40009
|
for (const file3 of files) {
|
|
39917
|
-
const
|
|
39918
|
-
|
|
39919
|
-
|
|
40010
|
+
const packageRelative = await resolvePackageRelative(file3, {
|
|
40011
|
+
packageDir,
|
|
40012
|
+
repoRoot: opts?.repoRoot,
|
|
40013
|
+
fileExists
|
|
40014
|
+
});
|
|
40015
|
+
if (packageRelative === null) {
|
|
39920
40016
|
allValid = false;
|
|
39921
40017
|
break;
|
|
39922
40018
|
}
|
|
39923
|
-
const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(
|
|
40019
|
+
const matchesPattern = resolvedTestPatterns.regex.some((re) => re.test(packageRelative));
|
|
39924
40020
|
if (!matchesPattern) {
|
|
39925
40021
|
allValid = false;
|
|
39926
40022
|
break;
|
|
@@ -41458,7 +41554,7 @@ var init_mutation = __esm(() => {
|
|
|
41458
41554
|
});
|
|
41459
41555
|
|
|
41460
41556
|
// src/operations/mutation-check.ts
|
|
41461
|
-
import { isAbsolute as
|
|
41557
|
+
import { isAbsolute as isAbsolute10, join as join24 } from "path";
|
|
41462
41558
|
var _mutationCheckDeps, mutationCheckOp;
|
|
41463
41559
|
var init_mutation_check = __esm(() => {
|
|
41464
41560
|
init_config();
|
|
@@ -41501,7 +41597,7 @@ var init_mutation_check = __esm(() => {
|
|
|
41501
41597
|
}
|
|
41502
41598
|
const changedFiles = await deps.getChangedNonTestFiles(input.workdir, input.storyGitRef, input.packagePrefix, [...input.resolvedTestPatterns.regex], undefined, input.repoRoot);
|
|
41503
41599
|
const anchor = input.repoRoot ?? input.workdir;
|
|
41504
|
-
const absoluteChangedFiles = changedFiles.map((f) =>
|
|
41600
|
+
const absoluteChangedFiles = changedFiles.map((f) => isAbsolute10(f) ? f : join24(anchor, f));
|
|
41505
41601
|
const survivors = [];
|
|
41506
41602
|
const mutants = [];
|
|
41507
41603
|
for (const file3 of absoluteChangedFiles) {
|
|
@@ -41626,6 +41722,31 @@ var init_operations = __esm(() => {
|
|
|
41626
41722
|
init_mutation_check();
|
|
41627
41723
|
});
|
|
41628
41724
|
|
|
41725
|
+
// src/findings/cycle-retirement.ts
|
|
41726
|
+
function createDeclineLedger() {
|
|
41727
|
+
const declinedByStrategy = new Map;
|
|
41728
|
+
const hasDeclined = (strategyName, finding) => declinedByStrategy.get(strategyName)?.has(findingKey(finding)) === true;
|
|
41729
|
+
const isRetiredFor = (strategy, findings) => {
|
|
41730
|
+
const claimed = findings.filter((f) => strategy.appliesTo(f));
|
|
41731
|
+
return claimed.length > 0 && claimed.every((f) => hasDeclined(strategy.name, f));
|
|
41732
|
+
};
|
|
41733
|
+
return {
|
|
41734
|
+
recordDeclined(strategy, dispatched) {
|
|
41735
|
+
const declined = declinedByStrategy.get(strategy.name) ?? new Set;
|
|
41736
|
+
for (const f of dispatched.filter((x) => strategy.appliesTo(x)))
|
|
41737
|
+
declined.add(findingKey(f));
|
|
41738
|
+
declinedByStrategy.set(strategy.name, declined);
|
|
41739
|
+
},
|
|
41740
|
+
isRetiredFor,
|
|
41741
|
+
retiredNames(strategies, findings) {
|
|
41742
|
+
return strategies.filter((s) => isRetiredFor(s, findings)).map((s) => s.name);
|
|
41743
|
+
}
|
|
41744
|
+
};
|
|
41745
|
+
}
|
|
41746
|
+
var init_cycle_retirement = __esm(() => {
|
|
41747
|
+
init_types6();
|
|
41748
|
+
});
|
|
41749
|
+
|
|
41629
41750
|
// src/findings/cycle.ts
|
|
41630
41751
|
function normalizeValidateResult(r) {
|
|
41631
41752
|
return Array.isArray(r) ? { findings: r, shortCircuited: false } : r;
|
|
@@ -41695,17 +41816,18 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41695
41816
|
const storyId = ctx.storyId;
|
|
41696
41817
|
const packageDir = ctx.packageDir;
|
|
41697
41818
|
let totalCostUsd = 0;
|
|
41698
|
-
const
|
|
41819
|
+
const declines = createDeclineLedger();
|
|
41699
41820
|
let unresolvedDetail;
|
|
41700
41821
|
const finish = (result) => unresolvedDetail !== undefined && result.unresolvedDetail === undefined ? { ...result, unresolvedDetail } : result;
|
|
41701
41822
|
for (;; ) {
|
|
41702
41823
|
if (cycle.findings.length === 0 && cycle.verdict === undefined) {
|
|
41703
41824
|
return { iterations: cycle.iterations, finalFindings: [], exitReason: "resolved", costUsd: totalCostUsd };
|
|
41704
41825
|
}
|
|
41705
|
-
const selectable = cycle.strategies.filter((s) => !
|
|
41826
|
+
const selectable = cycle.strategies.filter((s) => !declines.isRetiredFor(s, cycle.findings));
|
|
41706
41827
|
const active = selectActiveStrategies(selectable, cycle.findings, cycle.verdict);
|
|
41707
41828
|
if (active.length === 0) {
|
|
41708
41829
|
const orphanSources = [...new Set(cycle.findings.map((f) => f.source))];
|
|
41830
|
+
const retiredStrategies = declines.retiredNames(cycle.strategies, cycle.findings);
|
|
41709
41831
|
logger?.warn("findings.cycle", "cycle exited \u2014 no matching strategy (orphaned findings)", {
|
|
41710
41832
|
storyId,
|
|
41711
41833
|
packageDir,
|
|
@@ -41713,7 +41835,7 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41713
41835
|
reason: "no-strategy",
|
|
41714
41836
|
findingsCount: cycle.findings.length,
|
|
41715
41837
|
orphanSources,
|
|
41716
|
-
...
|
|
41838
|
+
...retiredStrategies.length > 0 ? { retiredStrategies } : {}
|
|
41717
41839
|
});
|
|
41718
41840
|
return finish({
|
|
41719
41841
|
iterations: cycle.iterations,
|
|
@@ -41784,7 +41906,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41784
41906
|
for (const strategy of group) {
|
|
41785
41907
|
const relevantFindings = findingsBefore.filter((f) => strategy.appliesTo(f));
|
|
41786
41908
|
const input = strategy.buildInput(relevantFindings, cycle.iterations, ctx);
|
|
41787
|
-
const
|
|
41909
|
+
const fixCtx = {
|
|
41910
|
+
...ctx,
|
|
41911
|
+
fixStrategy: { name: strategy.name, findingsBefore: findingsBefore.length }
|
|
41912
|
+
};
|
|
41913
|
+
const output = await doCallOp(fixCtx, strategy.fixOp, input);
|
|
41788
41914
|
const extracted = await (strategy.extractApplied?.(output, input) ?? {});
|
|
41789
41915
|
fixesApplied.push({
|
|
41790
41916
|
strategyName: strategy.name,
|
|
@@ -41799,8 +41925,11 @@ async function runFixCycle(cycle, ctx, cycleName, _deps = {}) {
|
|
|
41799
41925
|
if (unresolvedFas.length > 0) {
|
|
41800
41926
|
const firstUnresolved = unresolvedFas[0];
|
|
41801
41927
|
unresolvedDetail = firstUnresolved.unresolved;
|
|
41802
|
-
for (const fa of unresolvedFas)
|
|
41803
|
-
|
|
41928
|
+
for (const fa of unresolvedFas) {
|
|
41929
|
+
const strategy = group.find((s) => s.name === fa.strategyName);
|
|
41930
|
+
if (strategy)
|
|
41931
|
+
declines.recordDeclined(strategy, findingsBefore);
|
|
41932
|
+
}
|
|
41804
41933
|
const allGaveUp = unresolvedFas.length === fixesApplied.length;
|
|
41805
41934
|
if (allGaveUp) {
|
|
41806
41935
|
const finishedAt2 = now();
|
|
@@ -42015,6 +42144,7 @@ var _cycleDeps;
|
|
|
42015
42144
|
var init_cycle = __esm(() => {
|
|
42016
42145
|
init_logger2();
|
|
42017
42146
|
init_operations();
|
|
42147
|
+
init_cycle_retirement();
|
|
42018
42148
|
init_types6();
|
|
42019
42149
|
_cycleDeps = {
|
|
42020
42150
|
callOp,
|
|
@@ -42346,13 +42476,13 @@ var init_finding_projection = __esm(() => {
|
|
|
42346
42476
|
});
|
|
42347
42477
|
|
|
42348
42478
|
// src/review/prepare-inputs.ts
|
|
42349
|
-
import { relative as
|
|
42479
|
+
import { relative as relative10, sep } from "path";
|
|
42350
42480
|
function derivePackageDirs(workdir, projectDir) {
|
|
42351
42481
|
const repoRoot = projectDir ?? workdir;
|
|
42352
42482
|
const packageDir = workdir !== repoRoot ? workdir : undefined;
|
|
42353
42483
|
let packageDirRelative;
|
|
42354
42484
|
if (projectDir && workdir !== projectDir) {
|
|
42355
|
-
const rel =
|
|
42485
|
+
const rel = relative10(projectDir, workdir);
|
|
42356
42486
|
if (rel !== ".." && !rel.startsWith(`..${sep}`)) {
|
|
42357
42487
|
packageDirRelative = rel && rel !== "." ? rel : undefined;
|
|
42358
42488
|
}
|
|
@@ -42493,7 +42623,7 @@ var package_default;
|
|
|
42493
42623
|
var init_package = __esm(() => {
|
|
42494
42624
|
package_default = {
|
|
42495
42625
|
name: "@nathapp/nax",
|
|
42496
|
-
version: "0.75.
|
|
42626
|
+
version: "0.75.4",
|
|
42497
42627
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
42498
42628
|
type: "module",
|
|
42499
42629
|
bin: {
|
|
@@ -42504,9 +42634,10 @@ var init_package = __esm(() => {
|
|
|
42504
42634
|
dev: "bun run bin/nax.ts",
|
|
42505
42635
|
build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
|
|
42506
42636
|
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",
|
|
42637
|
+
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
42638
|
"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
42639
|
"lint:fix": "bun x biome check --write src/ bin/ flows/",
|
|
42640
|
+
"check:flows-no-bun": "bun run scripts/check-flows-no-bun.ts",
|
|
42510
42641
|
"check:no-real-global-nax": "bun run scripts/check-no-real-global-nax.ts",
|
|
42511
42642
|
"check:alias-internals": "bun run scripts/check-alias-internals.ts",
|
|
42512
42643
|
"check:deep-relatives": "bun run scripts/check-deep-relatives.ts",
|
|
@@ -42596,8 +42727,8 @@ var init_version = __esm(() => {
|
|
|
42596
42727
|
NAX_VERSION = package_default.version;
|
|
42597
42728
|
NAX_COMMIT = (() => {
|
|
42598
42729
|
try {
|
|
42599
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
42600
|
-
return "
|
|
42730
|
+
if (/^[0-9a-f]{6,10}$/.test("5aee16bf"))
|
|
42731
|
+
return "5aee16bf";
|
|
42601
42732
|
} catch {}
|
|
42602
42733
|
try {
|
|
42603
42734
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -42686,7 +42817,8 @@ function toAdvisorySummaryEntries(entry) {
|
|
|
42686
42817
|
file: f.file,
|
|
42687
42818
|
line: f.line,
|
|
42688
42819
|
issue: f.issue ?? "(no description)",
|
|
42689
|
-
coverageGap: f.meta?.coverageGap === true ? true : undefined
|
|
42820
|
+
coverageGap: f.meta?.coverageGap === true ? true : undefined,
|
|
42821
|
+
actionRequired: f.actionRequired === false ? false : undefined
|
|
42690
42822
|
};
|
|
42691
42823
|
});
|
|
42692
42824
|
}
|
|
@@ -43420,7 +43552,7 @@ var init_language_commands = __esm(() => {
|
|
|
43420
43552
|
});
|
|
43421
43553
|
|
|
43422
43554
|
// src/review/scoped-lint.ts
|
|
43423
|
-
import { join as join27, relative as
|
|
43555
|
+
import { join as join27, relative as relative11 } from "path";
|
|
43424
43556
|
function shellQuotePath4(path6) {
|
|
43425
43557
|
return `'${path6.replaceAll("'", "'\\''")}'`;
|
|
43426
43558
|
}
|
|
@@ -43456,7 +43588,7 @@ async function listChangedFiles(workdir, baseRef) {
|
|
|
43456
43588
|
function inferActivePackageDir(workdir, projectDir) {
|
|
43457
43589
|
if (!projectDir)
|
|
43458
43590
|
return;
|
|
43459
|
-
const rel = normalizePath3(
|
|
43591
|
+
const rel = normalizePath3(relative11(projectDir, workdir));
|
|
43460
43592
|
if (!rel || rel === "." || rel.startsWith(".."))
|
|
43461
43593
|
return;
|
|
43462
43594
|
return rel;
|
|
@@ -44610,6 +44742,7 @@ var init_runner2 = __esm(() => {
|
|
|
44610
44742
|
// src/review/index.ts
|
|
44611
44743
|
var init_review = __esm(() => {
|
|
44612
44744
|
init_semantic_helpers();
|
|
44745
|
+
init_adversarial_helpers();
|
|
44613
44746
|
init_category_fix_target();
|
|
44614
44747
|
init_finding_filters();
|
|
44615
44748
|
init_ac_quote_validator();
|
|
@@ -44894,7 +45027,10 @@ REASON: <one paragraph: which mock is wrong vs which dispatch the new code uses,
|
|
|
44894
45027
|
Rules:
|
|
44895
45028
|
- Do NOT make any edits yourself; the test-writer will fulfill.
|
|
44896
45029
|
- Do NOT also emit \`UNRESOLVED:\` in the same turn \u2014 this declaration IS the handoff.
|
|
44897
|
-
- FILES must list real test files. Each path must exist and be a test file
|
|
45030
|
+
- FILES must list real test files. Each path must exist and be a test file.
|
|
45031
|
+
- Write each path exactly as it appears in the findings above (repository-relative).
|
|
45032
|
+
Paths that resolve under neither the repository root nor the package directory are
|
|
45033
|
+
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 = `
|
|
44898
45034
|
|
|
44899
45035
|
## Test-edit guidance (single-session implementer)
|
|
44900
45036
|
|
|
@@ -48676,7 +48812,7 @@ var init_pid_registry = __esm(() => {
|
|
|
48676
48812
|
// src/session/manager-deps.ts
|
|
48677
48813
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
48678
48814
|
import { mkdir as mkdir5 } from "fs/promises";
|
|
48679
|
-
import { isAbsolute as
|
|
48815
|
+
import { isAbsolute as isAbsolute11, join as join30, relative as relative12, sep as sep2 } from "path";
|
|
48680
48816
|
function resolveProjectDirFromScratchDir(scratchDir) {
|
|
48681
48817
|
const marker = `${sep2}.nax${sep2}features${sep2}`;
|
|
48682
48818
|
const markerIdx = scratchDir.lastIndexOf(marker);
|
|
@@ -48688,7 +48824,7 @@ function resolveProjectDirFromScratchDir(scratchDir) {
|
|
|
48688
48824
|
return;
|
|
48689
48825
|
}
|
|
48690
48826
|
function toProjectRelativePath(projectDir, pathValue) {
|
|
48691
|
-
const relativePath =
|
|
48827
|
+
const relativePath = isAbsolute11(pathValue) ? relative12(projectDir, pathValue) : pathValue;
|
|
48692
48828
|
return relativePath === "" ? "." : relativePath;
|
|
48693
48829
|
}
|
|
48694
48830
|
var _sessionManagerDeps;
|
|
@@ -50198,7 +50334,7 @@ var init_windsurf = __esm(() => {
|
|
|
50198
50334
|
|
|
50199
50335
|
// src/context/generator.ts
|
|
50200
50336
|
import { existsSync as existsSync9 } from "fs";
|
|
50201
|
-
import { join as join33, relative as
|
|
50337
|
+
import { join as join33, relative as relative13 } from "path";
|
|
50202
50338
|
async function loadContextContent(options, config2) {
|
|
50203
50339
|
if (!_generatorDeps.existsSync(options.contextPath)) {
|
|
50204
50340
|
throw new Error(`Context file not found: ${options.contextPath}`);
|
|
@@ -50326,7 +50462,7 @@ async function discoverWorkspacePackages2(repoRoot) {
|
|
|
50326
50462
|
}
|
|
50327
50463
|
async function generateForPackage(packageDir, config2, dryRun = false, repoRoot) {
|
|
50328
50464
|
const resolvedRepoRoot = repoRoot ?? packageDir;
|
|
50329
|
-
const relativePkgPath =
|
|
50465
|
+
const relativePkgPath = relative13(resolvedRepoRoot, packageDir);
|
|
50330
50466
|
const contextPath = join33(resolvedRepoRoot, ".nax", "mono", relativePkgPath, "context.md");
|
|
50331
50467
|
if (!_generatorDeps.existsSync(contextPath)) {
|
|
50332
50468
|
return [
|
|
@@ -54173,7 +54309,7 @@ var init_checks_blockers = __esm(() => {
|
|
|
54173
54309
|
|
|
54174
54310
|
// src/precheck/checks-warnings.ts
|
|
54175
54311
|
import { existsSync as existsSync14 } from "fs";
|
|
54176
|
-
import { isAbsolute as
|
|
54312
|
+
import { isAbsolute as isAbsolute12 } from "path";
|
|
54177
54313
|
async function checkClaudeMdExists(workdir) {
|
|
54178
54314
|
const claudeMdPath = `${workdir}/CLAUDE.md`;
|
|
54179
54315
|
const passed = existsSync14(claudeMdPath);
|
|
@@ -54308,7 +54444,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
|
|
|
54308
54444
|
}
|
|
54309
54445
|
async function checkHomeEnvValid() {
|
|
54310
54446
|
const home = process.env.HOME ?? "";
|
|
54311
|
-
const passed = home !== "" &&
|
|
54447
|
+
const passed = home !== "" && isAbsolute12(home);
|
|
54312
54448
|
return {
|
|
54313
54449
|
name: "home-env-valid",
|
|
54314
54450
|
tier: "warning",
|
|
@@ -57012,6 +57148,79 @@ ${stderr}`;
|
|
|
57012
57148
|
};
|
|
57013
57149
|
});
|
|
57014
57150
|
|
|
57151
|
+
// src/pipeline/event-bus.ts
|
|
57152
|
+
class PipelineEventBus {
|
|
57153
|
+
subscribers = new Map;
|
|
57154
|
+
_pending = new Set;
|
|
57155
|
+
on(eventType, subscriber) {
|
|
57156
|
+
const list = this.subscribers.get(eventType) ?? [];
|
|
57157
|
+
list.push(subscriber);
|
|
57158
|
+
this.subscribers.set(eventType, list);
|
|
57159
|
+
return () => {
|
|
57160
|
+
const current = this.subscribers.get(eventType) ?? [];
|
|
57161
|
+
this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
|
|
57162
|
+
};
|
|
57163
|
+
}
|
|
57164
|
+
onAll(subscriber) {
|
|
57165
|
+
const list = this.subscribers.get("*") ?? [];
|
|
57166
|
+
list.push(subscriber);
|
|
57167
|
+
this.subscribers.set("*", list);
|
|
57168
|
+
return () => {
|
|
57169
|
+
const current = this.subscribers.get("*") ?? [];
|
|
57170
|
+
this.subscribers.set("*", current.filter((s) => s !== subscriber));
|
|
57171
|
+
};
|
|
57172
|
+
}
|
|
57173
|
+
emit(event) {
|
|
57174
|
+
const logger = getLogger();
|
|
57175
|
+
const specific = this.subscribers.get(event.type) ?? [];
|
|
57176
|
+
const all = this.subscribers.get("*") ?? [];
|
|
57177
|
+
const targets = [...specific, ...all];
|
|
57178
|
+
for (const sub of targets) {
|
|
57179
|
+
try {
|
|
57180
|
+
const result = sub(event);
|
|
57181
|
+
if (result instanceof Promise) {
|
|
57182
|
+
const tracked = result.catch((err) => {
|
|
57183
|
+
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57184
|
+
});
|
|
57185
|
+
this._pending.add(tracked);
|
|
57186
|
+
tracked.finally(() => this._pending.delete(tracked));
|
|
57187
|
+
}
|
|
57188
|
+
} catch (err) {
|
|
57189
|
+
logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
|
|
57190
|
+
}
|
|
57191
|
+
}
|
|
57192
|
+
}
|
|
57193
|
+
async emitAsync(event) {
|
|
57194
|
+
const logger = getLogger();
|
|
57195
|
+
const specific = this.subscribers.get(event.type) ?? [];
|
|
57196
|
+
const all = this.subscribers.get("*") ?? [];
|
|
57197
|
+
const targets = [...specific, ...all];
|
|
57198
|
+
await Promise.allSettled(targets.map(async (sub) => {
|
|
57199
|
+
try {
|
|
57200
|
+
await sub(event);
|
|
57201
|
+
} catch (err) {
|
|
57202
|
+
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57203
|
+
}
|
|
57204
|
+
}));
|
|
57205
|
+
}
|
|
57206
|
+
async drain() {
|
|
57207
|
+
if (this._pending.size === 0)
|
|
57208
|
+
return;
|
|
57209
|
+
await Promise.allSettled([...this._pending]);
|
|
57210
|
+
}
|
|
57211
|
+
clear() {
|
|
57212
|
+
this.subscribers.clear();
|
|
57213
|
+
}
|
|
57214
|
+
subscriberCount(eventType) {
|
|
57215
|
+
return (this.subscribers.get(eventType) ?? []).length;
|
|
57216
|
+
}
|
|
57217
|
+
}
|
|
57218
|
+
var pipelineEventBus;
|
|
57219
|
+
var init_event_bus = __esm(() => {
|
|
57220
|
+
init_logger2();
|
|
57221
|
+
pipelineEventBus = new PipelineEventBus;
|
|
57222
|
+
});
|
|
57223
|
+
|
|
57015
57224
|
// src/pipeline/stages/acceptance-setup.ts
|
|
57016
57225
|
var exports_acceptance_setup = {};
|
|
57017
57226
|
__export(exports_acceptance_setup, {
|
|
@@ -57027,6 +57236,214 @@ function computeACFingerprint(criteria) {
|
|
|
57027
57236
|
hasher.update(sorted);
|
|
57028
57237
|
return `sha256:${hasher.digest("hex")}`;
|
|
57029
57238
|
}
|
|
57239
|
+
async function runAcceptanceSetup(ctx, featureDir, phaseStartTime) {
|
|
57240
|
+
const language = ctx.config.project?.language;
|
|
57241
|
+
const testPathConfig = ctx.config.acceptance.testPath;
|
|
57242
|
+
const metaPath = path12.join(featureDir, "acceptance-meta.json");
|
|
57243
|
+
const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-") && s.status !== "decomposed").flatMap((s) => s.acceptanceCriteria);
|
|
57244
|
+
const featureName = ctx.prd.feature ?? ctx.prd.featureName;
|
|
57245
|
+
const groups = await groupStoriesByPackage(ctx.prd, ctx.workdir, featureName, testPathConfig, language);
|
|
57246
|
+
const nonFixStories = groups.flatMap((g) => g.stories);
|
|
57247
|
+
let totalCriteria = 0;
|
|
57248
|
+
let testableCount = 0;
|
|
57249
|
+
const fingerprint = computeACFingerprint(allCriteria);
|
|
57250
|
+
const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
|
|
57251
|
+
getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
|
|
57252
|
+
currentFingerprint: fingerprint,
|
|
57253
|
+
storedFingerprint: meta3?.acFingerprint ?? "none",
|
|
57254
|
+
match: meta3?.acFingerprint === fingerprint
|
|
57255
|
+
});
|
|
57256
|
+
let shouldGenerate = false;
|
|
57257
|
+
let regenerated = false;
|
|
57258
|
+
if (!meta3 || meta3.acFingerprint !== fingerprint) {
|
|
57259
|
+
if (!meta3) {
|
|
57260
|
+
getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
|
|
57261
|
+
} else {
|
|
57262
|
+
getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
|
|
57263
|
+
reason: "fingerprint mismatch",
|
|
57264
|
+
currentFingerprint: fingerprint,
|
|
57265
|
+
storedFingerprint: meta3.acFingerprint
|
|
57266
|
+
});
|
|
57267
|
+
}
|
|
57268
|
+
for (const { testPath } of groups) {
|
|
57269
|
+
if (await _acceptanceSetupDeps.fileExists(testPath)) {
|
|
57270
|
+
await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
|
|
57271
|
+
await _acceptanceSetupDeps.deleteFile(testPath);
|
|
57272
|
+
}
|
|
57273
|
+
}
|
|
57274
|
+
await _acceptanceSetupDeps.deleteSemanticVerdicts(featureDir);
|
|
57275
|
+
shouldGenerate = true;
|
|
57276
|
+
regenerated = true;
|
|
57277
|
+
} else {
|
|
57278
|
+
getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
|
|
57279
|
+
}
|
|
57280
|
+
if (shouldGenerate) {
|
|
57281
|
+
totalCriteria = allCriteria.length;
|
|
57282
|
+
let allRefinedCriteria;
|
|
57283
|
+
if (ctx.config.acceptance.refinement) {
|
|
57284
|
+
const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
|
|
57285
|
+
const results = new Array(nonFixStories.length);
|
|
57286
|
+
const executing = new Set;
|
|
57287
|
+
for (let i = 0;i < nonFixStories.length; i++) {
|
|
57288
|
+
const story = nonFixStories[i];
|
|
57289
|
+
const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
|
|
57290
|
+
criteria: story.acceptanceCriteria,
|
|
57291
|
+
codebaseContext: "",
|
|
57292
|
+
storyId: story.id,
|
|
57293
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
57294
|
+
testFramework: ctx.config.acceptance.testFramework,
|
|
57295
|
+
storyTitle: story.title,
|
|
57296
|
+
storyDescription: story.description
|
|
57297
|
+
}, story.id).then((refined) => {
|
|
57298
|
+
results[i] = refined;
|
|
57299
|
+
}).catch(() => {
|
|
57300
|
+
getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
|
|
57301
|
+
storyId: story.id
|
|
57302
|
+
});
|
|
57303
|
+
results[i] = story.acceptanceCriteria.map((c) => ({
|
|
57304
|
+
original: c,
|
|
57305
|
+
refined: c,
|
|
57306
|
+
testable: true,
|
|
57307
|
+
storyId: story.id
|
|
57308
|
+
}));
|
|
57309
|
+
}).finally(() => {
|
|
57310
|
+
executing.delete(task);
|
|
57311
|
+
});
|
|
57312
|
+
executing.add(task);
|
|
57313
|
+
if (executing.size >= maxConcurrency) {
|
|
57314
|
+
await Promise.race(executing);
|
|
57315
|
+
}
|
|
57316
|
+
}
|
|
57317
|
+
await Promise.all(executing);
|
|
57318
|
+
allRefinedCriteria = results.flat();
|
|
57319
|
+
} else {
|
|
57320
|
+
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
57321
|
+
original: c,
|
|
57322
|
+
refined: c,
|
|
57323
|
+
testable: true,
|
|
57324
|
+
storyId: story.id
|
|
57325
|
+
})));
|
|
57326
|
+
}
|
|
57327
|
+
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
57328
|
+
for (const group of groups) {
|
|
57329
|
+
const { testPath, packageDir } = group;
|
|
57330
|
+
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
57331
|
+
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
57332
|
+
const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
|
|
57333
|
+
`);
|
|
57334
|
+
const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
|
|
57335
|
+
[FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
|
|
57336
|
+
const groupStoryId = group.stories[0]?.id;
|
|
57337
|
+
const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
|
|
57338
|
+
featureName: featureName ?? "",
|
|
57339
|
+
criteriaList,
|
|
57340
|
+
frameworkOverrideLine,
|
|
57341
|
+
targetTestFilePath: testPath,
|
|
57342
|
+
..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
|
|
57343
|
+
}, groupStoryId);
|
|
57344
|
+
const testCode = genResult.testCode;
|
|
57345
|
+
if (testCode) {
|
|
57346
|
+
await _acceptanceSetupDeps.writeFile(testPath, testCode);
|
|
57347
|
+
} else {
|
|
57348
|
+
const skeletonCriteria = groupRefined.map((c, i) => ({
|
|
57349
|
+
id: `AC-${i + 1}`,
|
|
57350
|
+
text: c.refined,
|
|
57351
|
+
lineNumber: i + 1
|
|
57352
|
+
}));
|
|
57353
|
+
const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
|
|
57354
|
+
await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
|
|
57355
|
+
getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
|
|
57356
|
+
storyId: groupStoryId,
|
|
57357
|
+
testPath
|
|
57358
|
+
});
|
|
57359
|
+
}
|
|
57360
|
+
}
|
|
57361
|
+
if (allRefinedCriteria.length > 0) {
|
|
57362
|
+
const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
|
|
57363
|
+
acId: `AC-${i + 1}`,
|
|
57364
|
+
original: c.original,
|
|
57365
|
+
refined: c.refined,
|
|
57366
|
+
testable: c.testable,
|
|
57367
|
+
storyId: c.storyId
|
|
57368
|
+
})), null, 2);
|
|
57369
|
+
await _acceptanceSetupDeps.writeFile(path12.join(featureDir, "acceptance-refined.json"), refinedJsonContent);
|
|
57370
|
+
}
|
|
57371
|
+
const fingerprint2 = computeACFingerprint(allCriteria);
|
|
57372
|
+
await _acceptanceSetupDeps.writeMeta(metaPath, {
|
|
57373
|
+
generatedAt: new Date().toISOString(),
|
|
57374
|
+
acFingerprint: fingerprint2,
|
|
57375
|
+
storyCount: ctx.prd.userStories.length,
|
|
57376
|
+
acCount: totalCriteria,
|
|
57377
|
+
generator: "nax"
|
|
57378
|
+
});
|
|
57379
|
+
await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
|
|
57380
|
+
}
|
|
57381
|
+
const acceptanceTestPaths = [];
|
|
57382
|
+
for (const g of groups) {
|
|
57383
|
+
const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
|
|
57384
|
+
let groupConfig = ctx.config;
|
|
57385
|
+
if (relativeWorkdir && relativeWorkdir !== ".") {
|
|
57386
|
+
try {
|
|
57387
|
+
groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
|
|
57388
|
+
} catch {
|
|
57389
|
+
groupConfig = ctx.config;
|
|
57390
|
+
}
|
|
57391
|
+
}
|
|
57392
|
+
acceptanceTestPaths.push({
|
|
57393
|
+
testPath: g.testPath,
|
|
57394
|
+
packageDir: g.packageDir,
|
|
57395
|
+
testFramework: groupConfig.project?.testFramework,
|
|
57396
|
+
commandOverride: groupConfig.acceptance.command
|
|
57397
|
+
});
|
|
57398
|
+
}
|
|
57399
|
+
ctx.acceptanceTestPaths = acceptanceTestPaths;
|
|
57400
|
+
if (ctx.config.acceptance.redGate === false) {
|
|
57401
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57402
|
+
pipelineEventBus.emit({
|
|
57403
|
+
type: "postrun:phase:completed",
|
|
57404
|
+
phase: "acceptance-setup",
|
|
57405
|
+
passed: true,
|
|
57406
|
+
durationMs: Date.now() - phaseStartTime,
|
|
57407
|
+
details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
|
|
57408
|
+
});
|
|
57409
|
+
return { action: "continue" };
|
|
57410
|
+
}
|
|
57411
|
+
let redFailCount = 0;
|
|
57412
|
+
for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
|
|
57413
|
+
const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
|
|
57414
|
+
getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
|
|
57415
|
+
cmd: runCmd.join(" "),
|
|
57416
|
+
packageDir
|
|
57417
|
+
});
|
|
57418
|
+
const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
|
|
57419
|
+
if (exitCode !== 0) {
|
|
57420
|
+
redFailCount++;
|
|
57421
|
+
}
|
|
57422
|
+
}
|
|
57423
|
+
if (redFailCount === 0) {
|
|
57424
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57425
|
+
pipelineEventBus.emit({
|
|
57426
|
+
type: "postrun:phase:completed",
|
|
57427
|
+
phase: "acceptance-setup",
|
|
57428
|
+
passed: true,
|
|
57429
|
+
durationMs: Date.now() - phaseStartTime,
|
|
57430
|
+
details: { totalCriteria, testableCount, redFailCount: 0, regenerated }
|
|
57431
|
+
});
|
|
57432
|
+
return {
|
|
57433
|
+
action: "skip",
|
|
57434
|
+
reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
|
|
57435
|
+
};
|
|
57436
|
+
}
|
|
57437
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
|
|
57438
|
+
pipelineEventBus.emit({
|
|
57439
|
+
type: "postrun:phase:completed",
|
|
57440
|
+
phase: "acceptance-setup",
|
|
57441
|
+
passed: true,
|
|
57442
|
+
durationMs: Date.now() - phaseStartTime,
|
|
57443
|
+
details: { totalCriteria, testableCount, redFailCount, regenerated }
|
|
57444
|
+
});
|
|
57445
|
+
return { action: "continue" };
|
|
57446
|
+
}
|
|
57030
57447
|
var _acceptanceSetupDeps, acceptanceSetupStage;
|
|
57031
57448
|
var init_acceptance_setup = __esm(() => {
|
|
57032
57449
|
init_acceptance2();
|
|
@@ -57035,6 +57452,7 @@ var init_acceptance_setup = __esm(() => {
|
|
|
57035
57452
|
init_logger2();
|
|
57036
57453
|
init_operations();
|
|
57037
57454
|
init_git();
|
|
57455
|
+
init_event_bus();
|
|
57038
57456
|
_acceptanceSetupDeps = {
|
|
57039
57457
|
getAgent: (_name) => {
|
|
57040
57458
|
return;
|
|
@@ -57134,189 +57552,19 @@ ${stderr}` };
|
|
|
57134
57552
|
if (!ctx.featureDir) {
|
|
57135
57553
|
return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
|
|
57136
57554
|
}
|
|
57137
|
-
const
|
|
57138
|
-
|
|
57139
|
-
|
|
57140
|
-
|
|
57141
|
-
|
|
57142
|
-
|
|
57143
|
-
|
|
57144
|
-
|
|
57145
|
-
|
|
57146
|
-
|
|
57147
|
-
const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
|
|
57148
|
-
getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
|
|
57149
|
-
currentFingerprint: fingerprint,
|
|
57150
|
-
storedFingerprint: meta3?.acFingerprint ?? "none",
|
|
57151
|
-
match: meta3?.acFingerprint === fingerprint
|
|
57152
|
-
});
|
|
57153
|
-
let shouldGenerate = false;
|
|
57154
|
-
if (!meta3 || meta3.acFingerprint !== fingerprint) {
|
|
57155
|
-
if (!meta3) {
|
|
57156
|
-
getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
|
|
57157
|
-
} else {
|
|
57158
|
-
getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
|
|
57159
|
-
reason: "fingerprint mismatch",
|
|
57160
|
-
currentFingerprint: fingerprint,
|
|
57161
|
-
storedFingerprint: meta3.acFingerprint
|
|
57162
|
-
});
|
|
57163
|
-
}
|
|
57164
|
-
for (const { testPath } of groups) {
|
|
57165
|
-
if (await _acceptanceSetupDeps.fileExists(testPath)) {
|
|
57166
|
-
await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
|
|
57167
|
-
await _acceptanceSetupDeps.deleteFile(testPath);
|
|
57168
|
-
}
|
|
57169
|
-
}
|
|
57170
|
-
await _acceptanceSetupDeps.deleteSemanticVerdicts(ctx.featureDir);
|
|
57171
|
-
shouldGenerate = true;
|
|
57172
|
-
} else {
|
|
57173
|
-
getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
|
|
57174
|
-
}
|
|
57175
|
-
if (shouldGenerate) {
|
|
57176
|
-
totalCriteria = allCriteria.length;
|
|
57177
|
-
let allRefinedCriteria;
|
|
57178
|
-
if (ctx.config.acceptance.refinement) {
|
|
57179
|
-
const maxConcurrency = ctx.config.acceptance.refinementConcurrency ?? 3;
|
|
57180
|
-
const results = new Array(nonFixStories.length);
|
|
57181
|
-
const executing = new Set;
|
|
57182
|
-
for (let i = 0;i < nonFixStories.length; i++) {
|
|
57183
|
-
const story = nonFixStories[i];
|
|
57184
|
-
const task = _acceptanceSetupDeps.callOp(ctx, ctx.workdir, acceptanceRefineOp, {
|
|
57185
|
-
criteria: story.acceptanceCriteria,
|
|
57186
|
-
codebaseContext: "",
|
|
57187
|
-
storyId: story.id,
|
|
57188
|
-
testStrategy: ctx.config.acceptance.testStrategy,
|
|
57189
|
-
testFramework: ctx.config.acceptance.testFramework,
|
|
57190
|
-
storyTitle: story.title,
|
|
57191
|
-
storyDescription: story.description
|
|
57192
|
-
}, story.id).then((refined) => {
|
|
57193
|
-
results[i] = refined;
|
|
57194
|
-
}).catch(() => {
|
|
57195
|
-
getSafeLogger()?.warn("acceptance-setup", "AC refinement failed after retries \u2014 using unrefined criteria", {
|
|
57196
|
-
storyId: story.id
|
|
57197
|
-
});
|
|
57198
|
-
results[i] = story.acceptanceCriteria.map((c) => ({
|
|
57199
|
-
original: c,
|
|
57200
|
-
refined: c,
|
|
57201
|
-
testable: true,
|
|
57202
|
-
storyId: story.id
|
|
57203
|
-
}));
|
|
57204
|
-
}).finally(() => {
|
|
57205
|
-
executing.delete(task);
|
|
57206
|
-
});
|
|
57207
|
-
executing.add(task);
|
|
57208
|
-
if (executing.size >= maxConcurrency) {
|
|
57209
|
-
await Promise.race(executing);
|
|
57210
|
-
}
|
|
57211
|
-
}
|
|
57212
|
-
await Promise.all(executing);
|
|
57213
|
-
allRefinedCriteria = results.flat();
|
|
57214
|
-
} else {
|
|
57215
|
-
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
57216
|
-
original: c,
|
|
57217
|
-
refined: c,
|
|
57218
|
-
testable: true,
|
|
57219
|
-
storyId: story.id
|
|
57220
|
-
})));
|
|
57221
|
-
}
|
|
57222
|
-
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
57223
|
-
for (const group of groups) {
|
|
57224
|
-
const { testPath, packageDir } = group;
|
|
57225
|
-
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
57226
|
-
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
57227
|
-
const criteriaList = groupRefined.map((c, i) => `AC-${i + 1}: ${c.refined}`).join(`
|
|
57228
|
-
`);
|
|
57229
|
-
const frameworkOverrideLine = ctx.config.acceptance.testFramework ? `
|
|
57230
|
-
[FRAMEWORK OVERRIDE: Use ${ctx.config.acceptance.testFramework} as the test framework regardless of what you detect.]` : "";
|
|
57231
|
-
const groupStoryId = group.stories[0]?.id;
|
|
57232
|
-
const genResult = await _acceptanceSetupDeps.callOp(ctx, packageDir, acceptanceGenerateOp, {
|
|
57233
|
-
featureName: featureName ?? "",
|
|
57234
|
-
criteriaList,
|
|
57235
|
-
frameworkOverrideLine,
|
|
57236
|
-
targetTestFilePath: testPath,
|
|
57237
|
-
..."implementationContext" in ctx && ctx.implementationContext ? { implementationContext: ctx.implementationContext } : {}
|
|
57238
|
-
}, groupStoryId);
|
|
57239
|
-
const testCode = genResult.testCode;
|
|
57240
|
-
if (testCode) {
|
|
57241
|
-
await _acceptanceSetupDeps.writeFile(testPath, testCode);
|
|
57242
|
-
} else {
|
|
57243
|
-
const skeletonCriteria = groupRefined.map((c, i) => ({
|
|
57244
|
-
id: `AC-${i + 1}`,
|
|
57245
|
-
text: c.refined,
|
|
57246
|
-
lineNumber: i + 1
|
|
57247
|
-
}));
|
|
57248
|
-
const skeletonCode = generateSkeletonTests(featureName, skeletonCriteria, ctx.config.acceptance.testFramework, group.language);
|
|
57249
|
-
await _acceptanceSetupDeps.writeFile(testPath, skeletonCode);
|
|
57250
|
-
getSafeLogger()?.warn("acceptance-setup", "agent did not produce test content; using skeleton", {
|
|
57251
|
-
storyId: groupStoryId,
|
|
57252
|
-
testPath
|
|
57253
|
-
});
|
|
57254
|
-
}
|
|
57255
|
-
}
|
|
57256
|
-
if (allRefinedCriteria.length > 0) {
|
|
57257
|
-
const refinedJsonContent = JSON.stringify(allRefinedCriteria.map((c, i) => ({
|
|
57258
|
-
acId: `AC-${i + 1}`,
|
|
57259
|
-
original: c.original,
|
|
57260
|
-
refined: c.refined,
|
|
57261
|
-
testable: c.testable,
|
|
57262
|
-
storyId: c.storyId
|
|
57263
|
-
})), null, 2);
|
|
57264
|
-
await _acceptanceSetupDeps.writeFile(path12.join(ctx.featureDir, "acceptance-refined.json"), refinedJsonContent);
|
|
57265
|
-
}
|
|
57266
|
-
const fingerprint2 = computeACFingerprint(allCriteria);
|
|
57267
|
-
await _acceptanceSetupDeps.writeMeta(metaPath, {
|
|
57268
|
-
generatedAt: new Date().toISOString(),
|
|
57269
|
-
acFingerprint: fingerprint2,
|
|
57270
|
-
storyCount: ctx.prd.userStories.length,
|
|
57271
|
-
acCount: totalCriteria,
|
|
57272
|
-
generator: "nax"
|
|
57273
|
-
});
|
|
57274
|
-
await _acceptanceSetupDeps.autoCommitIfDirty(ctx.workdir, "acceptance-setup", "pre-run", ctx.prd.feature ?? "feature");
|
|
57275
|
-
}
|
|
57276
|
-
const acceptanceTestPaths = [];
|
|
57277
|
-
for (const g of groups) {
|
|
57278
|
-
const relativeWorkdir = path12.relative(ctx.projectDir, g.packageDir);
|
|
57279
|
-
let groupConfig = ctx.config;
|
|
57280
|
-
if (relativeWorkdir && relativeWorkdir !== ".") {
|
|
57281
|
-
try {
|
|
57282
|
-
groupConfig = await _acceptanceSetupDeps.loadGroupConfig(ctx.projectDir, relativeWorkdir);
|
|
57283
|
-
} catch {
|
|
57284
|
-
groupConfig = ctx.config;
|
|
57285
|
-
}
|
|
57286
|
-
}
|
|
57287
|
-
acceptanceTestPaths.push({
|
|
57288
|
-
testPath: g.testPath,
|
|
57289
|
-
packageDir: g.packageDir,
|
|
57290
|
-
testFramework: groupConfig.project?.testFramework,
|
|
57291
|
-
commandOverride: groupConfig.acceptance.command
|
|
57292
|
-
});
|
|
57293
|
-
}
|
|
57294
|
-
ctx.acceptanceTestPaths = acceptanceTestPaths;
|
|
57295
|
-
if (ctx.config.acceptance.redGate === false) {
|
|
57296
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57297
|
-
return { action: "continue" };
|
|
57298
|
-
}
|
|
57299
|
-
let redFailCount = 0;
|
|
57300
|
-
for (const { testPath, packageDir, testFramework, commandOverride } of acceptanceTestPaths) {
|
|
57301
|
-
const runCmd = buildAcceptanceRunCommand(testPath, testFramework, commandOverride, packageDir);
|
|
57302
|
-
getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
|
|
57303
|
-
cmd: runCmd.join(" "),
|
|
57304
|
-
packageDir
|
|
57555
|
+
const phaseStartTime = Date.now();
|
|
57556
|
+
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance-setup" });
|
|
57557
|
+
try {
|
|
57558
|
+
return await runAcceptanceSetup(ctx, ctx.featureDir, phaseStartTime);
|
|
57559
|
+
} catch (err) {
|
|
57560
|
+
pipelineEventBus.emit({
|
|
57561
|
+
type: "postrun:phase:completed",
|
|
57562
|
+
phase: "acceptance-setup",
|
|
57563
|
+
passed: false,
|
|
57564
|
+
durationMs: Date.now() - phaseStartTime
|
|
57305
57565
|
});
|
|
57306
|
-
|
|
57307
|
-
if (exitCode !== 0) {
|
|
57308
|
-
redFailCount++;
|
|
57309
|
-
}
|
|
57310
|
-
}
|
|
57311
|
-
if (redFailCount === 0) {
|
|
57312
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
57313
|
-
return {
|
|
57314
|
-
action: "skip",
|
|
57315
|
-
reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
|
|
57316
|
-
};
|
|
57566
|
+
throw err;
|
|
57317
57567
|
}
|
|
57318
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
|
|
57319
|
-
return { action: "continue" };
|
|
57320
57568
|
}
|
|
57321
57569
|
};
|
|
57322
57570
|
});
|
|
@@ -57462,79 +57710,6 @@ async function appendProgress(featureDir, storyId, status, message) {
|
|
|
57462
57710
|
}
|
|
57463
57711
|
var init_progress = () => {};
|
|
57464
57712
|
|
|
57465
|
-
// src/pipeline/event-bus.ts
|
|
57466
|
-
class PipelineEventBus {
|
|
57467
|
-
subscribers = new Map;
|
|
57468
|
-
_pending = new Set;
|
|
57469
|
-
on(eventType, subscriber) {
|
|
57470
|
-
const list = this.subscribers.get(eventType) ?? [];
|
|
57471
|
-
list.push(subscriber);
|
|
57472
|
-
this.subscribers.set(eventType, list);
|
|
57473
|
-
return () => {
|
|
57474
|
-
const current = this.subscribers.get(eventType) ?? [];
|
|
57475
|
-
this.subscribers.set(eventType, current.filter((s) => s !== subscriber));
|
|
57476
|
-
};
|
|
57477
|
-
}
|
|
57478
|
-
onAll(subscriber) {
|
|
57479
|
-
const list = this.subscribers.get("*") ?? [];
|
|
57480
|
-
list.push(subscriber);
|
|
57481
|
-
this.subscribers.set("*", list);
|
|
57482
|
-
return () => {
|
|
57483
|
-
const current = this.subscribers.get("*") ?? [];
|
|
57484
|
-
this.subscribers.set("*", current.filter((s) => s !== subscriber));
|
|
57485
|
-
};
|
|
57486
|
-
}
|
|
57487
|
-
emit(event) {
|
|
57488
|
-
const logger = getLogger();
|
|
57489
|
-
const specific = this.subscribers.get(event.type) ?? [];
|
|
57490
|
-
const all = this.subscribers.get("*") ?? [];
|
|
57491
|
-
const targets = [...specific, ...all];
|
|
57492
|
-
for (const sub of targets) {
|
|
57493
|
-
try {
|
|
57494
|
-
const result = sub(event);
|
|
57495
|
-
if (result instanceof Promise) {
|
|
57496
|
-
const tracked = result.catch((err) => {
|
|
57497
|
-
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57498
|
-
});
|
|
57499
|
-
this._pending.add(tracked);
|
|
57500
|
-
tracked.finally(() => this._pending.delete(tracked));
|
|
57501
|
-
}
|
|
57502
|
-
} catch (err) {
|
|
57503
|
-
logger.warn("event-bus", `Subscriber threw on ${event.type}`, { error: String(err) });
|
|
57504
|
-
}
|
|
57505
|
-
}
|
|
57506
|
-
}
|
|
57507
|
-
async emitAsync(event) {
|
|
57508
|
-
const logger = getLogger();
|
|
57509
|
-
const specific = this.subscribers.get(event.type) ?? [];
|
|
57510
|
-
const all = this.subscribers.get("*") ?? [];
|
|
57511
|
-
const targets = [...specific, ...all];
|
|
57512
|
-
await Promise.allSettled(targets.map(async (sub) => {
|
|
57513
|
-
try {
|
|
57514
|
-
await sub(event);
|
|
57515
|
-
} catch (err) {
|
|
57516
|
-
logger.warn("event-bus", `Subscriber error on ${event.type}`, { error: String(err) });
|
|
57517
|
-
}
|
|
57518
|
-
}));
|
|
57519
|
-
}
|
|
57520
|
-
async drain() {
|
|
57521
|
-
if (this._pending.size === 0)
|
|
57522
|
-
return;
|
|
57523
|
-
await Promise.allSettled([...this._pending]);
|
|
57524
|
-
}
|
|
57525
|
-
clear() {
|
|
57526
|
-
this.subscribers.clear();
|
|
57527
|
-
}
|
|
57528
|
-
subscriberCount(eventType) {
|
|
57529
|
-
return (this.subscribers.get(eventType) ?? []).length;
|
|
57530
|
-
}
|
|
57531
|
-
}
|
|
57532
|
-
var pipelineEventBus;
|
|
57533
|
-
var init_event_bus = __esm(() => {
|
|
57534
|
-
init_logger2();
|
|
57535
|
-
pipelineEventBus = new PipelineEventBus;
|
|
57536
|
-
});
|
|
57537
|
-
|
|
57538
57713
|
// src/pipeline/stages/completion.ts
|
|
57539
57714
|
async function getDiffText(workdir, baseRef) {
|
|
57540
57715
|
if (!baseRef)
|
|
@@ -58326,11 +58501,11 @@ var init_rollback = __esm(() => {
|
|
|
58326
58501
|
});
|
|
58327
58502
|
|
|
58328
58503
|
// src/utils/paths.ts
|
|
58329
|
-
import { join as join49, relative as
|
|
58504
|
+
import { join as join49, relative as relative14, sep as sep4 } from "path";
|
|
58330
58505
|
function packageDirRelative(projectDir, workdir) {
|
|
58331
58506
|
if (!projectDir || !workdir || workdir === projectDir)
|
|
58332
58507
|
return;
|
|
58333
|
-
const rel =
|
|
58508
|
+
const rel = relative14(projectDir, workdir);
|
|
58334
58509
|
if (rel === ".." || rel.startsWith(`..${sep4}`))
|
|
58335
58510
|
return;
|
|
58336
58511
|
return rel && rel !== "." ? rel : undefined;
|
|
@@ -58346,6 +58521,9 @@ var init_paths3 = __esm(() => {
|
|
|
58346
58521
|
});
|
|
58347
58522
|
|
|
58348
58523
|
// src/execution/non-blocking-fix.ts
|
|
58524
|
+
function actionableAdvisoryFindings(findings) {
|
|
58525
|
+
return findings.filter((f) => f.actionRequired !== false);
|
|
58526
|
+
}
|
|
58349
58527
|
function shouldRunNonBlockingFix(cfg, advisoryCount) {
|
|
58350
58528
|
return cfg?.enabled === true && advisoryCount > 0;
|
|
58351
58529
|
}
|
|
@@ -58418,10 +58596,9 @@ async function runNonBlockingFix(args, overrides = {}) {
|
|
|
58418
58596
|
exhausted = true;
|
|
58419
58597
|
}
|
|
58420
58598
|
if (!exhausted) {
|
|
58421
|
-
|
|
58422
|
-
|
|
58423
|
-
|
|
58424
|
-
});
|
|
58599
|
+
const gateVerdict = args.keptTreeRegressed?.();
|
|
58600
|
+
if (gateVerdict?.regressed) {
|
|
58601
|
+
logGateRegression(logger, args.storyId, "kept tree regressed the full-suite gate \u2014 restoring (ADR-024 \xA73)", gateVerdict);
|
|
58425
58602
|
return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
|
|
58426
58603
|
}
|
|
58427
58604
|
const cap = args.cfg.sourceDiffCap;
|
|
@@ -58449,8 +58626,23 @@ async function runNonBlockingFix(args, overrides = {}) {
|
|
|
58449
58626
|
logger?.info("non-blocking-fix", "best-effort fix kept", { storyId: args.storyId });
|
|
58450
58627
|
return { ran: true, kept: true, restored: false };
|
|
58451
58628
|
}
|
|
58629
|
+
const exhaustedGateVerdict = args.keptTreeRegressed?.();
|
|
58630
|
+
if (exhaustedGateVerdict?.regressed) {
|
|
58631
|
+
logGateRegression(logger, args.storyId, "best-effort fix exhausted with the full-suite gate red", exhaustedGateVerdict);
|
|
58632
|
+
}
|
|
58452
58633
|
return restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger);
|
|
58453
58634
|
}
|
|
58635
|
+
function logGateRegression(logger, storyId, message, verdict) {
|
|
58636
|
+
logger?.info("non-blocking-fix", message, {
|
|
58637
|
+
storyId,
|
|
58638
|
+
regressedKeys: verdict.regressedKeys.slice(0, MAX_LOGGED_REGRESSED_KEYS),
|
|
58639
|
+
regressedKeyCount: verdict.regressedKeys.length,
|
|
58640
|
+
baselineKeySize: verdict.baselineKeySize,
|
|
58641
|
+
keyless: verdict.keyless,
|
|
58642
|
+
memoExcludedKeyCount: verdict.memoExcludedKeys.length,
|
|
58643
|
+
flakeTriageRan: false
|
|
58644
|
+
});
|
|
58645
|
+
}
|
|
58454
58646
|
async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot, phaseCostsSnapshot, logger) {
|
|
58455
58647
|
await _deps.rollbackToRef(args.workdir, restoreRef);
|
|
58456
58648
|
for (const key of Object.keys(args.phaseOutputs))
|
|
@@ -58464,7 +58656,7 @@ async function restoreToSnapshot(args, _deps, restoreRef, phaseOutputsSnapshot,
|
|
|
58464
58656
|
});
|
|
58465
58657
|
return { ran: true, kept: false, restored: true };
|
|
58466
58658
|
}
|
|
58467
|
-
var REVIEW_PHASE_KINDS, _nonBlockingFixDeps, DEFAULT_DEPS;
|
|
58659
|
+
var REVIEW_PHASE_KINDS, MAX_LOGGED_REGRESSED_KEYS = 10, _nonBlockingFixDeps, DEFAULT_DEPS;
|
|
58468
58660
|
var init_non_blocking_fix = __esm(() => {
|
|
58469
58661
|
init_logger2();
|
|
58470
58662
|
init_rollback();
|
|
@@ -58603,17 +58795,41 @@ function gateFailureKeys(gateOutput) {
|
|
|
58603
58795
|
continue;
|
|
58604
58796
|
if (f.category === "flaky-test")
|
|
58605
58797
|
continue;
|
|
58606
|
-
keys.add(
|
|
58798
|
+
keys.add(gateFindingKey(f));
|
|
58607
58799
|
}
|
|
58608
58800
|
return keys;
|
|
58609
58801
|
}
|
|
58610
|
-
function
|
|
58611
|
-
|
|
58802
|
+
function gateFindingKey(finding) {
|
|
58803
|
+
return `${finding.file ?? ""}::${finding.rule ?? ""}`;
|
|
58804
|
+
}
|
|
58805
|
+
function isQuarantinedFlake(finding, quarantineMemo) {
|
|
58806
|
+
if (finding.source !== "test-runner")
|
|
58612
58807
|
return false;
|
|
58613
|
-
|
|
58614
|
-
|
|
58615
|
-
|
|
58616
|
-
|
|
58808
|
+
return quarantineMemo?.has(gateFindingKey(finding)) === true;
|
|
58809
|
+
}
|
|
58810
|
+
function describeGateRegression(input) {
|
|
58811
|
+
const { gateOutput, baselineKeys, gateName, storyId, quarantineMemo } = input;
|
|
58812
|
+
const notRegressed = {
|
|
58813
|
+
regressed: false,
|
|
58814
|
+
regressedKeys: [],
|
|
58815
|
+
memoExcludedKeys: [],
|
|
58816
|
+
baselineKeySize: baselineKeys.size,
|
|
58817
|
+
keyless: false
|
|
58818
|
+
};
|
|
58819
|
+
if (gateName === undefined || phasePassed(gateName, gateOutput, storyId))
|
|
58820
|
+
return notRegressed;
|
|
58821
|
+
const allKeys = gateFailureKeys(gateOutput);
|
|
58822
|
+
const memoExcludedKeys = quarantineMemo ? [...allKeys].filter((k) => quarantineMemo.has(k)) : [];
|
|
58823
|
+
const excluded = new Set(memoExcludedKeys);
|
|
58824
|
+
const regressedKeys = [...allKeys].filter((k) => !baselineKeys.has(k) && !excluded.has(k));
|
|
58825
|
+
const keyless = allKeys.size === 0 || allKeys.has(KEYLESS_GATE_FAILURE_KEY);
|
|
58826
|
+
return {
|
|
58827
|
+
regressed: regressedKeys.length > 0 || keyless,
|
|
58828
|
+
regressedKeys,
|
|
58829
|
+
memoExcludedKeys,
|
|
58830
|
+
baselineKeySize: baselineKeys.size,
|
|
58831
|
+
keyless
|
|
58832
|
+
};
|
|
58617
58833
|
}
|
|
58618
58834
|
function phasesToRevalidate(strategiesRun, allPhases) {
|
|
58619
58835
|
if (!strategiesRun || strategiesRun.length === 0)
|
|
@@ -58760,6 +58976,9 @@ function buildPhaseOutcomeLogData(storyId, opName, output, durationMs) {
|
|
|
58760
58976
|
const data = { storyId, phase: opName, durationMs };
|
|
58761
58977
|
if (findingsCount !== undefined)
|
|
58762
58978
|
data.findingsCount = findingsCount;
|
|
58979
|
+
const identities = extractPhaseFindings(output).slice(0, MAX_LOGGED_FINDING_IDENTITIES).map((f) => `${f.file ?? ""}::${f.rule ?? ""}`);
|
|
58980
|
+
if (identities.length > 0)
|
|
58981
|
+
data.findingIdentities = identities;
|
|
58763
58982
|
if (status !== undefined)
|
|
58764
58983
|
data.status = status;
|
|
58765
58984
|
if (typeof r.failureCategory === "string")
|
|
@@ -58791,8 +59010,10 @@ function logDeterministicPhaseOutcome(storyId, opName, output, durationMs, isTdd
|
|
|
58791
59010
|
logger?.warn("story-orchestrator", message, data);
|
|
58792
59011
|
}
|
|
58793
59012
|
}
|
|
59013
|
+
var MAX_LOGGED_FINDING_IDENTITIES = 10;
|
|
58794
59014
|
var init_story_orchestrator_logging = __esm(() => {
|
|
58795
59015
|
init_logger2();
|
|
59016
|
+
init_phase_eval();
|
|
58796
59017
|
});
|
|
58797
59018
|
// src/verification/flake-baseline-diff.ts
|
|
58798
59019
|
async function resolveFlakeBaselineDiff(config2, workdir, storyWorkdir) {
|
|
@@ -59065,10 +59286,13 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59065
59286
|
const beforeRef = isTddPhase ? await _storyOrchestratorDeps.captureGitRef(ctx.packageDir) : undefined;
|
|
59066
59287
|
let dispatchInput = isTddPhase && beforeRef ? { ...slot.input, beforeRef } : slot.input;
|
|
59067
59288
|
dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
|
|
59289
|
+
let advIterationBefore = 0;
|
|
59068
59290
|
if (opName === "adversarial-review" && ctx.storyId) {
|
|
59291
|
+
const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
|
|
59292
|
+
advIterationBefore = priorIterations.length;
|
|
59069
59293
|
dispatchInput = {
|
|
59070
59294
|
...dispatchInput,
|
|
59071
|
-
priorAdversarialIterations:
|
|
59295
|
+
priorAdversarialIterations: priorIterations
|
|
59072
59296
|
};
|
|
59073
59297
|
}
|
|
59074
59298
|
if (isTddPhase) {
|
|
@@ -59085,6 +59309,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59085
59309
|
}
|
|
59086
59310
|
const phaseStartedAt = Date.now();
|
|
59087
59311
|
const scope = ctx.runtime.costAggregator.openScope();
|
|
59312
|
+
let outcome = "passed";
|
|
59088
59313
|
try {
|
|
59089
59314
|
const output = await _storyOrchestratorDeps.callOp({ ...ctx, scopeId: scope.scopeId }, slot.op, dispatchInput);
|
|
59090
59315
|
phaseOutputs[opName] = output;
|
|
@@ -59098,6 +59323,7 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59098
59323
|
}
|
|
59099
59324
|
logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
|
|
59100
59325
|
logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
|
|
59326
|
+
outcome = derivePhaseOutcome(output);
|
|
59101
59327
|
if (isTddPhase) {
|
|
59102
59328
|
const durationMs = Date.now() - phaseStartedAt;
|
|
59103
59329
|
logger?.info("tdd", `Session complete: ${opName}`, {
|
|
@@ -59127,10 +59353,94 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
|
|
|
59127
59353
|
}
|
|
59128
59354
|
}
|
|
59129
59355
|
return output;
|
|
59356
|
+
} catch (err) {
|
|
59357
|
+
outcome = "error";
|
|
59358
|
+
throw err;
|
|
59130
59359
|
} finally {
|
|
59131
|
-
|
|
59360
|
+
const snapshot = scope.snapshot();
|
|
59361
|
+
phaseCosts[opName] = (phaseCosts[opName] ?? 0) + snapshot.totalCostUsd;
|
|
59132
59362
|
scope.close();
|
|
59363
|
+
if (ctx.storyId) {
|
|
59364
|
+
const phaseDetails = buildPhaseDetails(opName, phaseOutputs[opName], isThreeSession, ctx.packageView.config, advIterationBefore, ctx.fixStrategy);
|
|
59365
|
+
const event = {
|
|
59366
|
+
type: "story:phase:completed",
|
|
59367
|
+
storyId: ctx.storyId,
|
|
59368
|
+
phase: opName,
|
|
59369
|
+
outcome,
|
|
59370
|
+
durationMs: Date.now() - phaseStartedAt,
|
|
59371
|
+
costUsd: snapshot.totalCostUsd,
|
|
59372
|
+
...ctx.phaseTelemetry ? {
|
|
59373
|
+
tier: ctx.phaseTelemetry.tier,
|
|
59374
|
+
testStrategy: ctx.phaseTelemetry.testStrategy,
|
|
59375
|
+
sessionModel: ctx.phaseTelemetry.sessionModel
|
|
59376
|
+
} : {},
|
|
59377
|
+
...phaseDetails !== undefined ? { details: phaseDetails } : {}
|
|
59378
|
+
};
|
|
59379
|
+
pipelineEventBus.emit(event);
|
|
59380
|
+
}
|
|
59381
|
+
}
|
|
59382
|
+
}
|
|
59383
|
+
function buildPhaseDetails(opName, output, isThreeSession, config2, advIterationBefore, fixStrategy) {
|
|
59384
|
+
if (fixStrategy) {
|
|
59385
|
+
return { kind: "fix", strategy: fixStrategy.name, findingsBefore: fixStrategy.findingsBefore };
|
|
59386
|
+
}
|
|
59387
|
+
if (output === null || typeof output !== "object")
|
|
59388
|
+
return;
|
|
59389
|
+
if (opName === "adversarial-review") {
|
|
59390
|
+
const adv = output;
|
|
59391
|
+
const findings = adv?.normalizedFindings ?? [];
|
|
59392
|
+
const threshold = adv?.blockingThreshold ?? "error";
|
|
59393
|
+
const bySeverity = Object.fromEntries(ALL_FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length]));
|
|
59394
|
+
const blockingCount = findings.filter((f) => isBlockingSeverity(f.severity, threshold)).length;
|
|
59395
|
+
const advisoryCount = findings.length - blockingCount;
|
|
59396
|
+
return {
|
|
59397
|
+
kind: "review",
|
|
59398
|
+
reviewer: "adversarial",
|
|
59399
|
+
iteration: advIterationBefore,
|
|
59400
|
+
bySeverity,
|
|
59401
|
+
blockingCount,
|
|
59402
|
+
advisoryCount,
|
|
59403
|
+
...config2.reporters?.otel?.detail === "verbose" ? {
|
|
59404
|
+
items: findings.map((f) => ({
|
|
59405
|
+
message: f.message,
|
|
59406
|
+
severity: f.severity,
|
|
59407
|
+
...f.rule ? { rule: f.rule } : {},
|
|
59408
|
+
...f.file ? { file: f.file } : {}
|
|
59409
|
+
}))
|
|
59410
|
+
} : {}
|
|
59411
|
+
};
|
|
59412
|
+
}
|
|
59413
|
+
if (opName === "implementer") {
|
|
59414
|
+
const impl = output;
|
|
59415
|
+
const filesChanged = (impl?.filesChanged ?? []).length;
|
|
59416
|
+
if (isThreeSession) {
|
|
59417
|
+
return { kind: "authoring", role: "implementer", filesChanged, isolationPassed: impl?.isolation?.passed };
|
|
59418
|
+
}
|
|
59419
|
+
return { kind: "authoring", role: "implementer", filesChanged };
|
|
59420
|
+
}
|
|
59421
|
+
if (opName === "test-writer") {
|
|
59422
|
+
const tw = output;
|
|
59423
|
+
return { kind: "authoring", role: "test-writer", filesChanged: (tw?.filesChanged ?? []).length };
|
|
59424
|
+
}
|
|
59425
|
+
if (opName === "full-suite-gate") {
|
|
59426
|
+
const gate = output;
|
|
59427
|
+
return { kind: "gate", gate: "full-suite", failureCount: gate?.failureCount ?? 0 };
|
|
59428
|
+
}
|
|
59429
|
+
if (opName === "verifier" || opName === "verify-scoped") {
|
|
59430
|
+
const verdict = output;
|
|
59431
|
+
return { kind: "verdict", role: opName, passed: verdict.passed ?? false, failureCount: verdict.failureCount ?? 0 };
|
|
59133
59432
|
}
|
|
59433
|
+
return;
|
|
59434
|
+
}
|
|
59435
|
+
function derivePhaseOutcome(output) {
|
|
59436
|
+
const built = buildPhaseOutcomeLogData(undefined, "", output, 0);
|
|
59437
|
+
if (!built)
|
|
59438
|
+
return "passed";
|
|
59439
|
+
if (built.success)
|
|
59440
|
+
return "passed";
|
|
59441
|
+
if (built.data.status === "skipped")
|
|
59442
|
+
return "skipped";
|
|
59443
|
+
return "failed";
|
|
59134
59444
|
}
|
|
59135
59445
|
function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
|
|
59136
59446
|
if (!enabled)
|
|
@@ -59155,7 +59465,7 @@ function withIncreasingFailuresBail(strategies, enabled, consecutiveIncreases) {
|
|
|
59155
59465
|
}
|
|
59156
59466
|
}));
|
|
59157
59467
|
}
|
|
59158
|
-
var _storyOrchestratorDeps;
|
|
59468
|
+
var _storyOrchestratorDeps, ALL_FINDING_SEVERITIES;
|
|
59159
59469
|
var init_run_phase = __esm(() => {
|
|
59160
59470
|
init_findings();
|
|
59161
59471
|
init_logger2();
|
|
@@ -59185,12 +59495,23 @@ var init_run_phase = __esm(() => {
|
|
|
59185
59495
|
},
|
|
59186
59496
|
loadCheckpoints: async (_featureDir) => new Map
|
|
59187
59497
|
};
|
|
59498
|
+
ALL_FINDING_SEVERITIES = [
|
|
59499
|
+
"critical",
|
|
59500
|
+
"error",
|
|
59501
|
+
"warning",
|
|
59502
|
+
"info",
|
|
59503
|
+
"low",
|
|
59504
|
+
"unverifiable"
|
|
59505
|
+
];
|
|
59188
59506
|
});
|
|
59189
59507
|
|
|
59190
59508
|
// src/execution/story-orchestrator/rectification.ts
|
|
59191
|
-
function shouldSkipPhaseForRectification(
|
|
59509
|
+
function shouldSkipPhaseForRectification(input) {
|
|
59510
|
+
const { phase, state, phaseOutputs, nbfPath } = input;
|
|
59192
59511
|
if (phase.kind !== "full-suite-gate")
|
|
59193
59512
|
return false;
|
|
59513
|
+
if (nbfPath)
|
|
59514
|
+
return false;
|
|
59194
59515
|
const verifierName = state.verifier?.slot.op.name;
|
|
59195
59516
|
if (!verifierName)
|
|
59196
59517
|
return false;
|
|
@@ -59199,7 +59520,7 @@ function shouldSkipPhaseForRectification(phase, state, phaseOutputs) {
|
|
|
59199
59520
|
function gatherRectificationFindings(phaseOutputs, phases, state) {
|
|
59200
59521
|
const findings = [];
|
|
59201
59522
|
for (const phase of phases) {
|
|
59202
|
-
if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
|
|
59523
|
+
if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs }))
|
|
59203
59524
|
continue;
|
|
59204
59525
|
for (const f of extractPhaseFindings(phaseOutputs[phase.slot.op.name])) {
|
|
59205
59526
|
if (f.category === "flaky-test")
|
|
@@ -59280,7 +59601,9 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
|
|
|
59280
59601
|
return {};
|
|
59281
59602
|
}
|
|
59282
59603
|
let initialFindings;
|
|
59604
|
+
let nbfPath = false;
|
|
59283
59605
|
if (overrides?.initialFindings) {
|
|
59606
|
+
nbfPath = true;
|
|
59284
59607
|
initialFindings = [...overrides.initialFindings];
|
|
59285
59608
|
} else {
|
|
59286
59609
|
const gateName = state.fullSuiteGate?.slot.op.name;
|
|
@@ -59330,10 +59653,11 @@ async function runRectification(ctx, state, phaseCosts, phaseOutputs, overrides)
|
|
|
59330
59653
|
let shortCircuited = false;
|
|
59331
59654
|
for (const phase of phases) {
|
|
59332
59655
|
await runPhase(ctx, phase.slot, phaseCosts, phaseOutputs);
|
|
59333
|
-
if (shouldSkipPhaseForRectification(phase, state, phaseOutputs))
|
|
59656
|
+
if (shouldSkipPhaseForRectification({ phase, state, phaseOutputs, nbfPath }))
|
|
59334
59657
|
continue;
|
|
59335
59658
|
const output = phaseOutputs[phase.slot.op.name];
|
|
59336
|
-
|
|
59659
|
+
const phaseFindings = extractPhaseFindings(output);
|
|
59660
|
+
findings.push(...nbfPath ? phaseFindings.filter((f) => !isQuarantinedFlake(f, ctx.runtime.quarantineMemo)) : phaseFindings);
|
|
59337
59661
|
if (!phasePassed(phase.slot.op.name, output, ctx.storyId)) {
|
|
59338
59662
|
getSafeLogger()?.warn("story-orchestrator", "Short-circuiting revalidation on phase failure", {
|
|
59339
59663
|
storyId: ctx.storyId,
|
|
@@ -59408,6 +59732,15 @@ class ExecutionPlan {
|
|
|
59408
59732
|
this.state = state;
|
|
59409
59733
|
this.isThreeSession = isThreeSession;
|
|
59410
59734
|
}
|
|
59735
|
+
describeGateRegressionNow(phaseOutputs, gateName, baselineKeys) {
|
|
59736
|
+
return describeGateRegression({
|
|
59737
|
+
gateOutput: gateName === undefined ? undefined : phaseOutputs[gateName],
|
|
59738
|
+
baselineKeys,
|
|
59739
|
+
gateName,
|
|
59740
|
+
storyId: this.ctx.storyId,
|
|
59741
|
+
quarantineMemo: this.ctx.runtime.quarantineMemo
|
|
59742
|
+
});
|
|
59743
|
+
}
|
|
59411
59744
|
phaseNames() {
|
|
59412
59745
|
const names = collectOrderedPhases(this.state).map((p) => p.slot.op.name);
|
|
59413
59746
|
if (this.state.rectification) {
|
|
@@ -59544,7 +59877,7 @@ class ExecutionPlan {
|
|
|
59544
59877
|
const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
|
|
59545
59878
|
const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
|
|
59546
59879
|
const advisoryOut = phaseOutputs["adversarial-review"];
|
|
59547
|
-
const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
|
|
59880
|
+
const advisoryFindings = actionableAdvisoryFindings(advisoryOut?.advisoryFindings ?? []);
|
|
59548
59881
|
if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
|
|
59549
59882
|
await _storyOrchestratorDeps.runNonBlockingFix({
|
|
59550
59883
|
workdir: this.ctx.packageDir,
|
|
@@ -59561,7 +59894,7 @@ class ExecutionPlan {
|
|
|
59561
59894
|
maxAttempts,
|
|
59562
59895
|
postValidate: this.state.nonBlockingFixPostValidate
|
|
59563
59896
|
}),
|
|
59564
|
-
keptTreeRegressed: () =>
|
|
59897
|
+
keptTreeRegressed: () => this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys)
|
|
59565
59898
|
}, {
|
|
59566
59899
|
measureSourceDiff: createMeasureSourceDiff({
|
|
59567
59900
|
config: this.ctx.runtime.configLoader.current(),
|
|
@@ -59572,7 +59905,7 @@ class ExecutionPlan {
|
|
|
59572
59905
|
}
|
|
59573
59906
|
const verifierName = this.state.verifier?.slot.op.name;
|
|
59574
59907
|
const verifierExplicitlyPassed = verifierName !== undefined && phaseExplicitlyPassed(phaseOutputs[verifierName]);
|
|
59575
|
-
const gateRegressedDuringRect =
|
|
59908
|
+
const gateRegressedDuringRect = this.describeGateRegressionNow(phaseOutputs, gateName, preRectGateFailureKeys).regressed;
|
|
59576
59909
|
const verifierPassedSsot = verifierExplicitlyPassed && !gateRegressedDuringRect;
|
|
59577
59910
|
if (verifierExplicitlyPassed && gateRegressedDuringRect) {
|
|
59578
59911
|
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 });
|
|
@@ -59792,8 +60125,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
59792
60125
|
if (inputs.adversarialReview) {
|
|
59793
60126
|
builder.addAdversarialReview(inputs.adversarialReview);
|
|
59794
60127
|
}
|
|
59795
|
-
const
|
|
59796
|
-
const
|
|
60128
|
+
const repoRoot = ctx.packageDir;
|
|
60129
|
+
const packageDir = join50(repoRoot, story.workdir ?? "");
|
|
60130
|
+
const resolvedTestPatterns = await resolveTestFilePatterns(config2, repoRoot, story.workdir);
|
|
59797
60131
|
if (shouldRunRectification(config2) && inputs.rectification) {
|
|
59798
60132
|
const sink = makeDeclarationSink();
|
|
59799
60133
|
const strategies = [];
|
|
@@ -59829,7 +60163,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
59829
60163
|
files: h.files,
|
|
59830
60164
|
reasonDetail: h.reasonDetail
|
|
59831
60165
|
}));
|
|
59832
|
-
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir
|
|
60166
|
+
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
|
|
60167
|
+
repoRoot
|
|
60168
|
+
});
|
|
59833
60169
|
sink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
|
|
59834
60170
|
const allDeclarations = [...sink.testEdits, ...valid];
|
|
59835
60171
|
sink.testEdits = [];
|
|
@@ -59888,7 +60224,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
59888
60224
|
files: h.files,
|
|
59889
60225
|
reasonDetail: h.reasonDetail
|
|
59890
60226
|
}));
|
|
59891
|
-
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir
|
|
60227
|
+
const { valid, invalid } = await validateMockStructureFiles(pendingMock, resolvedTestPatterns, packageDir, {
|
|
60228
|
+
repoRoot
|
|
60229
|
+
});
|
|
59892
60230
|
nbSink.mockHandoffs = valid.map((d) => ({ files: d.files ?? [], reasonDetail: d.reasonDetail ?? "" }));
|
|
59893
60231
|
const allDeclarations = [...nbSink.testEdits, ...valid];
|
|
59894
60232
|
nbSink.testEdits = [];
|
|
@@ -60827,7 +61165,12 @@ var init_execution = __esm(() => {
|
|
|
60827
61165
|
featureName: ctx.prd.feature,
|
|
60828
61166
|
story: ctx.story,
|
|
60829
61167
|
...ctx.featureDir ? { featureDir: ctx.featureDir } : {},
|
|
60830
|
-
...interactionBridge ? { interactionBridge } : {}
|
|
61168
|
+
...interactionBridge ? { interactionBridge } : {},
|
|
61169
|
+
phaseTelemetry: {
|
|
61170
|
+
testStrategy: ctx.routing.testStrategy,
|
|
61171
|
+
sessionModel: isThreeSessionStrategy(ctx.routing.testStrategy) ? "three-session" : "single-session",
|
|
61172
|
+
tier: effectiveTier
|
|
61173
|
+
}
|
|
60831
61174
|
};
|
|
60832
61175
|
let capturedTokenUsage;
|
|
60833
61176
|
let capturedResponse = "";
|
|
@@ -61590,6 +61933,196 @@ var init_stages = __esm(() => {
|
|
|
61590
61933
|
preRunPipeline = [acceptanceSetupStage];
|
|
61591
61934
|
});
|
|
61592
61935
|
|
|
61936
|
+
// src/pipeline/subscribers/reporters.ts
|
|
61937
|
+
async function fanOutReporters(reporters, hook, invoke) {
|
|
61938
|
+
const logger = getSafeLogger();
|
|
61939
|
+
for (const reporter of reporters) {
|
|
61940
|
+
try {
|
|
61941
|
+
await invoke(reporter);
|
|
61942
|
+
} catch (err) {
|
|
61943
|
+
try {
|
|
61944
|
+
logger?.warn("plugins", `Reporter '${reporter.name}' ${hook} failed`, { error: err });
|
|
61945
|
+
} catch {}
|
|
61946
|
+
}
|
|
61947
|
+
}
|
|
61948
|
+
}
|
|
61949
|
+
function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
61950
|
+
const logger = getSafeLogger();
|
|
61951
|
+
const safe = (name, fn) => {
|
|
61952
|
+
return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
|
|
61953
|
+
};
|
|
61954
|
+
const unsubs = [];
|
|
61955
|
+
const phaseStart = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseStart", (reporter) => reporter.onPhaseStart?.(event));
|
|
61956
|
+
const phaseComplete = (event) => fanOutReporters(pluginRegistry.getReporters(), "onPhaseComplete", (reporter) => reporter.onPhaseComplete?.(event));
|
|
61957
|
+
unsubs.push(bus.on("story:step", (ev) => phaseStart({
|
|
61958
|
+
runId,
|
|
61959
|
+
scope: "story",
|
|
61960
|
+
storyId: ev.storyId,
|
|
61961
|
+
phase: ev.step,
|
|
61962
|
+
startTime: new Date().toISOString()
|
|
61963
|
+
})), bus.on("story:phase:completed", (phaseEvent) => phaseComplete({
|
|
61964
|
+
runId,
|
|
61965
|
+
scope: "story",
|
|
61966
|
+
storyId: phaseEvent.storyId,
|
|
61967
|
+
phase: phaseEvent.phase,
|
|
61968
|
+
outcome: phaseEvent.outcome,
|
|
61969
|
+
durationMs: phaseEvent.durationMs,
|
|
61970
|
+
costUsd: phaseEvent.costUsd,
|
|
61971
|
+
tier: phaseEvent.tier,
|
|
61972
|
+
testStrategy: phaseEvent.testStrategy,
|
|
61973
|
+
sessionModel: phaseEvent.sessionModel,
|
|
61974
|
+
details: phaseEvent.details
|
|
61975
|
+
})), bus.on("postrun:phase:started", (ev) => phaseStart({
|
|
61976
|
+
runId,
|
|
61977
|
+
scope: "run",
|
|
61978
|
+
phase: ev.phase,
|
|
61979
|
+
startTime: new Date().toISOString()
|
|
61980
|
+
})), bus.on("postrun:phase:completed", (phaseEvent) => phaseComplete({
|
|
61981
|
+
runId,
|
|
61982
|
+
scope: "run",
|
|
61983
|
+
phase: phaseEvent.phase,
|
|
61984
|
+
outcome: phaseEvent.passed ? "passed" : "failed",
|
|
61985
|
+
durationMs: phaseEvent.durationMs ?? 0,
|
|
61986
|
+
costUsd: phaseEvent.costUsd,
|
|
61987
|
+
details: phaseEvent.details
|
|
61988
|
+
})));
|
|
61989
|
+
unsubs.push(bus.on("run:started", (ev) => {
|
|
61990
|
+
return safe("onRunStart", async () => {
|
|
61991
|
+
const reporters = pluginRegistry.getReporters();
|
|
61992
|
+
for (const r of reporters) {
|
|
61993
|
+
if (r.onRunStart) {
|
|
61994
|
+
try {
|
|
61995
|
+
await r.onRunStart({
|
|
61996
|
+
runId,
|
|
61997
|
+
feature: ev.feature,
|
|
61998
|
+
totalStories: ev.totalStories,
|
|
61999
|
+
startTime: new Date(startTime).toISOString()
|
|
62000
|
+
});
|
|
62001
|
+
} catch (err) {
|
|
62002
|
+
logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
|
|
62003
|
+
}
|
|
62004
|
+
}
|
|
62005
|
+
}
|
|
62006
|
+
});
|
|
62007
|
+
}));
|
|
62008
|
+
unsubs.push(bus.on("story:completed", (ev) => {
|
|
62009
|
+
return safe("onStoryComplete(completed)", async () => {
|
|
62010
|
+
const reporters = pluginRegistry.getReporters();
|
|
62011
|
+
for (const r of reporters) {
|
|
62012
|
+
if (r.onStoryComplete) {
|
|
62013
|
+
try {
|
|
62014
|
+
await r.onStoryComplete({
|
|
62015
|
+
runId,
|
|
62016
|
+
storyId: ev.storyId,
|
|
62017
|
+
status: "completed",
|
|
62018
|
+
runElapsedMs: ev.runElapsedMs,
|
|
62019
|
+
cost: ev.cost ?? 0,
|
|
62020
|
+
tier: ev.modelTier ?? "balanced",
|
|
62021
|
+
testStrategy: ev.testStrategy ?? "test-after"
|
|
62022
|
+
});
|
|
62023
|
+
} catch (err) {
|
|
62024
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
62025
|
+
}
|
|
62026
|
+
}
|
|
62027
|
+
}
|
|
62028
|
+
});
|
|
62029
|
+
}));
|
|
62030
|
+
unsubs.push(bus.on("story:failed", (ev) => {
|
|
62031
|
+
return safe("onStoryComplete(failed)", async () => {
|
|
62032
|
+
const reporters = pluginRegistry.getReporters();
|
|
62033
|
+
for (const r of reporters) {
|
|
62034
|
+
if (r.onStoryComplete) {
|
|
62035
|
+
try {
|
|
62036
|
+
await r.onStoryComplete({
|
|
62037
|
+
runId,
|
|
62038
|
+
storyId: ev.storyId,
|
|
62039
|
+
status: "failed",
|
|
62040
|
+
runElapsedMs: Date.now() - startTime,
|
|
62041
|
+
cost: 0,
|
|
62042
|
+
tier: "balanced",
|
|
62043
|
+
testStrategy: "test-after"
|
|
62044
|
+
});
|
|
62045
|
+
} catch (err) {
|
|
62046
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
62047
|
+
}
|
|
62048
|
+
}
|
|
62049
|
+
}
|
|
62050
|
+
});
|
|
62051
|
+
}));
|
|
62052
|
+
unsubs.push(bus.on("story:paused", (ev) => {
|
|
62053
|
+
return safe("onStoryComplete(paused)", async () => {
|
|
62054
|
+
const reporters = pluginRegistry.getReporters();
|
|
62055
|
+
for (const r of reporters) {
|
|
62056
|
+
if (r.onStoryComplete) {
|
|
62057
|
+
try {
|
|
62058
|
+
await r.onStoryComplete({
|
|
62059
|
+
runId,
|
|
62060
|
+
storyId: ev.storyId,
|
|
62061
|
+
status: "paused",
|
|
62062
|
+
runElapsedMs: Date.now() - startTime,
|
|
62063
|
+
cost: 0,
|
|
62064
|
+
tier: "balanced",
|
|
62065
|
+
testStrategy: "test-after"
|
|
62066
|
+
});
|
|
62067
|
+
} catch (err) {
|
|
62068
|
+
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
62069
|
+
}
|
|
62070
|
+
}
|
|
62071
|
+
}
|
|
62072
|
+
});
|
|
62073
|
+
}));
|
|
62074
|
+
unsubs.push(bus.on("story:escalated", (ev) => {
|
|
62075
|
+
return safe("onEscalation", async () => {
|
|
62076
|
+
const reporters = pluginRegistry.getReporters();
|
|
62077
|
+
for (const r of reporters) {
|
|
62078
|
+
if (r.onEscalation) {
|
|
62079
|
+
try {
|
|
62080
|
+
await r.onEscalation({
|
|
62081
|
+
runId,
|
|
62082
|
+
storyId: ev.storyId,
|
|
62083
|
+
fromTier: ev.fromTier,
|
|
62084
|
+
toTier: ev.toTier
|
|
62085
|
+
});
|
|
62086
|
+
} catch (err) {
|
|
62087
|
+
logger?.warn("plugins", `Reporter '${r.name}' onEscalation failed`, { error: err });
|
|
62088
|
+
}
|
|
62089
|
+
}
|
|
62090
|
+
}
|
|
62091
|
+
});
|
|
62092
|
+
}));
|
|
62093
|
+
unsubs.push(bus.on("run:completed", (ev) => {
|
|
62094
|
+
return safe("onRunEnd", async () => {
|
|
62095
|
+
const reporters = pluginRegistry.getReporters();
|
|
62096
|
+
for (const r of reporters) {
|
|
62097
|
+
if (r.onRunEnd) {
|
|
62098
|
+
try {
|
|
62099
|
+
await r.onRunEnd({
|
|
62100
|
+
runId,
|
|
62101
|
+
totalDurationMs: Date.now() - startTime,
|
|
62102
|
+
totalCost: ev.totalCost ?? 0,
|
|
62103
|
+
storySummary: {
|
|
62104
|
+
completed: ev.passedStories,
|
|
62105
|
+
failed: ev.failedStories,
|
|
62106
|
+
skipped: ev.skippedStories,
|
|
62107
|
+
paused: ev.pausedStories
|
|
62108
|
+
}
|
|
62109
|
+
});
|
|
62110
|
+
} catch (err) {
|
|
62111
|
+
logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
|
|
62112
|
+
}
|
|
62113
|
+
}
|
|
62114
|
+
}
|
|
62115
|
+
});
|
|
62116
|
+
}));
|
|
62117
|
+
return () => {
|
|
62118
|
+
for (const u of unsubs)
|
|
62119
|
+
u();
|
|
62120
|
+
};
|
|
62121
|
+
}
|
|
62122
|
+
var init_reporters = __esm(() => {
|
|
62123
|
+
init_logger2();
|
|
62124
|
+
});
|
|
62125
|
+
|
|
61593
62126
|
// src/pipeline/index.ts
|
|
61594
62127
|
var init_pipeline = __esm(() => {
|
|
61595
62128
|
init_runner4();
|
|
@@ -61597,7 +62130,9 @@ var init_pipeline = __esm(() => {
|
|
|
61597
62130
|
init_stages();
|
|
61598
62131
|
init_queue_check();
|
|
61599
62132
|
init_execution_helpers();
|
|
62133
|
+
init_acceptance_setup();
|
|
61600
62134
|
init_event_bus();
|
|
62135
|
+
init_reporters();
|
|
61601
62136
|
});
|
|
61602
62137
|
|
|
61603
62138
|
// src/cli/prompts-shared.ts
|
|
@@ -64337,21 +64872,55 @@ var init_config2 = __esm(() => {
|
|
|
64337
64872
|
});
|
|
64338
64873
|
|
|
64339
64874
|
// src/plugins/builtin/nax-finish/telegram.ts
|
|
64875
|
+
function buildEscalationMessage(feature, reason, findings) {
|
|
64876
|
+
const head = `nax-finish escalated ${feature}: ${reason}`;
|
|
64877
|
+
if (findings.length === 0)
|
|
64878
|
+
return head;
|
|
64879
|
+
const footerReserve = `
|
|
64880
|
+
\u2026and ${findings.length} more`.length;
|
|
64881
|
+
const lines = [];
|
|
64882
|
+
let used = head.length;
|
|
64883
|
+
for (const f of findings) {
|
|
64884
|
+
const title = f.title.length > MAX_FINDING_TITLE_CHARS ? `${f.title.slice(0, MAX_FINDING_TITLE_CHARS)}\u2026` : f.title;
|
|
64885
|
+
const line = `
|
|
64886
|
+
- [${f.severity}] ${title}`;
|
|
64887
|
+
if (used + line.length + footerReserve > TELEGRAM_MAX_MESSAGE_CHARS)
|
|
64888
|
+
break;
|
|
64889
|
+
lines.push(line);
|
|
64890
|
+
used += line.length;
|
|
64891
|
+
}
|
|
64892
|
+
const omitted = findings.length - lines.length;
|
|
64893
|
+
return `${head}${lines.join("")}${omitted > 0 ? `
|
|
64894
|
+
\u2026and ${omitted} more` : ""}`;
|
|
64895
|
+
}
|
|
64340
64896
|
async function sendTelegramNotify(cfg, text) {
|
|
64341
64897
|
const res = await _telegramDeps.fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
|
|
64342
64898
|
method: "POST",
|
|
64343
64899
|
headers: { "content-type": "application/json" },
|
|
64344
|
-
body: JSON.stringify({ chat_id: cfg.chatId, text
|
|
64900
|
+
body: JSON.stringify({ chat_id: cfg.chatId, text })
|
|
64345
64901
|
});
|
|
64346
64902
|
return res.ok;
|
|
64347
64903
|
}
|
|
64348
|
-
var _telegramDeps;
|
|
64904
|
+
var TELEGRAM_MAX_MESSAGE_CHARS = 4096, MAX_FINDING_TITLE_CHARS = 120, _telegramDeps;
|
|
64349
64905
|
var init_telegram2 = __esm(() => {
|
|
64350
64906
|
_telegramDeps = { fetch: (...a) => fetch(...a) };
|
|
64351
64907
|
});
|
|
64352
64908
|
|
|
64353
64909
|
// src/plugins/builtin/nax-finish/index.ts
|
|
64354
64910
|
import * as path21 from "path";
|
|
64911
|
+
function logTail(stream) {
|
|
64912
|
+
if (stream.length <= LOG_TAIL_CHARS)
|
|
64913
|
+
return stream;
|
|
64914
|
+
return `[\u2026${stream.length - LOG_TAIL_CHARS} chars truncated\u2026]
|
|
64915
|
+
${stream.slice(-LOG_TAIL_CHARS)}`;
|
|
64916
|
+
}
|
|
64917
|
+
function stderrTail(stderr) {
|
|
64918
|
+
const trimmed = stderr.trim();
|
|
64919
|
+
if (!trimmed)
|
|
64920
|
+
return "";
|
|
64921
|
+
const tail = trimmed.length > STDERR_TAIL_CHARS ? `\u2026${trimmed.slice(-STDERR_TAIL_CHARS)}` : trimmed;
|
|
64922
|
+
return tail.replace(/\s+/g, " ");
|
|
64923
|
+
}
|
|
64355
64924
|
async function defaultRun2(cmd, opts) {
|
|
64356
64925
|
const proc = Bun.spawn(cmd, { cwd: opts.cwd, env: opts.env, stdout: "pipe", stderr: "pipe" });
|
|
64357
64926
|
let timedOut = false;
|
|
@@ -64425,7 +64994,7 @@ function buildFlowEnv(cfg) {
|
|
|
64425
64994
|
env2.NAX_FINISH_QUALITY_PROFILE = cfg.reviewers.quality;
|
|
64426
64995
|
return env2;
|
|
64427
64996
|
}
|
|
64428
|
-
var PLUGIN_NAME4 = "nax-finish", PLUGIN_VERSION4 = "0.1.0", PACKAGE_ROOT_SEARCH_DEPTH = 6, _naxFinishDeps, naxFinishAction, naxFinishPlugin;
|
|
64997
|
+
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;
|
|
64429
64998
|
var init_nax_finish = __esm(() => {
|
|
64430
64999
|
init_config2();
|
|
64431
65000
|
init_telegram2();
|
|
@@ -64476,10 +65045,43 @@ var init_nax_finish = __esm(() => {
|
|
|
64476
65045
|
});
|
|
64477
65046
|
const result = await _naxFinishDeps.readResult(ctx.workdir);
|
|
64478
65047
|
if (!result) {
|
|
64479
|
-
|
|
65048
|
+
ctx.logger.warn("nax-finish flow produced no result file", {
|
|
65049
|
+
exitCode: res.exitCode,
|
|
65050
|
+
stdout: logTail(res.stdout),
|
|
65051
|
+
stderr: logTail(res.stderr)
|
|
65052
|
+
});
|
|
65053
|
+
const tail = stderrTail(res.stderr);
|
|
65054
|
+
return {
|
|
65055
|
+
success: false,
|
|
65056
|
+
message: `nax-finish flow exited ${res.exitCode} (no result file)${tail ? `: ${tail}` : ""}`
|
|
65057
|
+
};
|
|
64480
65058
|
}
|
|
64481
|
-
if (result.status === "escalated"
|
|
64482
|
-
|
|
65059
|
+
if (result.status === "escalated") {
|
|
65060
|
+
const problems = [];
|
|
65061
|
+
let delivered = !escalateTelegram && !result.deliveryError;
|
|
65062
|
+
if (escalateTelegram && creds) {
|
|
65063
|
+
const sent = await _naxFinishDeps.notify(creds, buildEscalationMessage(result.feature, result.escalationReason ?? "", result.findings ?? []));
|
|
65064
|
+
if (sent)
|
|
65065
|
+
delivered = true;
|
|
65066
|
+
else
|
|
65067
|
+
problems.push("Telegram rejected the message");
|
|
65068
|
+
}
|
|
65069
|
+
if (!delivered) {
|
|
65070
|
+
if (result.deliveryError)
|
|
65071
|
+
problems.push(`the flow could not post it: ${result.deliveryError}`);
|
|
65072
|
+
if (problems.length === 0)
|
|
65073
|
+
problems.push("no escalation channel was reachable");
|
|
65074
|
+
ctx.logger.warn("nax-finish escalation was not delivered", {
|
|
65075
|
+
feature: result.feature,
|
|
65076
|
+
reasons: problems,
|
|
65077
|
+
escalationReason: result.escalationReason
|
|
65078
|
+
});
|
|
65079
|
+
return {
|
|
65080
|
+
success: false,
|
|
65081
|
+
message: `nax-finish: escalated but undelivered \u2014 ${problems.join("; ")}`,
|
|
65082
|
+
url: result.url
|
|
65083
|
+
};
|
|
65084
|
+
}
|
|
64483
65085
|
}
|
|
64484
65086
|
return { success: true, message: `nax-finish: ${result.status}`, url: result.url };
|
|
64485
65087
|
} catch (err) {
|
|
@@ -64554,28 +65156,119 @@ var init_reporter_shared = __esm(() => {
|
|
|
64554
65156
|
init_post_json();
|
|
64555
65157
|
});
|
|
64556
65158
|
|
|
64557
|
-
// src/plugins/builtin/otel-reporter/
|
|
64558
|
-
function
|
|
64559
|
-
const
|
|
64560
|
-
|
|
64561
|
-
let
|
|
64562
|
-
|
|
64563
|
-
|
|
64564
|
-
|
|
65159
|
+
// src/plugins/builtin/otel-reporter/batch-queue.ts
|
|
65160
|
+
function createBatchQueue(opts) {
|
|
65161
|
+
const { maxBatchSize, flushIntervalMs, maxQueueSize, send } = opts;
|
|
65162
|
+
let queue = [];
|
|
65163
|
+
let dropCount = 0;
|
|
65164
|
+
let overflowing = false;
|
|
65165
|
+
let tornDown = false;
|
|
65166
|
+
let timer;
|
|
65167
|
+
const armTimer = () => {
|
|
65168
|
+
timer = setTimeout(() => {
|
|
65169
|
+
doFlush();
|
|
65170
|
+
if (!tornDown)
|
|
65171
|
+
armTimer();
|
|
65172
|
+
}, flushIntervalMs);
|
|
65173
|
+
};
|
|
65174
|
+
const sendWithRetry = async (batch) => {
|
|
65175
|
+
for (let attempt = 0;attempt < RETRY_ATTEMPTS; attempt++) {
|
|
65176
|
+
try {
|
|
65177
|
+
const ok = await send(batch);
|
|
65178
|
+
if (ok)
|
|
65179
|
+
return;
|
|
65180
|
+
} catch (err) {
|
|
65181
|
+
getSafeLogger()?.warn(STAGE, "Batch export threw", { error: err instanceof Error ? err.message : String(err) });
|
|
65182
|
+
}
|
|
65183
|
+
}
|
|
65184
|
+
};
|
|
65185
|
+
const doFlush = () => {
|
|
65186
|
+
if (tornDown || queue.length === 0)
|
|
65187
|
+
return Promise.resolve();
|
|
65188
|
+
const batch = queue;
|
|
65189
|
+
queue = [];
|
|
65190
|
+
sendWithRetry(batch);
|
|
65191
|
+
return Promise.resolve();
|
|
65192
|
+
};
|
|
65193
|
+
const enqueue = (item) => {
|
|
65194
|
+
queue.push(item);
|
|
65195
|
+
if (queue.length > maxQueueSize) {
|
|
65196
|
+
queue.shift();
|
|
65197
|
+
dropCount++;
|
|
65198
|
+
if (!overflowing) {
|
|
65199
|
+
overflowing = true;
|
|
65200
|
+
getSafeLogger()?.warn(STAGE, "Batch queue overflow \u2014 dropping oldest entries", { maxQueueSize });
|
|
65201
|
+
}
|
|
65202
|
+
} else {
|
|
65203
|
+
overflowing = false;
|
|
65204
|
+
}
|
|
65205
|
+
if (queue.length >= maxBatchSize) {
|
|
65206
|
+
doFlush();
|
|
65207
|
+
}
|
|
65208
|
+
};
|
|
65209
|
+
armTimer();
|
|
65210
|
+
return {
|
|
65211
|
+
enqueue,
|
|
65212
|
+
flushNow: () => doFlush(),
|
|
65213
|
+
teardown: () => {
|
|
65214
|
+
tornDown = true;
|
|
65215
|
+
if (timer !== undefined)
|
|
65216
|
+
clearTimeout(timer);
|
|
65217
|
+
},
|
|
65218
|
+
getMetrics: () => ({ size: queue.length, dropCount })
|
|
65219
|
+
};
|
|
64565
65220
|
}
|
|
64566
|
-
var
|
|
65221
|
+
var STAGE = "otel-batch-queue", RETRY_ATTEMPTS = 2;
|
|
65222
|
+
var init_batch_queue = __esm(() => {
|
|
65223
|
+
init_logger2();
|
|
65224
|
+
});
|
|
64567
65225
|
|
|
64568
65226
|
// src/plugins/builtin/otel-reporter/otlp.ts
|
|
65227
|
+
import { hostname as hostname3 } from "os";
|
|
64569
65228
|
function attr(key, value) {
|
|
64570
65229
|
return typeof value === "number" ? { key, value: { doubleValue: value } } : { key, value: { stringValue: value } };
|
|
64571
65230
|
}
|
|
64572
65231
|
function msToUnixNano(ms) {
|
|
64573
65232
|
return (BigInt(Math.round(ms)) * 1000000n).toString();
|
|
64574
65233
|
}
|
|
65234
|
+
function buildHistogramPoint(values, bounds, attributes, timeUnixNano) {
|
|
65235
|
+
const bucketCounts = new Array(bounds.length + 1).fill(0);
|
|
65236
|
+
let sum = 0;
|
|
65237
|
+
for (const value of values) {
|
|
65238
|
+
sum += value;
|
|
65239
|
+
const bucketIndex = bounds.findIndex((bound) => value <= bound);
|
|
65240
|
+
bucketCounts[bucketIndex === -1 ? bounds.length : bucketIndex]++;
|
|
65241
|
+
}
|
|
65242
|
+
return { attributes, timeUnixNano, count: values.length, sum, bucketCounts, explicitBounds: bounds };
|
|
65243
|
+
}
|
|
65244
|
+
function buildCounterPoint(count, attributes, timeUnixNano) {
|
|
65245
|
+
return { attributes, timeUnixNano, asInt: String(count) };
|
|
65246
|
+
}
|
|
65247
|
+
function buildResourceAttributes(input) {
|
|
65248
|
+
const attrs = [
|
|
65249
|
+
attr("service.name", input.serviceName),
|
|
65250
|
+
attr("nax.run_id", input.runId),
|
|
65251
|
+
attr("nax.version", NAX_VERSION),
|
|
65252
|
+
attr("process.pid", process.pid)
|
|
65253
|
+
];
|
|
65254
|
+
try {
|
|
65255
|
+
attrs.push(attr("host.name", hostname3()));
|
|
65256
|
+
} catch {}
|
|
65257
|
+
if (input.feature !== undefined)
|
|
65258
|
+
attrs.push(attr("nax.feature", input.feature));
|
|
65259
|
+
if (input.project !== undefined)
|
|
65260
|
+
attrs.push(attr("nax.project", input.project));
|
|
65261
|
+
if (input.git?.branch !== undefined)
|
|
65262
|
+
attrs.push(attr("nax.git.branch", input.git.branch));
|
|
65263
|
+
if (input.git?.sha !== undefined)
|
|
65264
|
+
attrs.push(attr("nax.git.sha", input.git.sha));
|
|
65265
|
+
return attrs;
|
|
65266
|
+
}
|
|
64575
65267
|
function buildTracesPayload(p) {
|
|
64576
65268
|
const span = {
|
|
64577
65269
|
traceId: p.traceId,
|
|
64578
65270
|
spanId: p.spanId,
|
|
65271
|
+
...p.parentSpanId ? { parentSpanId: p.parentSpanId } : {},
|
|
64579
65272
|
name: "nax.run",
|
|
64580
65273
|
kind: 1,
|
|
64581
65274
|
startTimeUnixNano: p.startUnixNano,
|
|
@@ -64595,8 +65288,19 @@ function buildTracesPayload(p) {
|
|
|
64595
65288
|
return {
|
|
64596
65289
|
resourceSpans: [
|
|
64597
65290
|
{
|
|
64598
|
-
resource: {
|
|
64599
|
-
|
|
65291
|
+
resource: {
|
|
65292
|
+
attributes: buildResourceAttributes({
|
|
65293
|
+
serviceName: p.serviceName,
|
|
65294
|
+
runId: p.runId,
|
|
65295
|
+
feature: p.feature,
|
|
65296
|
+
project: p.project,
|
|
65297
|
+
git: {
|
|
65298
|
+
branch: p.gitBranch,
|
|
65299
|
+
sha: p.gitSha
|
|
65300
|
+
}
|
|
65301
|
+
})
|
|
65302
|
+
},
|
|
65303
|
+
scopeSpans: [{ scope: { name: "nax" }, spans: [span, ...p.extraSpans ?? []] }]
|
|
64600
65304
|
}
|
|
64601
65305
|
]
|
|
64602
65306
|
};
|
|
@@ -64622,7 +65326,18 @@ function buildMetricsPayload(p) {
|
|
|
64622
65326
|
return {
|
|
64623
65327
|
resourceMetrics: [
|
|
64624
65328
|
{
|
|
64625
|
-
resource: {
|
|
65329
|
+
resource: {
|
|
65330
|
+
attributes: buildResourceAttributes({
|
|
65331
|
+
serviceName: p.serviceName,
|
|
65332
|
+
runId: p.runId,
|
|
65333
|
+
feature: p.feature,
|
|
65334
|
+
project: p.project,
|
|
65335
|
+
git: {
|
|
65336
|
+
branch: p.gitBranch,
|
|
65337
|
+
sha: p.gitSha
|
|
65338
|
+
}
|
|
65339
|
+
})
|
|
65340
|
+
},
|
|
64626
65341
|
scopeMetrics: [
|
|
64627
65342
|
{
|
|
64628
65343
|
scope: { name: "nax" },
|
|
@@ -64633,17 +65348,482 @@ function buildMetricsPayload(p) {
|
|
|
64633
65348
|
]
|
|
64634
65349
|
};
|
|
64635
65350
|
}
|
|
65351
|
+
var init_otlp = __esm(() => {
|
|
65352
|
+
init_version();
|
|
65353
|
+
});
|
|
65354
|
+
|
|
65355
|
+
// src/plugins/builtin/otel-reporter/heartbeat.ts
|
|
65356
|
+
function startHeartbeat(opts) {
|
|
65357
|
+
const { intervalMs, getSnapshot, onTick } = opts;
|
|
65358
|
+
if (intervalMs <= 0)
|
|
65359
|
+
return { stop() {} };
|
|
65360
|
+
let stopped = false;
|
|
65361
|
+
let timer;
|
|
65362
|
+
const armTimer = () => {
|
|
65363
|
+
timer = setTimeout(() => {
|
|
65364
|
+
try {
|
|
65365
|
+
Promise.resolve(onTick(getSnapshot())).catch((err) => getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
|
|
65366
|
+
error: err instanceof Error ? err.message : String(err)
|
|
65367
|
+
}));
|
|
65368
|
+
} catch (err) {
|
|
65369
|
+
getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
|
|
65370
|
+
error: err instanceof Error ? err.message : String(err)
|
|
65371
|
+
});
|
|
65372
|
+
}
|
|
65373
|
+
if (!stopped)
|
|
65374
|
+
armTimer();
|
|
65375
|
+
}, intervalMs);
|
|
65376
|
+
};
|
|
65377
|
+
armTimer();
|
|
65378
|
+
return {
|
|
65379
|
+
stop() {
|
|
65380
|
+
stopped = true;
|
|
65381
|
+
if (timer !== undefined)
|
|
65382
|
+
clearTimeout(timer);
|
|
65383
|
+
}
|
|
65384
|
+
};
|
|
65385
|
+
}
|
|
65386
|
+
function heartbeatAttributes(a) {
|
|
65387
|
+
return [
|
|
65388
|
+
attr("run_id", a.runId),
|
|
65389
|
+
attr("feature", a.feature),
|
|
65390
|
+
attr("project", a.project),
|
|
65391
|
+
attr("story_id", a.storyId),
|
|
65392
|
+
attr("phase", a.phase),
|
|
65393
|
+
attr("tier", a.tier),
|
|
65394
|
+
attr("test_strategy", a.testStrategy)
|
|
65395
|
+
];
|
|
65396
|
+
}
|
|
65397
|
+
function buildHeartbeatMetricsPayload(p) {
|
|
65398
|
+
const attributes = heartbeatAttributes(p.snapshot.attributes);
|
|
65399
|
+
const gauge = (name, value) => ({
|
|
65400
|
+
name,
|
|
65401
|
+
gauge: { dataPoints: [{ asDouble: value, timeUnixNano: p.timeUnixNano, attributes }] }
|
|
65402
|
+
});
|
|
65403
|
+
return {
|
|
65404
|
+
resourceMetrics: [
|
|
65405
|
+
{
|
|
65406
|
+
resource: {
|
|
65407
|
+
attributes: buildResourceAttributes({
|
|
65408
|
+
serviceName: p.serviceName,
|
|
65409
|
+
runId: p.snapshot.attributes.runId
|
|
65410
|
+
})
|
|
65411
|
+
},
|
|
65412
|
+
scopeMetrics: [
|
|
65413
|
+
{
|
|
65414
|
+
scope: { name: "nax" },
|
|
65415
|
+
metrics: [
|
|
65416
|
+
gauge("nax.run.active", 1),
|
|
65417
|
+
gauge("nax.run.phase_elapsed_ms", p.snapshot.phaseElapsedMs),
|
|
65418
|
+
gauge("nax.run.cost_usd", p.snapshot.costUsd)
|
|
65419
|
+
]
|
|
65420
|
+
}
|
|
65421
|
+
]
|
|
65422
|
+
}
|
|
65423
|
+
]
|
|
65424
|
+
};
|
|
65425
|
+
}
|
|
65426
|
+
var STAGE2 = "otel-reporter-heartbeat";
|
|
65427
|
+
var init_heartbeat = __esm(() => {
|
|
65428
|
+
init_logger2();
|
|
65429
|
+
init_otlp();
|
|
65430
|
+
});
|
|
65431
|
+
|
|
65432
|
+
// src/plugins/builtin/otel-reporter/ids.ts
|
|
65433
|
+
function randomHex(bytes) {
|
|
65434
|
+
const arr = new Uint8Array(bytes);
|
|
65435
|
+
crypto.getRandomValues(arr);
|
|
65436
|
+
let out = "";
|
|
65437
|
+
for (const b of arr)
|
|
65438
|
+
out += b.toString(16).padStart(2, "0");
|
|
65439
|
+
return out;
|
|
65440
|
+
}
|
|
65441
|
+
var newTraceId = () => randomHex(16), newSpanId = () => randomHex(8);
|
|
65442
|
+
|
|
65443
|
+
// src/plugins/builtin/otel-reporter/logs.ts
|
|
65444
|
+
function entryTimestampMs(entry) {
|
|
65445
|
+
return new Date(entry.timestamp).getTime();
|
|
65446
|
+
}
|
|
65447
|
+
function toLogRecord(entry) {
|
|
65448
|
+
const timeUnixNano = msToUnixNano(entryTimestampMs(entry));
|
|
65449
|
+
const { number: severityNumber, text: severityText } = SEVERITY[entry.level];
|
|
65450
|
+
const attributes = [attr("nax.stage", entry.stage)];
|
|
65451
|
+
if (entry.storyId !== undefined)
|
|
65452
|
+
attributes.push(attr("nax.story_id", entry.storyId));
|
|
65453
|
+
if (entry.sessionRole !== undefined)
|
|
65454
|
+
attributes.push(attr("nax.session_role", entry.sessionRole));
|
|
65455
|
+
const data = entry.data ?? {};
|
|
65456
|
+
const nonScalars = {};
|
|
65457
|
+
for (const [key, value] of Object.entries(data)) {
|
|
65458
|
+
if (typeof value === "string") {
|
|
65459
|
+
attributes.push(attr(`nax.data.${key}`, truncate3(value)));
|
|
65460
|
+
} else if (typeof value === "number") {
|
|
65461
|
+
if (Number.isFinite(value)) {
|
|
65462
|
+
attributes.push(attr(`nax.data.${key}`, value));
|
|
65463
|
+
} else {
|
|
65464
|
+
nonScalars[key] = value;
|
|
65465
|
+
}
|
|
65466
|
+
} else if (typeof value === "boolean") {
|
|
65467
|
+
attributes.push(attr(`nax.data.${key}`, String(value)));
|
|
65468
|
+
} else {
|
|
65469
|
+
nonScalars[key] = value;
|
|
65470
|
+
}
|
|
65471
|
+
}
|
|
65472
|
+
if (Object.keys(nonScalars).length > 0) {
|
|
65473
|
+
attributes.push(attr("nax.data_json", truncate3(JSON.stringify(nonScalars))));
|
|
65474
|
+
}
|
|
65475
|
+
return {
|
|
65476
|
+
body: { stringValue: entry.message },
|
|
65477
|
+
timeUnixNano,
|
|
65478
|
+
severityNumber,
|
|
65479
|
+
severityText,
|
|
65480
|
+
attributes
|
|
65481
|
+
};
|
|
65482
|
+
}
|
|
65483
|
+
function buildLogsPayload(entries, resource) {
|
|
65484
|
+
const logRecords = entries.map(toLogRecord);
|
|
65485
|
+
return {
|
|
65486
|
+
resourceLogs: [
|
|
65487
|
+
{
|
|
65488
|
+
resource: {
|
|
65489
|
+
attributes: buildResourceAttributes(resource)
|
|
65490
|
+
},
|
|
65491
|
+
scopeLogs: [{ scope: { name: "nax" }, logRecords }]
|
|
65492
|
+
}
|
|
65493
|
+
]
|
|
65494
|
+
};
|
|
65495
|
+
}
|
|
65496
|
+
function truncate3(value) {
|
|
65497
|
+
if (value.length <= DATA_JSON_MAX)
|
|
65498
|
+
return value;
|
|
65499
|
+
const marker = TRUNCATION_MARKER;
|
|
65500
|
+
const keep = DATA_JSON_MAX - marker.length;
|
|
65501
|
+
return `${value.slice(0, keep)}${marker}`;
|
|
65502
|
+
}
|
|
65503
|
+
var SEVERITY, DATA_JSON_MAX = 2048, TRUNCATION_MARKER = "...[truncated]";
|
|
65504
|
+
var init_logs = __esm(() => {
|
|
65505
|
+
init_otlp();
|
|
65506
|
+
SEVERITY = {
|
|
65507
|
+
silent: { number: 0, text: "SILENT" },
|
|
65508
|
+
error: { number: 17, text: "ERROR" },
|
|
65509
|
+
warn: { number: 13, text: "WARN" },
|
|
65510
|
+
info: { number: 9, text: "INFO" },
|
|
65511
|
+
debug: { number: 5, text: "DEBUG" }
|
|
65512
|
+
};
|
|
65513
|
+
});
|
|
65514
|
+
|
|
65515
|
+
// src/plugins/builtin/otel-reporter/span-tree.ts
|
|
65516
|
+
function createSpanTree(traceId, runSpanId) {
|
|
65517
|
+
const storySpanIds = new Map;
|
|
65518
|
+
function storySpanId(storyId) {
|
|
65519
|
+
let spanId = storySpanIds.get(storyId);
|
|
65520
|
+
if (!spanId) {
|
|
65521
|
+
spanId = newSpanId();
|
|
65522
|
+
storySpanIds.set(storyId, spanId);
|
|
65523
|
+
}
|
|
65524
|
+
return spanId;
|
|
65525
|
+
}
|
|
65526
|
+
function buildStorySpan(storyId, startUnixNano, endUnixNano) {
|
|
65527
|
+
return {
|
|
65528
|
+
traceId,
|
|
65529
|
+
spanId: storySpanId(storyId),
|
|
65530
|
+
parentSpanId: runSpanId,
|
|
65531
|
+
name: "nax.story",
|
|
65532
|
+
startTimeUnixNano: startUnixNano,
|
|
65533
|
+
endTimeUnixNano: endUnixNano,
|
|
65534
|
+
attributes: [attr("nax.story_id", storyId)]
|
|
65535
|
+
};
|
|
65536
|
+
}
|
|
65537
|
+
function buildPhaseSpan({ event, traceId: spanTraceId, startUnixNano, endUnixNano }) {
|
|
65538
|
+
const parentSpanId = event.scope === "story" && event.storyId !== undefined ? storySpanId(event.storyId) : runSpanId;
|
|
65539
|
+
const attributes = [attr("phase", event.phase), attr("outcome", event.outcome)];
|
|
65540
|
+
if (event.testStrategy)
|
|
65541
|
+
attributes.push(attr("nax.test_strategy", event.testStrategy));
|
|
65542
|
+
return {
|
|
65543
|
+
traceId: spanTraceId,
|
|
65544
|
+
spanId: newSpanId(),
|
|
65545
|
+
parentSpanId,
|
|
65546
|
+
name: "nax.phase",
|
|
65547
|
+
startTimeUnixNano: startUnixNano,
|
|
65548
|
+
endTimeUnixNano: endUnixNano,
|
|
65549
|
+
attributes
|
|
65550
|
+
};
|
|
65551
|
+
}
|
|
65552
|
+
return { traceId, runSpanId, storySpanId, buildStorySpan, buildPhaseSpan };
|
|
65553
|
+
}
|
|
65554
|
+
function counterKey(attributes) {
|
|
65555
|
+
return attributes.map((a) => `${a.key}=${a.value.stringValue ?? a.value.doubleValue}`).join("|");
|
|
65556
|
+
}
|
|
65557
|
+
function bumpCounter(groups, attributes, count) {
|
|
65558
|
+
const key = counterKey(attributes);
|
|
65559
|
+
const existing = groups.get(key);
|
|
65560
|
+
if (existing) {
|
|
65561
|
+
existing.count += count;
|
|
65562
|
+
} else {
|
|
65563
|
+
groups.set(key, { attributes, count });
|
|
65564
|
+
}
|
|
65565
|
+
}
|
|
65566
|
+
function createPhaseMetricsAggregator() {
|
|
65567
|
+
const phaseGroups = new Map;
|
|
65568
|
+
const reviewFindings = new Map;
|
|
65569
|
+
const fixIterations = new Map;
|
|
65570
|
+
const escalations = new Map;
|
|
65571
|
+
function recordPhase(event) {
|
|
65572
|
+
const attributes = [
|
|
65573
|
+
attr("phase", event.phase),
|
|
65574
|
+
attr("outcome", event.outcome),
|
|
65575
|
+
attr("tier", event.tier ?? "unknown"),
|
|
65576
|
+
attr("test_strategy", event.testStrategy ?? "unknown"),
|
|
65577
|
+
attr("session_model", event.sessionModel ?? "unknown")
|
|
65578
|
+
];
|
|
65579
|
+
const key = counterKey(attributes);
|
|
65580
|
+
let group = phaseGroups.get(key);
|
|
65581
|
+
if (!group) {
|
|
65582
|
+
group = { attributes, durations: [], costs: [] };
|
|
65583
|
+
phaseGroups.set(key, group);
|
|
65584
|
+
}
|
|
65585
|
+
group.durations.push(event.durationMs);
|
|
65586
|
+
if (event.costUsd !== undefined)
|
|
65587
|
+
group.costs.push(event.costUsd);
|
|
65588
|
+
}
|
|
65589
|
+
function recordReviewFindings(phase, severity2, count) {
|
|
65590
|
+
bumpCounter(reviewFindings, [attr("phase", phase), attr("severity", severity2)], count);
|
|
65591
|
+
}
|
|
65592
|
+
function recordFixIterations(phase, strategy, count) {
|
|
65593
|
+
bumpCounter(fixIterations, [attr("phase", phase), attr("strategy", strategy)], count);
|
|
65594
|
+
}
|
|
65595
|
+
function recordEscalation(toTier, count) {
|
|
65596
|
+
bumpCounter(escalations, [attr("to_tier", toTier)], count);
|
|
65597
|
+
}
|
|
65598
|
+
function buildMetricsPayload2(input) {
|
|
65599
|
+
const { serviceName, runId, timeUnixNano, feature, project, gitBranch, gitSha } = input;
|
|
65600
|
+
const groups = [...phaseGroups.values()];
|
|
65601
|
+
const counterMetric = (name, source) => ({
|
|
65602
|
+
name,
|
|
65603
|
+
sum: {
|
|
65604
|
+
aggregationTemporality: 2,
|
|
65605
|
+
isMonotonic: true,
|
|
65606
|
+
dataPoints: [...source.values()].map((g) => buildCounterPoint(g.count, g.attributes, timeUnixNano))
|
|
65607
|
+
}
|
|
65608
|
+
});
|
|
65609
|
+
const metrics = [];
|
|
65610
|
+
if (groups.length > 0) {
|
|
65611
|
+
metrics.push({
|
|
65612
|
+
name: "nax.phase.duration",
|
|
65613
|
+
histogram: {
|
|
65614
|
+
aggregationTemporality: 2,
|
|
65615
|
+
dataPoints: groups.map((g) => buildHistogramPoint(g.durations, PHASE_DURATION_BOUNDS, g.attributes, timeUnixNano))
|
|
65616
|
+
}
|
|
65617
|
+
});
|
|
65618
|
+
metrics.push({
|
|
65619
|
+
name: "nax.phase.cost_usd",
|
|
65620
|
+
histogram: {
|
|
65621
|
+
aggregationTemporality: 2,
|
|
65622
|
+
dataPoints: groups.map((g) => buildHistogramPoint(g.costs, PHASE_COST_BOUNDS, g.attributes, timeUnixNano))
|
|
65623
|
+
}
|
|
65624
|
+
});
|
|
65625
|
+
}
|
|
65626
|
+
if (reviewFindings.size > 0)
|
|
65627
|
+
metrics.push(counterMetric("nax.review.findings", reviewFindings));
|
|
65628
|
+
if (fixIterations.size > 0)
|
|
65629
|
+
metrics.push(counterMetric("nax.fix.iterations", fixIterations));
|
|
65630
|
+
if (escalations.size > 0)
|
|
65631
|
+
metrics.push(counterMetric("nax.escalations", escalations));
|
|
65632
|
+
return {
|
|
65633
|
+
resourceMetrics: [
|
|
65634
|
+
{
|
|
65635
|
+
resource: {
|
|
65636
|
+
attributes: buildResourceAttributes({
|
|
65637
|
+
serviceName,
|
|
65638
|
+
runId,
|
|
65639
|
+
feature,
|
|
65640
|
+
project,
|
|
65641
|
+
git: { branch: gitBranch, sha: gitSha }
|
|
65642
|
+
})
|
|
65643
|
+
},
|
|
65644
|
+
scopeMetrics: [{ scope: { name: "nax" }, metrics }]
|
|
65645
|
+
}
|
|
65646
|
+
]
|
|
65647
|
+
};
|
|
65648
|
+
}
|
|
65649
|
+
return { recordPhase, recordReviewFindings, recordFixIterations, recordEscalation, buildMetricsPayload: buildMetricsPayload2 };
|
|
65650
|
+
}
|
|
65651
|
+
var PHASE_DURATION_BOUNDS, PHASE_COST_BOUNDS;
|
|
65652
|
+
var init_span_tree = __esm(() => {
|
|
65653
|
+
init_otlp();
|
|
65654
|
+
PHASE_DURATION_BOUNDS = [100, 500, 1000, 5000, 15000, 60000, 300000, 900000];
|
|
65655
|
+
PHASE_COST_BOUNDS = [0.001, 0.01, 0.05, 0.1, 0.5, 1, 5];
|
|
65656
|
+
});
|
|
65657
|
+
|
|
65658
|
+
// src/plugins/builtin/otel-reporter/traceparent.ts
|
|
65659
|
+
function parseTraceparent(value) {
|
|
65660
|
+
if (!value)
|
|
65661
|
+
return null;
|
|
65662
|
+
const match = TRACEPARENT_PATTERN.exec(value);
|
|
65663
|
+
if (!match)
|
|
65664
|
+
return null;
|
|
65665
|
+
const [, traceId, spanId] = match;
|
|
65666
|
+
if (!traceId || !spanId || ALL_ZERO.test(traceId))
|
|
65667
|
+
return null;
|
|
65668
|
+
return { traceId, spanId };
|
|
65669
|
+
}
|
|
65670
|
+
var TRACEPARENT_PATTERN, ALL_ZERO;
|
|
65671
|
+
var init_traceparent = __esm(() => {
|
|
65672
|
+
TRACEPARENT_PATTERN = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/;
|
|
65673
|
+
ALL_ZERO = /^0+$/;
|
|
65674
|
+
});
|
|
64636
65675
|
|
|
64637
65676
|
// src/plugins/builtin/otel-reporter/index.ts
|
|
64638
|
-
function
|
|
65677
|
+
function rootSpanIdentity() {
|
|
65678
|
+
const adopted = parseTraceparent(process.env.TRACEPARENT);
|
|
65679
|
+
if (!adopted)
|
|
65680
|
+
return { traceId: newTraceId(), spanId: newSpanId() };
|
|
65681
|
+
return { traceId: adopted.traceId, spanId: newSpanId(), parentSpanId: adopted.spanId };
|
|
65682
|
+
}
|
|
65683
|
+
function heartbeatSnapshotOf(runId, st) {
|
|
65684
|
+
const last = st.lastPhase;
|
|
65685
|
+
return {
|
|
65686
|
+
attributes: {
|
|
65687
|
+
runId,
|
|
65688
|
+
feature: st.feature,
|
|
65689
|
+
project: st.project,
|
|
65690
|
+
storyId: last?.storyId ?? "",
|
|
65691
|
+
phase: last?.phase ?? "",
|
|
65692
|
+
tier: last?.tier ?? "",
|
|
65693
|
+
testStrategy: last?.testStrategy ?? ""
|
|
65694
|
+
},
|
|
65695
|
+
phaseElapsedMs: last ? Date.now() - last.atMs : 0,
|
|
65696
|
+
costUsd: st.costUsd
|
|
65697
|
+
};
|
|
65698
|
+
}
|
|
65699
|
+
function recordDetailMetrics(metrics, phase, details) {
|
|
65700
|
+
if (typeof details !== "object" || details === null)
|
|
65701
|
+
return;
|
|
65702
|
+
const record2 = details;
|
|
65703
|
+
if (record2.kind === "review" && typeof record2.bySeverity === "object" && record2.bySeverity !== null) {
|
|
65704
|
+
for (const [severity2, count] of Object.entries(record2.bySeverity)) {
|
|
65705
|
+
if (count > 0)
|
|
65706
|
+
metrics.recordReviewFindings(phase, severity2, count);
|
|
65707
|
+
}
|
|
65708
|
+
} else if (record2.kind === "fix" && typeof record2.strategy === "string") {
|
|
65709
|
+
metrics.recordFixIterations(phase, record2.strategy, 1);
|
|
65710
|
+
}
|
|
65711
|
+
}
|
|
65712
|
+
function reviewSpanEvents(details, timeUnixNano, verbose) {
|
|
65713
|
+
if (!verbose)
|
|
65714
|
+
return [];
|
|
65715
|
+
if (typeof details !== "object" || details === null)
|
|
65716
|
+
return [];
|
|
65717
|
+
const record2 = details;
|
|
65718
|
+
if (record2.kind !== "review" || !Array.isArray(record2.items))
|
|
65719
|
+
return [];
|
|
65720
|
+
return record2.items.map((item) => {
|
|
65721
|
+
const finding = typeof item === "object" && item !== null ? item : {};
|
|
65722
|
+
const attributes = [attr("message", String(finding.message ?? ""))];
|
|
65723
|
+
if (typeof finding.file === "string")
|
|
65724
|
+
attributes.push(attr("file", finding.file));
|
|
65725
|
+
return { timeUnixNano, name: "review.finding", attributes };
|
|
65726
|
+
});
|
|
65727
|
+
}
|
|
65728
|
+
function createOtelReporterPlugin(cfg, deps, workdir) {
|
|
64639
65729
|
const states = new Map;
|
|
64640
65730
|
const base = cfg.endpoint?.replace(/\/$/, "");
|
|
65731
|
+
let tornDown = false;
|
|
65732
|
+
const makeSendSpanBatch = (resourceAttrs) => async (batch) => {
|
|
65733
|
+
if (!base || batch.length === 0)
|
|
65734
|
+
return true;
|
|
65735
|
+
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
65736
|
+
if (missing.length > 0) {
|
|
65737
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
65738
|
+
return true;
|
|
65739
|
+
}
|
|
65740
|
+
const payload = {
|
|
65741
|
+
resourceSpans: [
|
|
65742
|
+
{
|
|
65743
|
+
resource: { attributes: resourceAttrs },
|
|
65744
|
+
scopeSpans: [{ scope: { name: "nax" }, spans: batch }]
|
|
65745
|
+
}
|
|
65746
|
+
]
|
|
65747
|
+
};
|
|
65748
|
+
return postJson(`${base}/v1/traces`, payload, {
|
|
65749
|
+
headers: resolved,
|
|
65750
|
+
timeoutMs: cfg.timeoutMs,
|
|
65751
|
+
stage: STAGE3,
|
|
65752
|
+
deps
|
|
65753
|
+
});
|
|
65754
|
+
};
|
|
65755
|
+
const makeSpanQueue = (resourceAttrs) => createBatchQueue({
|
|
65756
|
+
maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
|
|
65757
|
+
flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
65758
|
+
maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
65759
|
+
send: makeSendSpanBatch(resourceAttrs)
|
|
65760
|
+
});
|
|
65761
|
+
const makeSendLogsBatch = (resource) => async (batch) => {
|
|
65762
|
+
if (!base || batch.length === 0)
|
|
65763
|
+
return true;
|
|
65764
|
+
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
65765
|
+
if (missing.length > 0) {
|
|
65766
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
65767
|
+
return true;
|
|
65768
|
+
}
|
|
65769
|
+
const payload = buildLogsPayload(batch, {
|
|
65770
|
+
serviceName: resource.serviceName,
|
|
65771
|
+
runId: resource.runId,
|
|
65772
|
+
feature: resource.feature,
|
|
65773
|
+
project: resource.project,
|
|
65774
|
+
git: { branch: resource.gitBranch, sha: resource.gitSha }
|
|
65775
|
+
});
|
|
65776
|
+
return postJson(`${base}/v1/logs`, payload, {
|
|
65777
|
+
headers: resolved,
|
|
65778
|
+
timeoutMs: cfg.timeoutMs,
|
|
65779
|
+
stage: STAGE3,
|
|
65780
|
+
deps
|
|
65781
|
+
});
|
|
65782
|
+
};
|
|
65783
|
+
const makeLogsQueue = (resource) => createBatchQueue({
|
|
65784
|
+
maxBatchSize: cfg.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE,
|
|
65785
|
+
flushIntervalMs: cfg.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
65786
|
+
maxQueueSize: cfg.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
65787
|
+
send: makeSendLogsBatch(resource)
|
|
65788
|
+
});
|
|
65789
|
+
const buildOrphanState = (startMs) => {
|
|
65790
|
+
const identity = rootSpanIdentity();
|
|
65791
|
+
const orphanAttrs = buildResourceAttributes({ serviceName: cfg.serviceName, runId: "orphan" });
|
|
65792
|
+
return {
|
|
65793
|
+
...identity,
|
|
65794
|
+
startMs,
|
|
65795
|
+
feature: "",
|
|
65796
|
+
project: "",
|
|
65797
|
+
events: [],
|
|
65798
|
+
spanTree: createSpanTree(identity.traceId, identity.spanId),
|
|
65799
|
+
spanQueue: makeSpanQueue(orphanAttrs),
|
|
65800
|
+
metrics: createPhaseMetricsAggregator(),
|
|
65801
|
+
storyBounds: new Map,
|
|
65802
|
+
costUsd: 0,
|
|
65803
|
+
heartbeat: { stop() {} }
|
|
65804
|
+
};
|
|
65805
|
+
};
|
|
65806
|
+
const exportHeartbeat = async (snapshot) => {
|
|
65807
|
+
if (!base)
|
|
65808
|
+
return;
|
|
65809
|
+
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
65810
|
+
if (missing.length > 0) {
|
|
65811
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
65812
|
+
return;
|
|
65813
|
+
}
|
|
65814
|
+
const metrics = buildHeartbeatMetricsPayload({
|
|
65815
|
+
serviceName: cfg.serviceName,
|
|
65816
|
+
timeUnixNano: msToUnixNano(Date.now()),
|
|
65817
|
+
snapshot
|
|
65818
|
+
});
|
|
65819
|
+
await postJson(`${base}/v1/metrics`, metrics, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps });
|
|
65820
|
+
};
|
|
64641
65821
|
const flush = async (st, endMs, e) => {
|
|
64642
65822
|
if (!base)
|
|
64643
65823
|
return;
|
|
64644
65824
|
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
64645
65825
|
if (missing.length > 0) {
|
|
64646
|
-
getSafeLogger()?.warn(
|
|
65826
|
+
getSafeLogger()?.warn(STAGE3, "Skipping OTLP export \u2014 unresolved env vars", { missing });
|
|
64647
65827
|
return;
|
|
64648
65828
|
}
|
|
64649
65829
|
const startUnixNano = msToUnixNano(st.startMs);
|
|
@@ -64652,9 +65832,13 @@ function createOtelReporterPlugin(cfg, deps) {
|
|
|
64652
65832
|
serviceName: cfg.serviceName,
|
|
64653
65833
|
traceId: st.traceId,
|
|
64654
65834
|
spanId: st.spanId,
|
|
65835
|
+
parentSpanId: st.parentSpanId,
|
|
64655
65836
|
startUnixNano,
|
|
64656
65837
|
endUnixNano,
|
|
64657
65838
|
feature: st.feature,
|
|
65839
|
+
project: st.project,
|
|
65840
|
+
gitBranch: st.gitBranch,
|
|
65841
|
+
gitSha: st.gitSha,
|
|
64658
65842
|
runId: e.runId,
|
|
64659
65843
|
storySummary: e.storySummary,
|
|
64660
65844
|
totalCost: e.totalCost,
|
|
@@ -64664,24 +65848,99 @@ function createOtelReporterPlugin(cfg, deps) {
|
|
|
64664
65848
|
serviceName: cfg.serviceName,
|
|
64665
65849
|
runId: e.runId,
|
|
64666
65850
|
timeUnixNano: endUnixNano,
|
|
65851
|
+
feature: st.feature,
|
|
65852
|
+
project: st.project,
|
|
65853
|
+
gitBranch: st.gitBranch,
|
|
65854
|
+
gitSha: st.gitSha,
|
|
64667
65855
|
storySummary: e.storySummary,
|
|
64668
65856
|
totalCost: e.totalCost,
|
|
64669
65857
|
totalDurationMs: e.totalDurationMs
|
|
64670
65858
|
});
|
|
64671
|
-
const
|
|
65859
|
+
const aggMetrics = st.metrics.buildMetricsPayload({
|
|
65860
|
+
serviceName: cfg.serviceName,
|
|
65861
|
+
runId: e.runId,
|
|
65862
|
+
timeUnixNano: endUnixNano,
|
|
65863
|
+
feature: st.feature,
|
|
65864
|
+
project: st.project,
|
|
65865
|
+
gitBranch: st.gitBranch,
|
|
65866
|
+
gitSha: st.gitSha
|
|
65867
|
+
});
|
|
65868
|
+
metrics.resourceMetrics[0].scopeMetrics[0].metrics.push(...aggMetrics.resourceMetrics[0].scopeMetrics[0].metrics);
|
|
65869
|
+
const opts = { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE3, deps };
|
|
64672
65870
|
await postJson(`${base}/v1/traces`, traces, opts);
|
|
64673
65871
|
await postJson(`${base}/v1/metrics`, metrics, opts);
|
|
64674
65872
|
};
|
|
64675
65873
|
const reporter = {
|
|
64676
|
-
name:
|
|
65874
|
+
name: STAGE3,
|
|
64677
65875
|
async onRunStart(event) {
|
|
64678
|
-
|
|
64679
|
-
|
|
64680
|
-
|
|
64681
|
-
|
|
65876
|
+
const identity = rootSpanIdentity();
|
|
65877
|
+
const runId = event.runId;
|
|
65878
|
+
let gitBranch;
|
|
65879
|
+
let gitSha;
|
|
65880
|
+
if (base && workdir) {
|
|
65881
|
+
const [branchResult, shaResult] = await Promise.all([
|
|
65882
|
+
gitWithTimeout(["rev-parse", "--abbrev-ref", "HEAD"], workdir).catch(() => null),
|
|
65883
|
+
gitWithTimeout(["rev-parse", "HEAD"], workdir).catch(() => null)
|
|
65884
|
+
]);
|
|
65885
|
+
if (branchResult?.exitCode === 0) {
|
|
65886
|
+
const branch = branchResult.stdout.trim();
|
|
65887
|
+
if (branch && branch !== "HEAD")
|
|
65888
|
+
gitBranch = branch;
|
|
65889
|
+
}
|
|
65890
|
+
if (shaResult?.exitCode === 0) {
|
|
65891
|
+
const sha = shaResult.stdout.trim();
|
|
65892
|
+
if (sha)
|
|
65893
|
+
gitSha = sha;
|
|
65894
|
+
}
|
|
65895
|
+
}
|
|
65896
|
+
const resourceAttrs = buildResourceAttributes({
|
|
65897
|
+
serviceName: cfg.serviceName,
|
|
65898
|
+
runId,
|
|
64682
65899
|
feature: event.feature,
|
|
64683
|
-
|
|
65900
|
+
project: event.project,
|
|
65901
|
+
git: { branch: gitBranch, sha: gitSha }
|
|
64684
65902
|
});
|
|
65903
|
+
const state = {
|
|
65904
|
+
...identity,
|
|
65905
|
+
startMs: Date.parse(event.startTime),
|
|
65906
|
+
feature: event.feature,
|
|
65907
|
+
project: event.project ?? "",
|
|
65908
|
+
gitBranch,
|
|
65909
|
+
gitSha,
|
|
65910
|
+
events: [],
|
|
65911
|
+
spanTree: createSpanTree(identity.traceId, identity.spanId),
|
|
65912
|
+
spanQueue: makeSpanQueue(resourceAttrs),
|
|
65913
|
+
metrics: createPhaseMetricsAggregator(),
|
|
65914
|
+
storyBounds: new Map,
|
|
65915
|
+
costUsd: 0,
|
|
65916
|
+
heartbeat: startHeartbeat({
|
|
65917
|
+
intervalMs: cfg.heartbeatIntervalMs ?? 0,
|
|
65918
|
+
getSnapshot: () => heartbeatSnapshotOf(runId, state),
|
|
65919
|
+
onTick: (snapshot) => exportHeartbeat(snapshot)
|
|
65920
|
+
})
|
|
65921
|
+
};
|
|
65922
|
+
states.set(runId, state);
|
|
65923
|
+
if (cfg.logs?.enabled) {
|
|
65924
|
+
const logsQueue = makeLogsQueue({
|
|
65925
|
+
serviceName: cfg.serviceName,
|
|
65926
|
+
runId,
|
|
65927
|
+
feature: event.feature,
|
|
65928
|
+
project: event.project ?? "",
|
|
65929
|
+
gitBranch,
|
|
65930
|
+
gitSha
|
|
65931
|
+
});
|
|
65932
|
+
const floorKey = cfg.logs.level;
|
|
65933
|
+
const sank = (entry) => {
|
|
65934
|
+
if (REENTRY_STAGES.has(entry.stage))
|
|
65935
|
+
return;
|
|
65936
|
+
if (LOG_PRIORITY[entry.level] > LOG_PRIORITY[floorKey])
|
|
65937
|
+
return;
|
|
65938
|
+
logsQueue.enqueue(entry);
|
|
65939
|
+
};
|
|
65940
|
+
const addSinkFn = deps?.addSink ?? addSink;
|
|
65941
|
+
state.logsQueue = logsQueue;
|
|
65942
|
+
state.logUnsubscribe = addSinkFn(sank);
|
|
65943
|
+
}
|
|
64685
65944
|
},
|
|
64686
65945
|
async onStoryComplete(event) {
|
|
64687
65946
|
const st = states.get(event.runId);
|
|
@@ -64698,32 +65957,119 @@ function createOtelReporterPlugin(cfg, deps) {
|
|
|
64698
65957
|
attr("testStrategy", event.testStrategy)
|
|
64699
65958
|
]
|
|
64700
65959
|
});
|
|
65960
|
+
const bounds = st.storyBounds.get(event.storyId);
|
|
65961
|
+
if (bounds) {
|
|
65962
|
+
st.spanQueue.enqueue(st.spanTree.buildStorySpan(event.storyId, msToUnixNano(bounds.startMs), msToUnixNano(bounds.endMs)));
|
|
65963
|
+
st.storyBounds.delete(event.storyId);
|
|
65964
|
+
}
|
|
65965
|
+
},
|
|
65966
|
+
async onPhaseComplete(event) {
|
|
65967
|
+
const st = states.get(event.runId);
|
|
65968
|
+
if (!st)
|
|
65969
|
+
return;
|
|
65970
|
+
st.costUsd += event.costUsd ?? 0;
|
|
65971
|
+
st.lastPhase = {
|
|
65972
|
+
phase: event.phase,
|
|
65973
|
+
storyId: event.storyId ?? "",
|
|
65974
|
+
tier: event.tier ?? "",
|
|
65975
|
+
testStrategy: event.testStrategy ?? "",
|
|
65976
|
+
atMs: Date.now()
|
|
65977
|
+
};
|
|
65978
|
+
const endMs = Date.now();
|
|
65979
|
+
const startMs = endMs - event.durationMs;
|
|
65980
|
+
const endUnixNano = msToUnixNano(endMs);
|
|
65981
|
+
const span = st.spanTree.buildPhaseSpan({
|
|
65982
|
+
event,
|
|
65983
|
+
traceId: st.traceId,
|
|
65984
|
+
startUnixNano: msToUnixNano(startMs),
|
|
65985
|
+
endUnixNano
|
|
65986
|
+
});
|
|
65987
|
+
const events = reviewSpanEvents(event.details, endUnixNano, cfg.detail === "verbose");
|
|
65988
|
+
if (events.length > 0)
|
|
65989
|
+
span.events = events;
|
|
65990
|
+
st.spanQueue.enqueue(span);
|
|
65991
|
+
st.metrics.recordPhase(event);
|
|
65992
|
+
recordDetailMetrics(st.metrics, event.phase, event.details);
|
|
65993
|
+
if (event.scope === "story" && event.storyId !== undefined) {
|
|
65994
|
+
const bounds = st.storyBounds.get(event.storyId);
|
|
65995
|
+
st.storyBounds.set(event.storyId, {
|
|
65996
|
+
startMs: bounds ? Math.min(bounds.startMs, startMs) : startMs,
|
|
65997
|
+
endMs: bounds ? Math.max(bounds.endMs, endMs) : endMs
|
|
65998
|
+
});
|
|
65999
|
+
}
|
|
66000
|
+
},
|
|
66001
|
+
async onEscalation(event) {
|
|
66002
|
+
const st = states.get(event.runId);
|
|
66003
|
+
if (!st)
|
|
66004
|
+
return;
|
|
66005
|
+
st.metrics.recordEscalation(event.toTier, 1);
|
|
64701
66006
|
},
|
|
64702
66007
|
async onRunEnd(event) {
|
|
64703
66008
|
const existing = states.get(event.runId);
|
|
66009
|
+
existing?.heartbeat.stop();
|
|
64704
66010
|
const startMs = existing?.startMs ?? Date.now() - event.totalDurationMs;
|
|
64705
|
-
const st = existing ??
|
|
64706
|
-
traceId: newTraceId(),
|
|
64707
|
-
spanId: newSpanId(),
|
|
64708
|
-
startMs,
|
|
64709
|
-
feature: "",
|
|
64710
|
-
events: []
|
|
64711
|
-
};
|
|
66011
|
+
const st = existing ?? buildOrphanState(startMs);
|
|
64712
66012
|
states.delete(event.runId);
|
|
66013
|
+
await st.spanQueue.flushNow();
|
|
66014
|
+
st.spanQueue.teardown();
|
|
66015
|
+
if (st.logsQueue) {
|
|
66016
|
+
await st.logsQueue.flushNow();
|
|
66017
|
+
st.logsQueue.teardown();
|
|
66018
|
+
st.logUnsubscribe?.();
|
|
66019
|
+
}
|
|
64713
66020
|
await flush(st, startMs + event.totalDurationMs, event);
|
|
64714
66021
|
}
|
|
64715
66022
|
};
|
|
64716
66023
|
return {
|
|
64717
|
-
name:
|
|
66024
|
+
name: STAGE3,
|
|
64718
66025
|
version: "1.0.0",
|
|
64719
66026
|
provides: ["reporter"],
|
|
66027
|
+
async teardown() {
|
|
66028
|
+
if (tornDown)
|
|
66029
|
+
return;
|
|
66030
|
+
tornDown = true;
|
|
66031
|
+
const entries = [...states.entries()];
|
|
66032
|
+
states.clear();
|
|
66033
|
+
for (const [runId, st] of entries) {
|
|
66034
|
+
st.heartbeat.stop();
|
|
66035
|
+
await st.spanQueue.flushNow();
|
|
66036
|
+
st.spanQueue.teardown();
|
|
66037
|
+
if (st.logsQueue) {
|
|
66038
|
+
await st.logsQueue.flushNow();
|
|
66039
|
+
st.logsQueue.teardown();
|
|
66040
|
+
st.logUnsubscribe?.();
|
|
66041
|
+
}
|
|
66042
|
+
const endMs = Date.now();
|
|
66043
|
+
await flush(st, endMs, {
|
|
66044
|
+
runId,
|
|
66045
|
+
totalDurationMs: endMs - st.startMs,
|
|
66046
|
+
totalCost: st.costUsd,
|
|
66047
|
+
storySummary: { completed: 0, failed: 0, skipped: 0, paused: 0 }
|
|
66048
|
+
});
|
|
66049
|
+
}
|
|
66050
|
+
},
|
|
64720
66051
|
extensions: { reporter }
|
|
64721
66052
|
};
|
|
64722
66053
|
}
|
|
64723
|
-
var
|
|
66054
|
+
var STAGE3 = "otel-reporter", REENTRY_STAGE = "otel-batch-queue", REENTRY_STAGES, DEFAULT_MAX_BATCH_SIZE = 64, DEFAULT_FLUSH_INTERVAL_MS = 5000, DEFAULT_MAX_QUEUE_SIZE = 2048, LOG_PRIORITY;
|
|
64724
66055
|
var init_otel_reporter = __esm(() => {
|
|
64725
66056
|
init_logger2();
|
|
66057
|
+
init_git();
|
|
64726
66058
|
init_reporter_shared();
|
|
66059
|
+
init_batch_queue();
|
|
66060
|
+
init_heartbeat();
|
|
66061
|
+
init_logs();
|
|
66062
|
+
init_otlp();
|
|
66063
|
+
init_span_tree();
|
|
66064
|
+
init_traceparent();
|
|
66065
|
+
REENTRY_STAGES = new Set([STAGE3, REENTRY_STAGE]);
|
|
66066
|
+
LOG_PRIORITY = {
|
|
66067
|
+
silent: -1,
|
|
66068
|
+
error: 0,
|
|
66069
|
+
warn: 1,
|
|
66070
|
+
info: 2,
|
|
66071
|
+
debug: 3
|
|
66072
|
+
};
|
|
64727
66073
|
});
|
|
64728
66074
|
|
|
64729
66075
|
// src/plugins/builtin/webhook-reporter/index.ts
|
|
@@ -64734,25 +66080,27 @@ function createWebhookReporterPlugin(cfg, deps) {
|
|
|
64734
66080
|
return;
|
|
64735
66081
|
const { resolved, missing } = interpolateHeaders(cfg.headers);
|
|
64736
66082
|
if (missing.length > 0) {
|
|
64737
|
-
getSafeLogger()?.warn(
|
|
66083
|
+
getSafeLogger()?.warn(STAGE4, "Skipping webhook \u2014 unresolved env vars", { missing });
|
|
64738
66084
|
return;
|
|
64739
66085
|
}
|
|
64740
|
-
await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage:
|
|
66086
|
+
await postJson(cfg.url, { type, emittedAt: new Date().toISOString(), data }, { headers: resolved, timeoutMs: cfg.timeoutMs, stage: STAGE4, deps });
|
|
64741
66087
|
};
|
|
64742
66088
|
const reporter = {
|
|
64743
|
-
name:
|
|
66089
|
+
name: STAGE4,
|
|
64744
66090
|
onRunStart: (event) => emit("onRunStart", event),
|
|
64745
66091
|
onStoryComplete: (event) => emit("onStoryComplete", event),
|
|
64746
|
-
onRunEnd: (event) => emit("onRunEnd", event)
|
|
66092
|
+
onRunEnd: (event) => emit("onRunEnd", event),
|
|
66093
|
+
onPhaseStart: (event) => emit("onPhaseStart", event),
|
|
66094
|
+
onPhaseComplete: (event) => emit("onPhaseComplete", event)
|
|
64747
66095
|
};
|
|
64748
66096
|
return {
|
|
64749
|
-
name:
|
|
66097
|
+
name: STAGE4,
|
|
64750
66098
|
version: "1.0.0",
|
|
64751
66099
|
provides: ["reporter"],
|
|
64752
66100
|
extensions: { reporter }
|
|
64753
66101
|
};
|
|
64754
66102
|
}
|
|
64755
|
-
var
|
|
66103
|
+
var STAGE4 = "webhook-reporter";
|
|
64756
66104
|
var init_webhook_reporter = __esm(() => {
|
|
64757
66105
|
init_logger2();
|
|
64758
66106
|
init_reporter_shared();
|
|
@@ -65177,7 +66525,7 @@ async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, di
|
|
|
65177
66525
|
{
|
|
65178
66526
|
name: "otel-reporter",
|
|
65179
66527
|
enabled: reporters.otel.enabled,
|
|
65180
|
-
make: () => createOtelReporterPlugin(reporters.otel)
|
|
66528
|
+
make: () => createOtelReporterPlugin(reporters.otel, undefined, effectiveProjectRoot)
|
|
65181
66529
|
}
|
|
65182
66530
|
] : [];
|
|
65183
66531
|
for (const { name, enabled: reporterEnabled, make } of reporterFactories) {
|
|
@@ -65787,7 +67135,7 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
|
|
|
65787
67135
|
}
|
|
65788
67136
|
}
|
|
65789
67137
|
}
|
|
65790
|
-
function
|
|
67138
|
+
function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
|
|
65791
67139
|
const logger = _heartbeatDeps.getSafeLogger();
|
|
65792
67140
|
_heartbeatActive = true;
|
|
65793
67141
|
const gen = ++_heartbeatGen;
|
|
@@ -67381,28 +68729,47 @@ async function handleRunCompletion(options) {
|
|
|
67381
68729
|
const regressionMode = config2.execution.regressionGate?.mode;
|
|
67382
68730
|
if (options.skipRegression) {} else if ((regressionMode === "deferred" || regressionMode === "per-story") && config2.quality.commands.test) {
|
|
67383
68731
|
statusWriter.setPostRunPhase("regression", { status: "running" });
|
|
68732
|
+
const regressionStartTime = Date.now();
|
|
67384
68733
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "regression" });
|
|
67385
|
-
|
|
67386
|
-
|
|
67387
|
-
|
|
67388
|
-
|
|
67389
|
-
|
|
67390
|
-
|
|
67391
|
-
|
|
67392
|
-
|
|
67393
|
-
|
|
67394
|
-
|
|
67395
|
-
|
|
67396
|
-
|
|
68734
|
+
let regressionResult;
|
|
68735
|
+
try {
|
|
68736
|
+
regressionResult = await _runCompletionDeps.runDeferredRegression({
|
|
68737
|
+
config: config2,
|
|
68738
|
+
prd,
|
|
68739
|
+
workdir,
|
|
68740
|
+
runtime: options.runtime,
|
|
68741
|
+
quarantineMemo: options.runtime.quarantineMemo,
|
|
68742
|
+
storyMetrics: options.isSequential === false ? undefined : allStoryMetrics.map((m) => ({
|
|
68743
|
+
storyId: m.storyId,
|
|
68744
|
+
completedAt: m.completedAt,
|
|
68745
|
+
failingTestFiles: m.failingTestFiles
|
|
68746
|
+
}))
|
|
68747
|
+
});
|
|
68748
|
+
} catch (err) {
|
|
68749
|
+
pipelineEventBus.emit({
|
|
68750
|
+
type: "postrun:phase:completed",
|
|
68751
|
+
phase: "regression",
|
|
68752
|
+
passed: false,
|
|
68753
|
+
durationMs: Date.now() - regressionStartTime
|
|
68754
|
+
});
|
|
68755
|
+
throw err;
|
|
68756
|
+
}
|
|
67397
68757
|
const lastRunAt = new Date().toISOString();
|
|
67398
68758
|
logger?.info("regression", "Deferred regression gate completed", {
|
|
67399
68759
|
success: regressionResult.success,
|
|
67400
68760
|
failedTests: regressionResult.failedTests,
|
|
67401
68761
|
affectedStories: regressionResult.affectedStories
|
|
67402
68762
|
});
|
|
68763
|
+
const regressionDurationMs = Date.now() - regressionStartTime;
|
|
67403
68764
|
if (regressionResult.success) {
|
|
67404
68765
|
statusWriter.setPostRunPhase("regression", { status: "passed", lastRunAt });
|
|
67405
|
-
pipelineEventBus.emit({
|
|
68766
|
+
pipelineEventBus.emit({
|
|
68767
|
+
type: "postrun:phase:completed",
|
|
68768
|
+
phase: "regression",
|
|
68769
|
+
passed: true,
|
|
68770
|
+
durationMs: regressionDurationMs,
|
|
68771
|
+
details: { mode: regressionMode, failedTests: 0 }
|
|
68772
|
+
});
|
|
67406
68773
|
} else {
|
|
67407
68774
|
statusWriter.setPostRunPhase("regression", {
|
|
67408
68775
|
status: "failed",
|
|
@@ -67410,7 +68777,16 @@ async function handleRunCompletion(options) {
|
|
|
67410
68777
|
affectedStories: regressionResult.affectedStories,
|
|
67411
68778
|
lastRunAt
|
|
67412
68779
|
});
|
|
67413
|
-
pipelineEventBus.emit({
|
|
68780
|
+
pipelineEventBus.emit({
|
|
68781
|
+
type: "postrun:phase:completed",
|
|
68782
|
+
phase: "regression",
|
|
68783
|
+
passed: false,
|
|
68784
|
+
durationMs: regressionDurationMs,
|
|
68785
|
+
details: {
|
|
68786
|
+
mode: regressionMode,
|
|
68787
|
+
failedTests: regressionResult.failedTests
|
|
68788
|
+
}
|
|
68789
|
+
});
|
|
67414
68790
|
for (const storyId of regressionResult.affectedStories) {
|
|
67415
68791
|
const story = prd.userStories.find((s) => s.id === storyId);
|
|
67416
68792
|
if (story) {
|
|
@@ -67478,7 +68854,15 @@ async function handleRunCompletion(options) {
|
|
|
67478
68854
|
let pluginGateFailed = false;
|
|
67479
68855
|
const deferredReview = options.deferredReview;
|
|
67480
68856
|
if (deferredReview !== undefined) {
|
|
67481
|
-
|
|
68857
|
+
const findingCount = deferredReview.reviewerResults.filter((r) => !r.passed).length;
|
|
68858
|
+
const reviewDurationMs = Date.now() - (options.deferredReviewStartedAt ?? Date.now());
|
|
68859
|
+
pipelineEventBus.emit({
|
|
68860
|
+
type: "postrun:phase:completed",
|
|
68861
|
+
phase: "review",
|
|
68862
|
+
passed: !deferredReview.anyFailed,
|
|
68863
|
+
durationMs: reviewDurationMs,
|
|
68864
|
+
details: { findingCount, anyFailed: deferredReview.anyFailed }
|
|
68865
|
+
});
|
|
67482
68866
|
}
|
|
67483
68867
|
if (deferredReview?.anyFailed) {
|
|
67484
68868
|
const failedReviewers = deferredReview.reviewerResults.filter((r) => !r.passed).map((r) => r.name);
|
|
@@ -67727,6 +69111,7 @@ async function runCompletionPhase(options) {
|
|
|
67727
69111
|
logger?.info("execution", "Acceptance already passed \u2014 skipping acceptance phase");
|
|
67728
69112
|
} else if (options.config.acceptance.enabled && isComplete(options.prd)) {
|
|
67729
69113
|
options.statusWriter.setPostRunPhase("acceptance", { status: "running" });
|
|
69114
|
+
const acceptanceStartTime = Date.now();
|
|
67730
69115
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "acceptance" });
|
|
67731
69116
|
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) => {
|
|
67732
69117
|
const relativeWorkdir = path25.relative(options.workdir, g.packageDir);
|
|
@@ -67749,32 +69134,54 @@ async function runCompletionPhase(options) {
|
|
|
67749
69134
|
commandOverride: groupConfig.acceptance.command
|
|
67750
69135
|
};
|
|
67751
69136
|
})) : undefined;
|
|
67752
|
-
|
|
67753
|
-
|
|
67754
|
-
|
|
67755
|
-
|
|
67756
|
-
|
|
67757
|
-
|
|
67758
|
-
|
|
67759
|
-
|
|
67760
|
-
|
|
67761
|
-
|
|
67762
|
-
|
|
67763
|
-
|
|
67764
|
-
|
|
67765
|
-
|
|
67766
|
-
|
|
67767
|
-
|
|
67768
|
-
|
|
67769
|
-
|
|
67770
|
-
|
|
67771
|
-
|
|
67772
|
-
|
|
67773
|
-
|
|
69137
|
+
let acceptanceResult;
|
|
69138
|
+
try {
|
|
69139
|
+
acceptanceResult = await _runnerCompletionDeps.runAcceptanceLoop({
|
|
69140
|
+
config: options.config,
|
|
69141
|
+
prd: options.prd,
|
|
69142
|
+
prdPath: options.prdPath,
|
|
69143
|
+
workdir: options.workdir,
|
|
69144
|
+
featureDir: options.featureDir,
|
|
69145
|
+
hooks: options.hooks,
|
|
69146
|
+
feature: options.feature,
|
|
69147
|
+
totalCost: options.totalCost,
|
|
69148
|
+
iterations: options.iterations,
|
|
69149
|
+
storiesCompleted: options.storiesCompleted,
|
|
69150
|
+
allStoryMetrics: options.allStoryMetrics,
|
|
69151
|
+
pluginRegistry: options.pluginRegistry,
|
|
69152
|
+
eventEmitter: options.eventEmitter,
|
|
69153
|
+
statusWriter: options.statusWriter,
|
|
69154
|
+
agentGetFn: options.agentGetFn,
|
|
69155
|
+
agentManager: options.agentManager,
|
|
69156
|
+
sessionManager: options.sessionManager,
|
|
69157
|
+
runtime: options.runtime,
|
|
69158
|
+
abortSignal: options.abortSignal,
|
|
69159
|
+
acceptanceTestPaths
|
|
69160
|
+
});
|
|
69161
|
+
} catch (err) {
|
|
69162
|
+
pipelineEventBus.emit({
|
|
69163
|
+
type: "postrun:phase:completed",
|
|
69164
|
+
phase: "acceptance",
|
|
69165
|
+
passed: false,
|
|
69166
|
+
durationMs: Date.now() - acceptanceStartTime
|
|
69167
|
+
});
|
|
69168
|
+
throw err;
|
|
69169
|
+
}
|
|
67774
69170
|
const lastRunAt = new Date().toISOString();
|
|
69171
|
+
const acceptanceDurationMs = Date.now() - acceptanceStartTime;
|
|
67775
69172
|
if (acceptanceResult.success) {
|
|
67776
69173
|
options.statusWriter.setPostRunPhase("acceptance", { status: "passed", lastRunAt });
|
|
67777
|
-
pipelineEventBus.emit({
|
|
69174
|
+
pipelineEventBus.emit({
|
|
69175
|
+
type: "postrun:phase:completed",
|
|
69176
|
+
phase: "acceptance",
|
|
69177
|
+
passed: true,
|
|
69178
|
+
durationMs: acceptanceDurationMs,
|
|
69179
|
+
details: {
|
|
69180
|
+
retries: acceptanceResult.retries ?? 0,
|
|
69181
|
+
failedACCount: acceptanceResult.failedACs?.length ?? 0,
|
|
69182
|
+
fixStoriesCreated: 0
|
|
69183
|
+
}
|
|
69184
|
+
});
|
|
67778
69185
|
} else {
|
|
67779
69186
|
acceptancePassed = false;
|
|
67780
69187
|
options.statusWriter.setPostRunPhase("acceptance", {
|
|
@@ -67783,7 +69190,17 @@ async function runCompletionPhase(options) {
|
|
|
67783
69190
|
retries: acceptanceResult.retries ?? 0,
|
|
67784
69191
|
lastRunAt
|
|
67785
69192
|
});
|
|
67786
|
-
pipelineEventBus.emit({
|
|
69193
|
+
pipelineEventBus.emit({
|
|
69194
|
+
type: "postrun:phase:completed",
|
|
69195
|
+
phase: "acceptance",
|
|
69196
|
+
passed: false,
|
|
69197
|
+
durationMs: acceptanceDurationMs,
|
|
69198
|
+
details: {
|
|
69199
|
+
retries: acceptanceResult.retries ?? 0,
|
|
69200
|
+
failedACCount: acceptanceResult.failedACs?.length ?? 0,
|
|
69201
|
+
fixStoriesCreated: 0
|
|
69202
|
+
}
|
|
69203
|
+
});
|
|
67787
69204
|
}
|
|
67788
69205
|
Object.assign(options, {
|
|
67789
69206
|
prd: acceptanceResult.prd,
|
|
@@ -67814,6 +69231,7 @@ async function runCompletionPhase(options) {
|
|
|
67814
69231
|
sessionManager: options.sessionManager,
|
|
67815
69232
|
pluginProviderCache: options.pluginProviderCache,
|
|
67816
69233
|
deferredReview: options.deferredReview,
|
|
69234
|
+
deferredReviewStartedAt: options.deferredReviewStartedAt,
|
|
67817
69235
|
exitReason: options.exitReason,
|
|
67818
69236
|
runtime: options.runtime,
|
|
67819
69237
|
abortSignal: options.abortSignal
|
|
@@ -67890,7 +69308,7 @@ var init_runner_completion = __esm(() => {
|
|
|
67890
69308
|
});
|
|
67891
69309
|
|
|
67892
69310
|
// src/execution/batching.ts
|
|
67893
|
-
function groupStoriesIntoBatches(stories, maxBatchSize =
|
|
69311
|
+
function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
|
|
67894
69312
|
const batches = [];
|
|
67895
69313
|
let currentBatch = [];
|
|
67896
69314
|
for (const story of stories) {
|
|
@@ -67923,7 +69341,7 @@ function groupStoriesIntoBatches(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE)
|
|
|
67923
69341
|
}
|
|
67924
69342
|
return batches;
|
|
67925
69343
|
}
|
|
67926
|
-
function precomputeBatchPlan(stories, maxBatchSize =
|
|
69344
|
+
function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE2) {
|
|
67927
69345
|
const batches = [];
|
|
67928
69346
|
let currentBatch = [];
|
|
67929
69347
|
for (const story of stories) {
|
|
@@ -67960,7 +69378,7 @@ function precomputeBatchPlan(stories, maxBatchSize = DEFAULT_MAX_BATCH_SIZE) {
|
|
|
67960
69378
|
}
|
|
67961
69379
|
return batches;
|
|
67962
69380
|
}
|
|
67963
|
-
var
|
|
69381
|
+
var DEFAULT_MAX_BATCH_SIZE2 = 4;
|
|
67964
69382
|
|
|
67965
69383
|
// src/execution/ensure-package-dirs.ts
|
|
67966
69384
|
import path26 from "path";
|
|
@@ -68240,131 +69658,6 @@ var init_registry6 = __esm(() => {
|
|
|
68240
69658
|
init_paths3();
|
|
68241
69659
|
});
|
|
68242
69660
|
|
|
68243
|
-
// src/pipeline/subscribers/reporters.ts
|
|
68244
|
-
function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
68245
|
-
const logger = getSafeLogger();
|
|
68246
|
-
const safe = (name, fn) => {
|
|
68247
|
-
return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
|
|
68248
|
-
};
|
|
68249
|
-
const unsubs = [];
|
|
68250
|
-
unsubs.push(bus.on("run:started", (ev) => {
|
|
68251
|
-
return safe("onRunStart", async () => {
|
|
68252
|
-
const reporters = pluginRegistry.getReporters();
|
|
68253
|
-
for (const r of reporters) {
|
|
68254
|
-
if (r.onRunStart) {
|
|
68255
|
-
try {
|
|
68256
|
-
await r.onRunStart({
|
|
68257
|
-
runId,
|
|
68258
|
-
feature: ev.feature,
|
|
68259
|
-
totalStories: ev.totalStories,
|
|
68260
|
-
startTime: new Date(startTime).toISOString()
|
|
68261
|
-
});
|
|
68262
|
-
} catch (err) {
|
|
68263
|
-
logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
|
|
68264
|
-
}
|
|
68265
|
-
}
|
|
68266
|
-
}
|
|
68267
|
-
});
|
|
68268
|
-
}));
|
|
68269
|
-
unsubs.push(bus.on("story:completed", (ev) => {
|
|
68270
|
-
return safe("onStoryComplete(completed)", async () => {
|
|
68271
|
-
const reporters = pluginRegistry.getReporters();
|
|
68272
|
-
for (const r of reporters) {
|
|
68273
|
-
if (r.onStoryComplete) {
|
|
68274
|
-
try {
|
|
68275
|
-
await r.onStoryComplete({
|
|
68276
|
-
runId,
|
|
68277
|
-
storyId: ev.storyId,
|
|
68278
|
-
status: "completed",
|
|
68279
|
-
runElapsedMs: ev.runElapsedMs,
|
|
68280
|
-
cost: ev.cost ?? 0,
|
|
68281
|
-
tier: ev.modelTier ?? "balanced",
|
|
68282
|
-
testStrategy: ev.testStrategy ?? "test-after"
|
|
68283
|
-
});
|
|
68284
|
-
} catch (err) {
|
|
68285
|
-
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
68286
|
-
}
|
|
68287
|
-
}
|
|
68288
|
-
}
|
|
68289
|
-
});
|
|
68290
|
-
}));
|
|
68291
|
-
unsubs.push(bus.on("story:failed", (ev) => {
|
|
68292
|
-
return safe("onStoryComplete(failed)", async () => {
|
|
68293
|
-
const reporters = pluginRegistry.getReporters();
|
|
68294
|
-
for (const r of reporters) {
|
|
68295
|
-
if (r.onStoryComplete) {
|
|
68296
|
-
try {
|
|
68297
|
-
await r.onStoryComplete({
|
|
68298
|
-
runId,
|
|
68299
|
-
storyId: ev.storyId,
|
|
68300
|
-
status: "failed",
|
|
68301
|
-
runElapsedMs: Date.now() - startTime,
|
|
68302
|
-
cost: 0,
|
|
68303
|
-
tier: "balanced",
|
|
68304
|
-
testStrategy: "test-after"
|
|
68305
|
-
});
|
|
68306
|
-
} catch (err) {
|
|
68307
|
-
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
68308
|
-
}
|
|
68309
|
-
}
|
|
68310
|
-
}
|
|
68311
|
-
});
|
|
68312
|
-
}));
|
|
68313
|
-
unsubs.push(bus.on("story:paused", (ev) => {
|
|
68314
|
-
return safe("onStoryComplete(paused)", async () => {
|
|
68315
|
-
const reporters = pluginRegistry.getReporters();
|
|
68316
|
-
for (const r of reporters) {
|
|
68317
|
-
if (r.onStoryComplete) {
|
|
68318
|
-
try {
|
|
68319
|
-
await r.onStoryComplete({
|
|
68320
|
-
runId,
|
|
68321
|
-
storyId: ev.storyId,
|
|
68322
|
-
status: "paused",
|
|
68323
|
-
runElapsedMs: Date.now() - startTime,
|
|
68324
|
-
cost: 0,
|
|
68325
|
-
tier: "balanced",
|
|
68326
|
-
testStrategy: "test-after"
|
|
68327
|
-
});
|
|
68328
|
-
} catch (err) {
|
|
68329
|
-
logger?.warn("plugins", `Reporter '${r.name}' onStoryComplete failed`, { error: err });
|
|
68330
|
-
}
|
|
68331
|
-
}
|
|
68332
|
-
}
|
|
68333
|
-
});
|
|
68334
|
-
}));
|
|
68335
|
-
unsubs.push(bus.on("run:completed", (ev) => {
|
|
68336
|
-
return safe("onRunEnd", async () => {
|
|
68337
|
-
const reporters = pluginRegistry.getReporters();
|
|
68338
|
-
for (const r of reporters) {
|
|
68339
|
-
if (r.onRunEnd) {
|
|
68340
|
-
try {
|
|
68341
|
-
await r.onRunEnd({
|
|
68342
|
-
runId,
|
|
68343
|
-
totalDurationMs: Date.now() - startTime,
|
|
68344
|
-
totalCost: ev.totalCost ?? 0,
|
|
68345
|
-
storySummary: {
|
|
68346
|
-
completed: ev.passedStories,
|
|
68347
|
-
failed: ev.failedStories,
|
|
68348
|
-
skipped: ev.skippedStories,
|
|
68349
|
-
paused: ev.pausedStories
|
|
68350
|
-
}
|
|
68351
|
-
});
|
|
68352
|
-
} catch (err) {
|
|
68353
|
-
logger?.warn("plugins", `Reporter '${r.name}' onRunEnd failed`, { error: err });
|
|
68354
|
-
}
|
|
68355
|
-
}
|
|
68356
|
-
}
|
|
68357
|
-
});
|
|
68358
|
-
}));
|
|
68359
|
-
return () => {
|
|
68360
|
-
for (const u of unsubs)
|
|
68361
|
-
u();
|
|
68362
|
-
};
|
|
68363
|
-
}
|
|
68364
|
-
var init_reporters = __esm(() => {
|
|
68365
|
-
init_logger2();
|
|
68366
|
-
});
|
|
68367
|
-
|
|
68368
69661
|
// src/execution/deferred-review.ts
|
|
68369
69662
|
var {spawn: spawn4 } = globalThis.Bun;
|
|
68370
69663
|
async function captureRunStartRef(workdir) {
|
|
@@ -70378,6 +71671,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
70378
71671
|
const allStoryMetrics = [];
|
|
70379
71672
|
let warningSent = false;
|
|
70380
71673
|
let deferredReview;
|
|
71674
|
+
let deferredReviewStartedAt;
|
|
70381
71675
|
const runStartRef = await captureRunStartRef(ctx.workdir);
|
|
70382
71676
|
let cachedNaxIgnoreKey;
|
|
70383
71677
|
const getRunNaxIgnoreIndex = async (currentPrd) => {
|
|
@@ -70416,14 +71710,16 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
70416
71710
|
totalCost: totalCost2,
|
|
70417
71711
|
allStoryMetrics,
|
|
70418
71712
|
exitReason,
|
|
70419
|
-
deferredReview
|
|
71713
|
+
deferredReview,
|
|
71714
|
+
deferredReviewStartedAt
|
|
70420
71715
|
});
|
|
70421
|
-
|
|
71716
|
+
startHeartbeat2(ctx.statusWriter, () => totalCost2, () => iterations, ctx.logFilePath);
|
|
70422
71717
|
let _executeThrew = false;
|
|
70423
71718
|
try {
|
|
70424
71719
|
if (isComplete(prd)) {
|
|
70425
71720
|
logger?.info("execution", "All stories already complete \u2014 skipping pre-run pipeline");
|
|
70426
71721
|
const naxIgnoreIndex = await getRunNaxIgnoreIndex(prd);
|
|
71722
|
+
deferredReviewStartedAt = Date.now();
|
|
70427
71723
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
|
|
70428
71724
|
deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
|
|
70429
71725
|
return buildResult2("completed");
|
|
@@ -70478,6 +71774,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
70478
71774
|
return buildResult2("pre-merge-aborted");
|
|
70479
71775
|
}
|
|
70480
71776
|
logger?.debug("execution", "Running deferred review");
|
|
71777
|
+
deferredReviewStartedAt = Date.now();
|
|
70481
71778
|
pipelineEventBus.emit({ type: "postrun:phase:started", phase: "review" });
|
|
70482
71779
|
deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef, naxIgnoreIndex);
|
|
70483
71780
|
logger?.debug("execution", "Deferred review done \u2014 returning completed");
|
|
@@ -70988,6 +72285,7 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
|
|
|
70988
72285
|
totalCost: totalCost2,
|
|
70989
72286
|
allStoryMetrics,
|
|
70990
72287
|
deferredReview: unifiedResult.deferredReview,
|
|
72288
|
+
deferredReviewStartedAt: unifiedResult.deferredReviewStartedAt,
|
|
70991
72289
|
exitReason: unifiedResult.exitReason
|
|
70992
72290
|
};
|
|
70993
72291
|
}
|
|
@@ -72241,10 +73539,10 @@ async function cleanupRun(options) {
|
|
|
72241
73539
|
}
|
|
72242
73540
|
const actions = pluginRegistry.getPostRunActions();
|
|
72243
73541
|
const pluginLogger = {
|
|
72244
|
-
debug: (msg) => logger?.debug("post-run", msg),
|
|
72245
|
-
info: (msg) => logger?.info("post-run", msg),
|
|
72246
|
-
warn: (msg) => logger?.warn("post-run", msg),
|
|
72247
|
-
error: (msg) => logger?.error("post-run", msg)
|
|
73542
|
+
debug: (msg, data) => logger?.debug("post-run", msg, data),
|
|
73543
|
+
info: (msg, data) => logger?.info("post-run", msg, data),
|
|
73544
|
+
warn: (msg, data) => logger?.warn("post-run", msg, data),
|
|
73545
|
+
error: (msg, data) => logger?.error("post-run", msg, data)
|
|
72248
73546
|
};
|
|
72249
73547
|
const ctx = buildPostRunContext(options, durationMs, pluginLogger);
|
|
72250
73548
|
for (const action of actions) {
|
|
@@ -72440,6 +73738,7 @@ async function run(options) {
|
|
|
72440
73738
|
agentManager,
|
|
72441
73739
|
pluginProviderCache,
|
|
72442
73740
|
deferredReview: executionResult.deferredReview,
|
|
73741
|
+
deferredReviewStartedAt: executionResult.deferredReviewStartedAt,
|
|
72443
73742
|
exitReason: executionResult.exitReason,
|
|
72444
73743
|
runtime,
|
|
72445
73744
|
abortSignal: shutdownController.signal
|
|
@@ -72538,8 +73837,11 @@ __export(exports_execution, {
|
|
|
72538
73837
|
withIncreasingFailuresBail: () => withIncreasingFailuresBail,
|
|
72539
73838
|
synthesizeBackfillMetric: () => synthesizeBackfillMetric,
|
|
72540
73839
|
stopHeartbeat: () => stopHeartbeat,
|
|
72541
|
-
startHeartbeat: () =>
|
|
73840
|
+
startHeartbeat: () => startHeartbeat2,
|
|
73841
|
+
runRectification: () => runRectification,
|
|
73842
|
+
runPhase: () => runPhase,
|
|
72542
73843
|
runDeferredRegression: () => runDeferredRegression,
|
|
73844
|
+
runCompletionPhase: () => runCompletionPhase,
|
|
72543
73845
|
run: () => run,
|
|
72544
73846
|
resolveMaxAttemptsOutcome: () => resolveMaxAttemptsOutcome,
|
|
72545
73847
|
resetCrashHandlers: () => resetCrashHandlers,
|
|
@@ -72565,7 +73867,6 @@ __export(exports_execution, {
|
|
|
72565
73867
|
getTierConfig: () => getTierConfig,
|
|
72566
73868
|
getOscillations: () => getOscillations,
|
|
72567
73869
|
getAllReadyStories: () => getAllReadyStories,
|
|
72568
|
-
gateRegressedAfterRectification: () => gateRegressedAfterRectification,
|
|
72569
73870
|
gateFailureKeys: () => gateFailureKeys,
|
|
72570
73871
|
formatProgress: () => formatProgress,
|
|
72571
73872
|
formatPhaseResultMessage: () => formatPhaseResultMessage,
|
|
@@ -72574,11 +73875,13 @@ __export(exports_execution, {
|
|
|
72574
73875
|
extractPauseReason: () => extractPauseReason,
|
|
72575
73876
|
escalateTier: () => escalateTier,
|
|
72576
73877
|
ensureStoryPackageDirs: () => ensureStoryPackageDirs,
|
|
73878
|
+
describeGateRegression: () => describeGateRegression,
|
|
72577
73879
|
deriveTddFailureCategory: () => deriveTddFailureCategory,
|
|
72578
73880
|
decideStageAction: () => decideStageAction,
|
|
72579
73881
|
createCheckpointWriter: () => createCheckpointWriter,
|
|
72580
73882
|
countOscillationOutcomes: () => countOscillationOutcomes,
|
|
72581
73883
|
clearQueueFile: () => clearQueueFile,
|
|
73884
|
+
cleanupRun: () => cleanupRun,
|
|
72582
73885
|
captureTreeState: () => captureTreeState,
|
|
72583
73886
|
calculateMaxIterations: () => calculateMaxIterations,
|
|
72584
73887
|
buildStoryContext: () => buildStoryContext,
|
|
@@ -72595,6 +73898,7 @@ __export(exports_execution, {
|
|
|
72595
73898
|
_storyOrchestratorDeps: () => _storyOrchestratorDeps,
|
|
72596
73899
|
_runnerReentrancyGuard: () => _runnerReentrancyGuard,
|
|
72597
73900
|
_runnerDeps: () => _runnerDeps,
|
|
73901
|
+
_runnerCompletionDeps: () => _runnerCompletionDeps,
|
|
72598
73902
|
_runCompletionDeps: () => _runCompletionDeps,
|
|
72599
73903
|
_regressionDeps: () => _regressionDeps,
|
|
72600
73904
|
_postRunDeps: () => _postRunDeps,
|
|
@@ -72626,6 +73930,7 @@ var init_execution2 = __esm(() => {
|
|
|
72626
73930
|
init_plan_inputs();
|
|
72627
73931
|
init_build_plan_for_strategy();
|
|
72628
73932
|
init_checkpoint();
|
|
73933
|
+
init_runner_completion();
|
|
72629
73934
|
init_post_run();
|
|
72630
73935
|
});
|
|
72631
73936
|
|
|
@@ -104405,7 +105710,7 @@ async function resolveRunProfileOverride(opts) {
|
|
|
104405
105710
|
// src/cli/features-resolve.ts
|
|
104406
105711
|
init_config();
|
|
104407
105712
|
import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
|
|
104408
|
-
import { join as join74, relative as
|
|
105713
|
+
import { join as join74, relative as relative17 } from "path";
|
|
104409
105714
|
|
|
104410
105715
|
// src/cli/features-acceptance.ts
|
|
104411
105716
|
init_acceptance2();
|
|
@@ -104413,7 +105718,7 @@ init_config();
|
|
|
104413
105718
|
init_logger2();
|
|
104414
105719
|
init_prd();
|
|
104415
105720
|
import { existsSync as existsSync27 } from "fs";
|
|
104416
|
-
import { join as join73, relative as
|
|
105721
|
+
import { join as join73, relative as relative16 } from "path";
|
|
104417
105722
|
async function resolveFeatureAcceptance(featureName, workdir) {
|
|
104418
105723
|
let enabled = true;
|
|
104419
105724
|
try {
|
|
@@ -104434,11 +105739,11 @@ async function resolveFeatureAcceptance(featureName, workdir) {
|
|
|
104434
105739
|
const prd = await loadPRD(prdPath);
|
|
104435
105740
|
const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
|
|
104436
105741
|
const groups = await Promise.all(testGroups.map(async (g) => {
|
|
104437
|
-
const packageDir =
|
|
105742
|
+
const packageDir = relative16(repoRoot, g.packageDir);
|
|
104438
105743
|
const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
|
|
104439
105744
|
return {
|
|
104440
105745
|
packageDir,
|
|
104441
|
-
testPath:
|
|
105746
|
+
testPath: relative16(repoRoot, g.testPath),
|
|
104442
105747
|
exists: await Bun.file(g.testPath).exists(),
|
|
104443
105748
|
command,
|
|
104444
105749
|
cwd: packageDir,
|
|
@@ -104475,17 +105780,17 @@ async function searchSpecSource(naxDir, repoRoot, name) {
|
|
|
104475
105780
|
];
|
|
104476
105781
|
const docsSpecExact = join74(repoRoot, "docs", "specs", `SPEC-${name}.md`);
|
|
104477
105782
|
candidates.push({ abs: docsSpecExact, kind: "markdown" });
|
|
104478
|
-
const checked = candidates.map((c) =>
|
|
105783
|
+
const checked = candidates.map((c) => relative17(repoRoot, c.abs));
|
|
104479
105784
|
for (const { abs, kind } of candidates.slice(0, 2)) {
|
|
104480
105785
|
if (kind === "markdown") {
|
|
104481
105786
|
const nonEmpty = await isNonEmptyFile(abs);
|
|
104482
105787
|
if (nonEmpty) {
|
|
104483
|
-
return { source: { kind, path:
|
|
105788
|
+
return { source: { kind, path: relative17(repoRoot, abs) }, checked };
|
|
104484
105789
|
}
|
|
104485
105790
|
}
|
|
104486
105791
|
}
|
|
104487
105792
|
if (await isNonEmptyFile(docsSpecExact)) {
|
|
104488
|
-
return { source: { kind: "markdown", path:
|
|
105793
|
+
return { source: { kind: "markdown", path: relative17(repoRoot, docsSpecExact) }, checked };
|
|
104489
105794
|
}
|
|
104490
105795
|
const docsSpecsDir = join74(repoRoot, "docs", "specs");
|
|
104491
105796
|
if (existsSync28(docsSpecsDir)) {
|
|
@@ -104493,7 +105798,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
|
|
|
104493
105798
|
for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
|
|
104494
105799
|
const abs = join74(docsSpecsDir, match);
|
|
104495
105800
|
if (await isNonEmptyFile(abs)) {
|
|
104496
|
-
const relPath =
|
|
105801
|
+
const relPath = relative17(repoRoot, abs);
|
|
104497
105802
|
if (!checked.includes(relPath))
|
|
104498
105803
|
checked.push(relPath);
|
|
104499
105804
|
return { source: { kind: "markdown", path: relPath }, checked };
|
|
@@ -104501,7 +105806,7 @@ async function searchSpecSource(naxDir, repoRoot, name) {
|
|
|
104501
105806
|
}
|
|
104502
105807
|
}
|
|
104503
105808
|
const prdAbs = join74(naxDir, "features", name, "prd.json");
|
|
104504
|
-
const prdRel =
|
|
105809
|
+
const prdRel = relative17(repoRoot, prdAbs);
|
|
104505
105810
|
if (!checked.includes(prdRel))
|
|
104506
105811
|
checked.push(prdRel);
|
|
104507
105812
|
if (existsSync28(prdAbs)) {
|
|
@@ -104553,8 +105858,8 @@ async function resolveFeatureSpec(name, workdir) {
|
|
|
104553
105858
|
return {
|
|
104554
105859
|
status: "ok",
|
|
104555
105860
|
featureName: null,
|
|
104556
|
-
specSource: { kind: "markdown", path:
|
|
104557
|
-
message: `resolved spec: ${
|
|
105861
|
+
specSource: { kind: "markdown", path: relative17(repoRoot, abs) },
|
|
105862
|
+
message: `resolved spec: ${relative17(repoRoot, abs)}`
|
|
104558
105863
|
};
|
|
104559
105864
|
}
|
|
104560
105865
|
if (name !== undefined && name.trim() !== "") {
|
|
@@ -106052,9 +107357,9 @@ function parseSchedule(input, now2) {
|
|
|
106052
107357
|
const trimmed = input.trim();
|
|
106053
107358
|
if (trimmed === "")
|
|
106054
107359
|
return { ok: false, error: `Empty schedule value. ${ACCEPTED}` };
|
|
106055
|
-
const
|
|
106056
|
-
if (
|
|
106057
|
-
return
|
|
107360
|
+
const relative18 = parseRelative(trimmed, now2);
|
|
107361
|
+
if (relative18)
|
|
107362
|
+
return relative18;
|
|
106058
107363
|
const timeOfDay = parseTimeOfDay(trimmed, now2);
|
|
106059
107364
|
if (timeOfDay)
|
|
106060
107365
|
return timeOfDay;
|