@nathapp/nax 0.75.5 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nax.js CHANGED
@@ -17377,7 +17377,11 @@ var init_schemas_review = __esm(() => {
17377
17377
  maxRequotes: 5
17378
17378
  }),
17379
17379
  excludePatterns: exports_external.array(exports_external.string()).optional(),
17380
- demandInspectionTrail: exports_external.boolean().default(true)
17380
+ demandInspectionTrail: exports_external.boolean().default(true),
17381
+ recurrenceDemotion: exports_external.object({
17382
+ enabled: exports_external.boolean().default(false),
17383
+ maxBlockingRounds: exports_external.number().int().min(1).default(2)
17384
+ }).default({ enabled: false, maxBlockingRounds: 2 })
17381
17385
  });
17382
17386
  AdversarialReviewConfigSchema = exports_external.object({
17383
17387
  model: ConfiguredModelSchema.default("balanced"),
@@ -17641,6 +17645,7 @@ var init_schemas3 = __esm(() => {
17641
17645
  rules: [],
17642
17646
  timeoutMs: 600000,
17643
17647
  demandInspectionTrail: true,
17648
+ recurrenceDemotion: { enabled: false, maxBlockingRounds: 2 },
17644
17649
  substantiation: {
17645
17650
  requote: true,
17646
17651
  maxRequotes: 5
@@ -17818,6 +17823,7 @@ var init_schemas3 = __esm(() => {
17818
17823
  enabled: exports_external.boolean().default(false),
17819
17824
  flowPath: exports_external.string().default("flows/nax-finish/nax-finish.flow.ts"),
17820
17825
  defaultAgent: exports_external.string().nullable().default(null),
17826
+ model: exports_external.string().min(1, "model must be non-empty").nullable().default(null),
17821
17827
  reviewers: exports_external.object({
17822
17828
  spec: exports_external.string().nullable().default(null),
17823
17829
  quality: exports_external.string().nullable().default(null)
@@ -17834,6 +17840,7 @@ var init_schemas3 = __esm(() => {
17834
17840
  enabled: false,
17835
17841
  flowPath: "flows/nax-finish/nax-finish.flow.ts",
17836
17842
  defaultAgent: null,
17843
+ model: null,
17837
17844
  reviewers: { spec: null, quality: null },
17838
17845
  escalate: { telegram: true },
17839
17846
  notify: { mode: "escalation" },
@@ -17844,6 +17851,7 @@ var init_schemas3 = __esm(() => {
17844
17851
  enabled: false,
17845
17852
  flowPath: "flows/nax-finish/nax-finish.flow.ts",
17846
17853
  defaultAgent: null,
17854
+ model: null,
17847
17855
  reviewers: { spec: null, quality: null },
17848
17856
  escalate: { telegram: true },
17849
17857
  notify: { mode: "escalation" },
@@ -19634,6 +19642,13 @@ function reshapeSelector(name, fn) {
19634
19642
  return { name, select: fn };
19635
19643
  }
19636
19644
 
19645
+ // src/config/project-key.ts
19646
+ import { basename as basename3 } from "path";
19647
+ function getProjectKey(config2, projectDir) {
19648
+ return config2.name?.trim() || basename3(projectDir);
19649
+ }
19650
+ var init_project_key = () => {};
19651
+
19637
19652
  // src/config/selectors.ts
19638
19653
  var reviewConfigSelector, planConfigSelector, decomposeConfigSelector, rectifyConfigSelector, acceptanceConfigSelector, acceptanceFixConfigSelector, acceptanceGenConfigSelector, tddConfigSelector, debateConfigSelector, routingConfigSelector, verifyConfigSelector, mutationCheckConfigSelector, rectificationGateConfigSelector, agentConfigSelector, agentManagerConfigSelector, interactionConfigSelector, precheckConfigSelector, qualityConfigSelector, autofixConfigSelector, executionGatesConfigSelector, testPatternConfigSelector, contextConfigSelector, contextToolRuntimeConfigSelector, promptLoaderConfigSelector, llmRoutingConfigSelector, finishConfigSelector;
19639
19654
  var init_selectors = __esm(() => {
@@ -19654,7 +19669,7 @@ var init_selectors = __esm(() => {
19654
19669
  mutationCheckConfigSelector = reshapeSelector("mutation-check", (c) => c.execution?.mutationCheck);
19655
19670
  rectificationGateConfigSelector = pickSelector("rectification-gate", "execution", "models", "agent", "quality", "review");
19656
19671
  agentConfigSelector = pickSelector("agent", "agent");
19657
- agentManagerConfigSelector = pickSelector("agent-manager", "agent", "execution");
19672
+ agentManagerConfigSelector = pickSelector("agent-manager", "agent", "execution", "profile");
19658
19673
  interactionConfigSelector = pickSelector("interaction", "interaction");
19659
19674
  precheckConfigSelector = pickSelector("precheck", "precheck", "quality", "execution", "prompts", "review", "project");
19660
19675
  qualityConfigSelector = pickSelector("quality", "quality", "execution");
@@ -19665,7 +19680,7 @@ var init_selectors = __esm(() => {
19665
19680
  contextToolRuntimeConfigSelector = pickSelector("context-tool-runtime", "context", "execution", "project", "quality");
19666
19681
  promptLoaderConfigSelector = pickSelector("prompt-loader", "prompts", "context", "project");
19667
19682
  llmRoutingConfigSelector = pickSelector("llm-routing", "routing", "models", "agent", "tdd", "execution", "precheck");
19668
- finishConfigSelector = pickSelector("finish", "finish", "interaction", "quality");
19683
+ finishConfigSelector = pickSelector("finish", "finish", "interaction", "quality", "agent");
19669
19684
  });
19670
19685
 
19671
19686
  // src/config/loader-runtime.ts
@@ -19945,6 +19960,7 @@ __export(exports_config, {
19945
19960
  interactionConfigSelector: () => interactionConfigSelector,
19946
19961
  globalConfigPath: () => globalConfigPath,
19947
19962
  globalConfigDir: () => globalConfigDir,
19963
+ getProjectKey: () => getProjectKey,
19948
19964
  getAcQualityRules: () => getAcQualityRules,
19949
19965
  finishConfigSelector: () => finishConfigSelector,
19950
19966
  findProjectDir: () => findProjectDir,
@@ -19996,6 +20012,7 @@ var init_config = __esm(() => {
19996
20012
  init_path_security();
19997
20013
  init_paths();
19998
20014
  init_profile();
20015
+ init_project_key();
19999
20016
  init_selectors();
20000
20017
  init_test_strategy();
20001
20018
  });
@@ -20108,6 +20125,11 @@ function estimateCostFromTokenUsage(usage, model) {
20108
20125
  const cacheCreationCost = (usage.cacheCreationInputTokens ?? 0) * cacheCreationRate;
20109
20126
  return inputCost + outputCost + cacheReadCost + cacheCreationCost;
20110
20127
  }
20128
+ function resolvePricingSource(model) {
20129
+ if (model === undefined || model === "" || model === "unknown")
20130
+ return "unknown-model";
20131
+ return MODEL_PRICING[model] ? "model-rates" : "fallback-rates";
20132
+ }
20111
20133
  var init_calculate = __esm(() => {
20112
20134
  init_pricing();
20113
20135
  });
@@ -21111,6 +21133,8 @@ class AcpSessionHandleImpl {
21111
21133
  id;
21112
21134
  agentName;
21113
21135
  protocolIds;
21136
+ modelDef;
21137
+ modelTier;
21114
21138
  _client;
21115
21139
  _session;
21116
21140
  _sessionName;
@@ -21128,6 +21152,8 @@ class AcpSessionHandleImpl {
21128
21152
  this._resumed = opts.resumed;
21129
21153
  this._timeoutSeconds = opts.timeoutSeconds;
21130
21154
  this._modelDef = opts.modelDef;
21155
+ this.modelDef = opts.modelDef;
21156
+ this.modelTier = opts.modelTier;
21131
21157
  this._permissionMode = opts.permissionMode;
21132
21158
  }
21133
21159
  }
@@ -21513,6 +21539,7 @@ class AcpAgentAdapter {
21513
21539
  resumed: ensured.resumed,
21514
21540
  timeoutSeconds,
21515
21541
  modelDef,
21542
+ modelTier: opts.modelTier,
21516
21543
  permissionMode: resolvedPermissions.mode
21517
21544
  });
21518
21545
  } catch (error48) {
@@ -21657,7 +21684,7 @@ class AcpAgentAdapter {
21657
21684
  }
21658
21685
  }
21659
21686
  }
21660
- var MAX_AGENT_OUTPUT_CHARS = 5000, INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, ACP_ADAPTER_NAMES;
21687
+ var INTERACTION_TIMEOUT_MS, AGENT_REGISTRY, DEFAULT_ENTRY, ACP_ADAPTER_NAMES;
21661
21688
  var init_adapter = __esm(() => {
21662
21689
  init_errors();
21663
21690
  init_logger2();
@@ -22108,6 +22135,92 @@ function formatSessionName(req) {
22108
22135
  }
22109
22136
  var init_session_name = () => {};
22110
22137
 
22138
+ // src/agents/manager-dispatch.ts
22139
+ function modelAttribution(src) {
22140
+ return {
22141
+ ...src.modelDef?.model !== undefined ? { model: src.modelDef.model } : {},
22142
+ ...src.modelTier !== undefined ? { modelTier: src.modelTier } : {}
22143
+ };
22144
+ }
22145
+ function buildSessionTurnEvent(input) {
22146
+ const { handle, result, opts, startedAt } = input;
22147
+ return {
22148
+ kind: "session-turn",
22149
+ sessionName: handle.id,
22150
+ sessionRole: input.sessionRole,
22151
+ prompt: input.prompt,
22152
+ response: result.output,
22153
+ agentName: input.agentName,
22154
+ ...modelAttribution(handle),
22155
+ ...input.profile !== undefined ? { profile: input.profile } : {},
22156
+ stage: input.stage,
22157
+ storyId: opts.storyId,
22158
+ featureName: opts.featureName,
22159
+ workdir: opts.workdir,
22160
+ projectDir: opts.projectDir,
22161
+ resolvedPermissions: input.resolvedPermissions,
22162
+ tokenUsage: result.tokenUsage,
22163
+ estimatedCostUsd: result.estimatedCostUsd,
22164
+ exactCostUsd: result.exactCostUsd,
22165
+ durationMs: Date.now() - startedAt,
22166
+ timestamp: Date.now(),
22167
+ turn: result.internalRoundTrips ?? 1,
22168
+ protocolIds: {
22169
+ sessionId: handle.protocolIds?.sessionId ?? null,
22170
+ recordId: handle.protocolIds?.recordId ?? null
22171
+ },
22172
+ ...result.interactions?.length ? { interactions: result.interactions } : {},
22173
+ origin: "runAsSession",
22174
+ ...opts.callId !== undefined ? { callId: opts.callId } : {},
22175
+ ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
22176
+ };
22177
+ }
22178
+ function buildCompleteEvent(input) {
22179
+ const { options } = input;
22180
+ return {
22181
+ kind: "complete",
22182
+ sessionName: input.sessionName,
22183
+ sessionRole: options.sessionRole ?? "auto",
22184
+ prompt: input.prompt,
22185
+ response: input.response,
22186
+ agentName: input.agentName,
22187
+ ...modelAttribution(options),
22188
+ ...input.profile !== undefined ? { profile: input.profile } : {},
22189
+ stage: input.stage,
22190
+ storyId: options.storyId,
22191
+ featureName: options.featureName,
22192
+ workdir: options.workdir,
22193
+ resolvedPermissions: input.resolvedPermissions,
22194
+ tokenUsage: input.tokenUsage,
22195
+ estimatedCostUsd: input.estimatedCostUsd,
22196
+ exactCostUsd: input.exactCostUsd,
22197
+ durationMs: Date.now() - input.startedAt,
22198
+ timestamp: Date.now(),
22199
+ ...options.callId !== undefined ? { callId: options.callId } : {},
22200
+ ...options.scopeId !== undefined ? { scopeId: options.scopeId } : {}
22201
+ };
22202
+ }
22203
+ function buildDispatchErrorEvent(input) {
22204
+ return {
22205
+ kind: "error",
22206
+ origin: input.origin,
22207
+ agentName: input.agentName,
22208
+ stage: input.stage,
22209
+ storyId: input.storyId,
22210
+ errorCode: input.error instanceof NaxError ? input.error.code : "DISPATCH_ERROR",
22211
+ errorMessage: errorMessage(input.error),
22212
+ prompt: input.prompt,
22213
+ durationMs: Date.now() - input.startedAt,
22214
+ timestamp: Date.now(),
22215
+ resolvedPermissions: input.resolvedPermissions,
22216
+ ...input.callId !== undefined ? { callId: input.callId } : {},
22217
+ ...input.scopeId !== undefined ? { scopeId: input.scopeId } : {}
22218
+ };
22219
+ }
22220
+ var init_manager_dispatch = __esm(() => {
22221
+ init_errors();
22222
+ });
22223
+
22111
22224
  // src/agents/retry/default-strategy.ts
22112
22225
  var MAX_RETRIES = 3, defaultRetryStrategy;
22113
22226
  var init_default_strategy = __esm(() => {
@@ -22735,52 +22848,33 @@ class AgentManager {
22735
22848
  ...rawResult,
22736
22849
  protocolIds: rawResult.protocolIds ?? handle.protocolIds
22737
22850
  };
22738
- const event = {
22739
- kind: "session-turn",
22740
- sessionName: handle.id,
22851
+ const event = buildSessionTurnEvent({
22852
+ handle,
22741
22853
  sessionRole,
22742
22854
  prompt,
22743
- response: result.output,
22855
+ result,
22744
22856
  agentName,
22745
22857
  stage,
22746
- storyId: opts.storyId,
22747
- featureName: opts.featureName,
22748
- workdir: opts.workdir,
22749
- projectDir: opts.projectDir,
22858
+ opts,
22750
22859
  resolvedPermissions,
22751
- tokenUsage: result.tokenUsage,
22752
- estimatedCostUsd: result.estimatedCostUsd,
22753
- exactCostUsd: result.exactCostUsd,
22754
- durationMs: Date.now() - start,
22755
- timestamp: Date.now(),
22756
- turn: result.internalRoundTrips ?? 1,
22757
- protocolIds: {
22758
- sessionId: handle.protocolIds?.sessionId ?? null,
22759
- recordId: handle.protocolIds?.recordId ?? null
22760
- },
22761
- ...result.interactions?.length ? { interactions: result.interactions } : {},
22762
- origin: "runAsSession",
22763
- ...opts.callId !== undefined ? { callId: opts.callId } : {},
22764
- ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
22765
- };
22860
+ profile: this._config.profile,
22861
+ startedAt: start
22862
+ });
22766
22863
  this._dispatchEvents.emitDispatch(event);
22767
22864
  return result;
22768
22865
  } catch (err) {
22769
- const errEvent = {
22770
- kind: "error",
22866
+ const errEvent = buildDispatchErrorEvent({
22771
22867
  origin: "runAsSession",
22772
22868
  agentName,
22773
22869
  stage,
22774
22870
  storyId: opts.storyId,
22775
- errorCode: err instanceof NaxError ? err.code : "DISPATCH_ERROR",
22776
- errorMessage: errorMessage(err),
22871
+ error: err,
22777
22872
  prompt,
22778
- durationMs: Date.now() - start,
22779
- timestamp: Date.now(),
22780
22873
  resolvedPermissions,
22781
- ...opts.callId !== undefined ? { callId: opts.callId } : {},
22782
- ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
22783
- };
22874
+ callId: opts.callId,
22875
+ scopeId: opts.scopeId,
22876
+ startedAt: start
22877
+ });
22784
22878
  this._dispatchEvents.emitDispatchError(errEvent);
22785
22879
  throw err;
22786
22880
  }
@@ -22799,44 +22893,35 @@ class AgentManager {
22799
22893
  const start = Date.now();
22800
22894
  try {
22801
22895
  const outcome = await this.completeWithFallback(prompt, augmented, agentName);
22802
- const event = {
22803
- kind: "complete",
22896
+ const event = buildCompleteEvent({
22804
22897
  sessionName,
22805
- sessionRole: options.sessionRole ?? "auto",
22806
22898
  prompt,
22807
22899
  response: outcome.result.output,
22808
22900
  agentName,
22809
22901
  stage,
22810
- storyId: options.storyId,
22811
- featureName: options.featureName,
22812
- workdir: options.workdir,
22902
+ options,
22813
22903
  resolvedPermissions,
22814
22904
  tokenUsage: outcome.result.tokenUsage,
22815
22905
  estimatedCostUsd: outcome.result.estimatedCostUsd,
22816
22906
  exactCostUsd: outcome.result.exactCostUsd,
22817
- durationMs: Date.now() - start,
22818
- timestamp: Date.now(),
22819
- ...options.callId !== undefined ? { callId: options.callId } : {},
22820
- ...options.scopeId !== undefined ? { scopeId: options.scopeId } : {}
22821
- };
22907
+ profile: this._config.profile,
22908
+ startedAt: start
22909
+ });
22822
22910
  this._dispatchEvents.emitDispatch(event);
22823
22911
  return outcome.result;
22824
22912
  } catch (err) {
22825
- const errEvent = {
22826
- kind: "error",
22913
+ const errEvent = buildDispatchErrorEvent({
22827
22914
  origin: "completeAs",
22828
22915
  agentName,
22829
22916
  stage,
22830
22917
  storyId: options.storyId,
22831
- errorCode: err instanceof NaxError ? err.code : "DISPATCH_ERROR",
22832
- errorMessage: errorMessage(err),
22918
+ error: err,
22833
22919
  prompt,
22834
- durationMs: Date.now() - start,
22835
- timestamp: Date.now(),
22836
22920
  resolvedPermissions,
22837
- ...options.callId !== undefined ? { callId: options.callId } : {},
22838
- ...options.scopeId !== undefined ? { scopeId: options.scopeId } : {}
22839
- };
22921
+ callId: options.callId,
22922
+ scopeId: options.scopeId,
22923
+ startedAt: start
22924
+ });
22840
22925
  this._dispatchEvents.emitDispatchError(errEvent);
22841
22926
  throw err;
22842
22927
  }
@@ -22859,6 +22944,7 @@ var init_manager = __esm(() => {
22859
22944
  init_dispatch_events();
22860
22945
  init_session_name();
22861
22946
  init_bun_deps();
22947
+ init_manager_dispatch();
22862
22948
  init_registry();
22863
22949
  init_default_strategy();
22864
22950
  init_hop_retry_policy();
@@ -22947,11 +23033,38 @@ var init_compose = __esm(() => {
22947
23033
 
22948
23034
  // src/review/truncation.ts
22949
23035
  function looksLikeTruncatedJson(raw) {
22950
- return raw.trimEnd().length >= MAX_AGENT_OUTPUT_CHARS - 100;
23036
+ const text = raw.trimEnd();
23037
+ if (text.length === 0)
23038
+ return false;
23039
+ let depth = 0;
23040
+ let inString = false;
23041
+ let escaped = false;
23042
+ let opened = false;
23043
+ for (const ch of text) {
23044
+ if (escaped) {
23045
+ escaped = false;
23046
+ continue;
23047
+ }
23048
+ if (inString) {
23049
+ if (ch === "\\")
23050
+ escaped = true;
23051
+ else if (ch === '"')
23052
+ inString = false;
23053
+ continue;
23054
+ }
23055
+ if (ch === '"') {
23056
+ inString = true;
23057
+ continue;
23058
+ }
23059
+ if (ch === "{" || ch === "[") {
23060
+ depth++;
23061
+ opened = true;
23062
+ } else if (ch === "}" || ch === "]") {
23063
+ depth--;
23064
+ }
23065
+ }
23066
+ return opened && (inString || depth > 0);
22951
23067
  }
22952
- var init_truncation = __esm(() => {
22953
- init_adapter();
22954
- });
22955
23068
 
22956
23069
  // src/utils/llm-json.ts
22957
23070
  function extractJsonFromMarkdown(text) {
@@ -23094,9 +23207,9 @@ function makeParseRetryStrategy(opts) {
23094
23207
  }
23095
23208
  };
23096
23209
  }
23210
+ var UNPARSED_PREVIEW_BYTES = 600;
23097
23211
  var init_parse_retry = __esm(() => {
23098
23212
  init_logger2();
23099
- init_truncation();
23100
23213
  init_types4();
23101
23214
  });
23102
23215
 
@@ -23114,7 +23227,7 @@ function makeTieredParseRetryStrategy(opts) {
23114
23227
  if (attempt >= opts.maxAttempts - 1) {
23115
23228
  return { retry: false, fallback: opts.exhaustedFallback(inspection, ctx.lastOutput) };
23116
23229
  }
23117
- const isTruncated = ctx.lastOutput.trimEnd().length >= MAX_AGENT_OUTPUT_CHARS - 100;
23230
+ const isTruncated = looksLikeTruncatedJson(ctx.lastOutput);
23118
23231
  const logger = opts._logger ?? getSafeLogger();
23119
23232
  logger?.warn(opts.reviewerKind, `Parse retry \u2014 ${inspection.kind ?? "unknown"}`, {
23120
23233
  storyId: ctx.storyId,
@@ -23128,7 +23241,6 @@ function makeTieredParseRetryStrategy(opts) {
23128
23241
  }
23129
23242
  var init_tiered_parse_retry = __esm(() => {
23130
23243
  init_logger2();
23131
- init_adapter();
23132
23244
  init_types4();
23133
23245
  });
23134
23246
 
@@ -23522,6 +23634,56 @@ var init_digest = __esm(() => {
23522
23634
  SCOPE_ORDER2 = ["project", "feature", "story", "session", "retrieved"];
23523
23635
  });
23524
23636
 
23637
+ // src/context/engine/manifest-builder.ts
23638
+ function buildManifest(inputs) {
23639
+ const {
23640
+ requestId,
23641
+ request,
23642
+ packed,
23643
+ usedTokens,
23644
+ digestTokens: digestTokens2,
23645
+ buildMs,
23646
+ providerResults,
23647
+ roleFiltered,
23648
+ belowMin,
23649
+ dedupeDropped,
23650
+ budgetExcludedIds,
23651
+ floorPackedIds,
23652
+ floorOverageIds
23653
+ } = inputs;
23654
+ const staleChunkIds = packed.filter((c) => c.staleCandidate).map((c) => c.id);
23655
+ const chunkSummaries = {};
23656
+ const chunkTokens = {};
23657
+ for (const c of packed) {
23658
+ chunkSummaries[c.id] = c.content.slice(0, CHUNK_SUMMARY_CHARS);
23659
+ chunkTokens[c.id] = c.tokens;
23660
+ }
23661
+ return {
23662
+ requestId,
23663
+ stage: request.stage,
23664
+ totalBudgetTokens: request.budgetTokens,
23665
+ usedTokens: usedTokens + digestTokens2,
23666
+ includedChunks: packed.map((c) => c.id),
23667
+ excludedChunks: [
23668
+ ...roleFiltered.map((c) => ({ id: c.id, reason: "role-filter" })),
23669
+ ...belowMin.map((c) => ({ id: c.id, reason: "below-min-score" })),
23670
+ ...dedupeDropped.map((id) => ({ id, reason: "dedupe" })),
23671
+ ...budgetExcludedIds.map((id) => ({ id, reason: "budget" }))
23672
+ ],
23673
+ floorItems: floorPackedIds,
23674
+ floorOverageItems: floorOverageIds.length > 0 ? floorOverageIds : undefined,
23675
+ digestTokens: digestTokens2,
23676
+ buildMs,
23677
+ providerResults,
23678
+ repoRoot: request.repoRoot,
23679
+ packageDir: request.packageDir,
23680
+ ...Object.keys(chunkSummaries).length > 0 && { chunkSummaries },
23681
+ ...Object.keys(chunkTokens).length > 0 && { chunkTokens },
23682
+ ...staleChunkIds.length > 0 && { staleChunks: staleChunkIds }
23683
+ };
23684
+ }
23685
+ var CHUNK_SUMMARY_CHARS = 300;
23686
+
23525
23687
  // src/context/engine/packing.ts
23526
23688
  function packChunks(chunks, budgetTokens, availableBudgetTokens) {
23527
23689
  const effectiveBudget = availableBudgetTokens !== undefined ? Math.min(budgetTokens, availableBudgetTokens) : budgetTokens;
@@ -25742,7 +25904,7 @@ async function captureWorkingTreeChanges(workdir, baseRef, scopePrefix) {
25742
25904
  return [];
25743
25905
  const runDiff = async (args) => {
25744
25906
  const fullArgs = scopePrefix ? [...args, "--", `${scopePrefix}/`] : args;
25745
- const { stdout, exitCode } = await gitWithTimeout(fullArgs, workdir, TIMEOUT_RETRY_GIT_TIMEOUT_MS);
25907
+ const { stdout, exitCode } = await gitWithTimeout(fullArgs, workdir, _gitDeps.timeoutRetryGitTimeoutMs);
25746
25908
  if (exitCode !== 0)
25747
25909
  return [];
25748
25910
  return stdout.trim().split(`
@@ -25791,16 +25953,20 @@ async function captureDiffSummary(workdir, baseRef, scopePrefix) {
25791
25953
  return "";
25792
25954
  }
25793
25955
  }
25794
- var _gitDeps, GIT_TIMEOUT_MS = 1e4, TIMEOUT_RETRY_GIT_TIMEOUT_MS = 3000;
25956
+ var GIT_TIMEOUT_MS = 1e4, TIMEOUT_RETRY_GIT_TIMEOUT_MS = 3000, _gitDeps;
25795
25957
  var init_git = __esm(() => {
25796
25958
  init_logger2();
25797
25959
  init_bun_deps();
25798
- _gitDeps = { spawn, getSafeLogger };
25960
+ _gitDeps = {
25961
+ spawn,
25962
+ getSafeLogger,
25963
+ timeoutRetryGitTimeoutMs: TIMEOUT_RETRY_GIT_TIMEOUT_MS
25964
+ };
25799
25965
  });
25800
25966
 
25801
25967
  // src/utils/path-filters.ts
25802
25968
  import { join as join8, relative as relative2 } from "path";
25803
- function basename3(path) {
25969
+ function basename4(path) {
25804
25970
  const stripped = path.startsWith("./") ? path.slice(2) : path;
25805
25971
  const idx = stripped.lastIndexOf("/");
25806
25972
  return idx === -1 ? stripped : stripped.slice(idx + 1);
@@ -25930,7 +26096,7 @@ function isNaxInternalPath(path) {
25930
26096
  return true;
25931
26097
  if (path.includes("/.nax/"))
25932
26098
  return true;
25933
- return LOCKFILE_BASENAMES.has(basename3(path));
26099
+ return LOCKFILE_BASENAMES.has(basename4(path));
25934
26100
  }
25935
26101
  function filterNaxInternalPaths(paths, ignoreMatchers = []) {
25936
26102
  return paths.filter((path) => !isNaxInternalPath(path) && !ignoreMatchers.some((matcher) => matcher.test(path)));
@@ -25974,8 +26140,8 @@ async function getGitRootMemo(workdir) {
25974
26140
  return result ?? null;
25975
26141
  }
25976
26142
  function extractBasenamePattern(pattern) {
25977
- const basename4 = pattern.slice(pattern.lastIndexOf("/") + 1);
25978
- const parts = basename4.split("*");
26143
+ const basename5 = pattern.slice(pattern.lastIndexOf("/") + 1);
26144
+ const parts = basename5.split("*");
25979
26145
  if (parts.length !== 2)
25980
26146
  return null;
25981
26147
  const [prefix, suffix] = parts;
@@ -25987,8 +26153,8 @@ function extractSearchTerms(sourceFile) {
25987
26153
  const withoutPrefix = sourceFile.replace(/^(?:.*\/)?src\//, "");
25988
26154
  const withoutExt = withoutPrefix.replace(/\.[^.]+$/, "");
25989
26155
  const parts = withoutExt.split("/");
25990
- const basename4 = parts[parts.length - 1];
25991
- return [`/${basename4}`, withoutExt];
26156
+ const basename5 = parts[parts.length - 1];
26157
+ return [`/${basename5}`, withoutExt];
25992
26158
  }
25993
26159
  async function importGrepFallback(sourceFiles, workdir, testFilePatterns, maxScanFiles = MAX_GREP_TEST_FILES) {
25994
26160
  if (sourceFiles.length === 0 || testFilePatterns.length === 0)
@@ -27808,33 +27974,21 @@ class ContextOrchestrator {
27808
27974
  const digest = buildDigest(packed);
27809
27975
  const dTokens = digestTokens(digest);
27810
27976
  const buildMs = _orchestratorDeps.now() - startMs;
27811
- const staleChunkIds = packed.filter((c) => c.staleCandidate).map((c) => c.id);
27812
- const chunkSummaries = {};
27813
- for (const c of packed) {
27814
- chunkSummaries[c.id] = c.content.slice(0, 300);
27815
- }
27816
- const manifest = {
27977
+ const manifest = buildManifest({
27817
27978
  requestId,
27818
- stage: request.stage,
27819
- totalBudgetTokens: request.budgetTokens,
27820
- usedTokens: usedTokens + dTokens,
27821
- includedChunks: packed.map((c) => c.id),
27822
- excludedChunks: [
27823
- ...roleFiltered.map((c) => ({ id: c.id, reason: "role-filter" })),
27824
- ...belowMin.map((c) => ({ id: c.id, reason: "below-min-score" })),
27825
- ...dedupeDropped.map((id) => ({ id, reason: "dedupe" })),
27826
- ...budgetExcludedIds.map((id) => ({ id, reason: "budget" }))
27827
- ],
27828
- floorItems: floorPackedIds,
27829
- floorOverageItems: floorOverageIds.length > 0 ? floorOverageIds : undefined,
27979
+ request,
27980
+ packed,
27981
+ usedTokens,
27830
27982
  digestTokens: dTokens,
27831
27983
  buildMs,
27832
27984
  providerResults,
27833
- repoRoot: request.repoRoot,
27834
- packageDir: request.packageDir,
27835
- ...Object.keys(chunkSummaries).length > 0 && { chunkSummaries },
27836
- ...staleChunkIds.length > 0 && { staleChunks: staleChunkIds }
27837
- };
27985
+ roleFiltered,
27986
+ belowMin,
27987
+ dedupeDropped,
27988
+ budgetExcludedIds,
27989
+ floorPackedIds,
27990
+ floorOverageIds
27991
+ });
27838
27992
  logger.debug("context-v2", "Bundle assembled", {
27839
27993
  storyId: request.storyId,
27840
27994
  stage: request.stage,
@@ -27891,6 +28045,7 @@ class ContextOrchestrator {
27891
28045
  ...prior.manifest,
27892
28046
  requestId: _orchestratorDeps.uuid(),
27893
28047
  includedChunks: packedChunks.map((c) => c.id),
28048
+ chunkTokens: Object.fromEntries(packedChunks.map((c) => [c.id, c.tokens])),
27894
28049
  usedTokens: Math.max(0, prior.manifest.usedTokens - prior.manifest.digestTokens + dTokens + extraTokens),
27895
28050
  digestTokens: dTokens,
27896
28051
  buildMs: 0,
@@ -27928,7 +28083,7 @@ var init_orchestrator = __esm(() => {
27928
28083
  });
27929
28084
 
27930
28085
  // src/context/rules/canonical-loader.ts
27931
- import { basename as basename4, join as join12 } from "path";
28086
+ import { basename as basename5, join as join12 } from "path";
27932
28087
  function parseRuleAllowMarker(line) {
27933
28088
  const allowed = new Set;
27934
28089
  RULE_ALLOW_MARKER.lastIndex = 0;
@@ -28068,7 +28223,7 @@ async function loadCanonicalRules(workdir, options = {}) {
28068
28223
  const filePaths = allFilePaths.filter((filePath) => {
28069
28224
  const normalized = filePath.replaceAll("\\", "/");
28070
28225
  const normalizedRulesDir = rulesDir.replaceAll("\\", "/");
28071
- const relativePath = normalized.startsWith(`${normalizedRulesDir}/`) ? normalized.slice(normalizedRulesDir.length + 1) : basename4(normalized);
28226
+ const relativePath = normalized.startsWith(`${normalizedRulesDir}/`) ? normalized.slice(normalizedRulesDir.length + 1) : basename5(normalized);
28072
28227
  return relativePath.split("/").length <= 2;
28073
28228
  });
28074
28229
  if (allFilePaths.length > filePaths.length) {
@@ -28084,8 +28239,8 @@ async function loadCanonicalRules(workdir, options = {}) {
28084
28239
  for (const filePath of filePaths) {
28085
28240
  const normalizedPath = filePath.replaceAll("\\", "/");
28086
28241
  const normalizedRulesDir = rulesDir.replaceAll("\\", "/");
28087
- const relativePath = normalizedPath.startsWith(`${normalizedRulesDir}/`) ? normalizedPath.slice(normalizedRulesDir.length + 1) : basename4(normalizedPath);
28088
- const fileName = basename4(filePath);
28242
+ const relativePath = normalizedPath.startsWith(`${normalizedRulesDir}/`) ? normalizedPath.slice(normalizedRulesDir.length + 1) : basename5(normalizedPath);
28243
+ const fileName = basename5(filePath);
28089
28244
  let content;
28090
28245
  try {
28091
28246
  content = await _canonicalLoaderDeps.readFile(filePath);
@@ -29747,8 +29902,8 @@ function deriveTestPatterns(contextFiles, resolvedGlobs) {
29747
29902
  const suffixes = resolvedGlobs ? extractGlobSuffixes(resolvedGlobs) : DEFAULT_TS_DERIVE_SUFFIXES;
29748
29903
  const effectiveSuffixes = suffixes.length > 0 ? suffixes : DEFAULT_TS_DERIVE_SUFFIXES;
29749
29904
  for (const filePath of contextFiles) {
29750
- const basename5 = path.basename(filePath);
29751
- const basenameNoExt = basename5.replace(/\.[^.]+$/, "");
29905
+ const basename6 = path.basename(filePath);
29906
+ const basenameNoExt = basename6.replace(/\.[^.]+$/, "");
29752
29907
  for (const suffix of effectiveSuffixes) {
29753
29908
  patterns.add(`${basenameNoExt}${suffix}`);
29754
29909
  }
@@ -29810,8 +29965,8 @@ async function scanTestFiles(options) {
29810
29965
  const files = [];
29811
29966
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
29812
29967
  if (allowedBasenames !== null) {
29813
- const basename5 = path.basename(filePath);
29814
- if (!allowedBasenames.has(basename5)) {
29968
+ const basename6 = path.basename(filePath);
29969
+ if (!allowedBasenames.has(basename6)) {
29815
29970
  continue;
29816
29971
  }
29817
29972
  }
@@ -31570,6 +31725,11 @@ isolation scope: Only create or modify files in the test/ directory. Tests must
31570
31725
  isolation scope: Create test files in test/. MAY read src/ files and MAY import from src/ to ensure correct types/interfaces. May create minimal stubs in src/ if needed to make imports work, but do NOT implement real logic.${footer}`;
31571
31726
  }
31572
31727
  if (role === "implementer") {
31728
+ if (mode === "lite") {
31729
+ return `${header}
31730
+
31731
+ isolation scope: Implement source code in src/ to make tests pass. You MAY add tests for acceptance criteria that have no coverage yet; do NOT weaken, delete, or skip existing tests. Run tests frequently to track progress.${footer}`;
31732
+ }
31573
31733
  return `${header}
31574
31734
 
31575
31735
  isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
@@ -32136,12 +32296,36 @@ Include the story ID when known \u2014 \`feat(<story-id>): <description>\`.
32136
32296
  When the story is ambiguous, pick an interpretation, proceed, and document the choice in the commit body under \`Assumptions:\`. Do not invent requirements; do not silently choose when the story is genuinely under-specified \u2014 note it.`;
32137
32297
  }
32138
32298
 
32299
+ // src/prompts/sections/test-quality.ts
32300
+ function buildTestQualitySection(role, variant, storyId) {
32301
+ const authors = AUTHORING_ROLES.has(role) || role === "implementer" && variant === "lite";
32302
+ if (!authors)
32303
+ return "";
32304
+ const storyIdLine = storyId ? `
32305
+ - Test names must use THIS story's ID (${storyId}) \u2014 never a sibling story's ID copied from a nearby test.` : "";
32306
+ return `# Review-Proof Tests
32307
+
32308
+ An adversarial reviewer will audit your tests after implementation and BLOCK the story on any test-gap finding. Each block costs a full rectification round. Write tests that survive that audit the first time:
32309
+
32310
+ - Every acceptance criterion needs a test that INVOKES the code at runtime and asserts its observable behavior (return value, thrown error, rendered/mounted output, emitted event, persisted state).
32311
+ - NEVER write source-inspection tests \u2014 asserting that a file contains a pattern, string, or symbol proves nothing about behavior and WILL be blocked as test-gap.
32312
+ - No placeholder or tautological tests: \`expect(true).toBe(true)\`, asserting on literals, empty bodies, \`.skip\`/\`.todo\` on an AC-covering test \u2014 all blocked as test-gap.
32313
+ - Every new exported symbol an AC depends on must be exercised by at least one test.
32314
+ - Cover the boundary and error paths the reviewer probes: empty/null/zero/negative inputs, and failure modes (errors must surface, not be swallowed).
32315
+ - For UI/page-level ACs, mount the page/component and simulate the interaction \u2014 a test that only exercises the underlying unit reads as no coverage for the AC's wiring.${storyIdLine}`;
32316
+ }
32317
+ var AUTHORING_ROLES;
32318
+ var init_test_quality = __esm(() => {
32319
+ AUTHORING_ROLES = new Set(["test-writer", "single-session", "tdd-simple", "batch"]);
32320
+ });
32321
+
32139
32322
  // src/prompts/sections/index.ts
32140
32323
  var init_sections2 = __esm(() => {
32141
32324
  init_hermetic();
32142
32325
  init_role_task();
32143
32326
  init_story();
32144
32327
  init_acceptance();
32328
+ init_test_quality();
32145
32329
  });
32146
32330
 
32147
32331
  // src/prompts/loader.ts
@@ -32284,7 +32468,7 @@ class TddPromptBuilder {
32284
32468
  if (this.role === "verifier" && this.story_) {
32285
32469
  acc.add(this.s("verdict", buildVerdictSection(this.story_)));
32286
32470
  }
32287
- const isolation = this.options.isolation;
32471
+ const isolation = this.role === "implementer" && this.options.variant === "lite" ? "lite" : this.options.isolation;
32288
32472
  acc.add(this.s("isolation", buildIsolationSection(this.role, isolation, this.testCommand_)));
32289
32473
  const tddLang = buildTddLanguageSection(this.loaderConfig_?.project?.language);
32290
32474
  if (tddLang)
@@ -32300,6 +32484,9 @@ class TddPromptBuilder {
32300
32484
  const guardrails = buildBehavioralGuardrailsSection(this.role, guardrailLevel, guardrailVariant, guardrailIsolation);
32301
32485
  if (guardrails)
32302
32486
  acc.add(this.s("guardrails", guardrails));
32487
+ const testQuality = buildTestQualitySection(this.role, this.options.variant, this.story_?.id);
32488
+ if (testQuality)
32489
+ acc.add(this.s("test-quality", testQuality));
32303
32490
  if (this.role !== "verifier") {
32304
32491
  const selfVerify = buildSelfVerificationSection(this.role, this.selfVerification_);
32305
32492
  if (selfVerify)
@@ -32751,6 +32938,32 @@ var init_debate_builder = __esm(() => {
32751
32938
  RE_REVIEW_JSON_DIRECTIVE = `Respond with JSON: { passed: boolean; findings: Array<${FINDING_SCHEMA}>; findingReasoning: { [ruleId: string]: string }; deltaSummary: string }`;
32752
32939
  });
32753
32940
 
32941
+ // src/review/semantic-categories.ts
32942
+ function isSemanticCategory(value) {
32943
+ return SEMANTIC_CATEGORY_SET.has(value);
32944
+ }
32945
+ function normalizeSemanticCategory(raw) {
32946
+ if (typeof raw !== "string")
32947
+ return "";
32948
+ const normalized = raw.trim().toLowerCase();
32949
+ if (normalized === "")
32950
+ return "";
32951
+ return isSemanticCategory(normalized) ? normalized : "other";
32952
+ }
32953
+ var SEMANTIC_CATEGORIES, SEMANTIC_CATEGORY_SET, SEMANTIC_CATEGORY_ENUM_LINE;
32954
+ var init_semantic_categories = __esm(() => {
32955
+ SEMANTIC_CATEGORIES = [
32956
+ "unimplemented",
32957
+ "partial",
32958
+ "contradiction",
32959
+ "dead-path",
32960
+ "unwired",
32961
+ "other"
32962
+ ];
32963
+ SEMANTIC_CATEGORY_SET = new Set(SEMANTIC_CATEGORIES);
32964
+ SEMANTIC_CATEGORY_ENUM_LINE = SEMANTIC_CATEGORIES.map((c) => `"${c}"`).join(" | ");
32965
+ });
32966
+
32754
32967
  // src/prompts/builders/prior-iterations-builder.ts
32755
32968
  function buildPriorIterationsBlock(iterations) {
32756
32969
  if (iterations.length === 0)
@@ -32809,9 +33022,13 @@ function renderVerdictTemplate(iterations) {
32809
33022
  When outcome is "unchanged", the prior hypothesis is FALSIFIED \u2014 the change did not affect what was tested. Choose a different category before producing a new verdict. Do NOT repeat fixes listed above.` : "";
32810
33023
  return [
32811
33024
  `**Required:** before adding any new finding, classify each of the ${total} prior finding(s) above as one of:`,
32812
- "- `addressed` \u2014 the current diff resolves it (cite the diff line that fixes it in your `message` field)",
32813
- "- `still-blocking` \u2014 the implementer did not fix it; re-flag it with the IDENTICAL `file`, `line`, `category`, and substantively the same `message` wording",
32814
- `- \`never-an-issue\` \u2014 your prior judgment was wrong; explain why in \`message\` and emit severity \`"info"\``,
33025
+ "- `addressed` \u2014 the current diff resolves it; record it in `acks` (not `findings`), citing the diff line that fixes it in `note`",
33026
+ "- `still-blocking` \u2014 the implementer did not fix it; re-flag it in `findings` with the IDENTICAL `file`, `line`, `category`, and substantively the same `message` wording",
33027
+ "- `never-an-issue` \u2014 your prior judgment was wrong; record it in `acks` (not `findings`) and explain why in `note`",
33028
+ "",
33029
+ "Do NOT emit an acknowledgement as a finding. A resolved or withdrawn prior finding is not a defect \u2014",
33030
+ "reporting it as one inflates the finding count and buries the real defects. Only `still-blocking` belongs in `findings`.",
33031
+ "",
32815
33032
  `Then surface any genuinely new findings.${unchangedNote}`
32816
33033
  ].join(`
32817
33034
  `);
@@ -32875,13 +33092,36 @@ Flag issues only when you have confirmed:
32875
33092
  3. New code has dead paths that will never execute (stubs, noops, unreachable branches)
32876
33093
  4. New code is not wired into callers/exports (verified by grepping for usage)
32877
33094
 
32878
- Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.`, SEMANTIC_OUTPUT_SCHEMA = `Respond with JSON only \u2014 no explanation text before or after:
33095
+ Do NOT flag: style issues, naming conventions, import ordering, file length, or anything lint handles.
33096
+
33097
+ **Finding categories \u2014 every finding MUST carry exactly one \`category\`:**
33098
+ - \`unimplemented\` \u2014 an AC has no implementation at all.
33099
+ - \`partial\` \u2014 an AC is implemented for some inputs or paths, but not everything it specifies.
33100
+ - \`contradiction\` \u2014 the implementation does the opposite of what the AC specifies, or something the AC forbids.
33101
+ - \`dead-path\` \u2014 a stub, noop, or unreachable branch that will never execute.
33102
+ - \`unwired\` \u2014 new code exists but is not reachable: not exported, not called, not registered.
33103
+ - \`other\` \u2014 genuinely AC-related, but none of the above.
33104
+
33105
+ Pick the most specific axis that fits. Do not invent categories outside this list \u2014 an unrecognised value is recorded as \`other\` and loses its signal.`, SEMANTIC_OUTPUT_SCHEMA, ReviewPromptBuilder;
33106
+ var init_review_builder = __esm(() => {
33107
+ init_semantic_categories();
33108
+ init_sections2();
33109
+ SEMANTIC_ROLE = "You are a semantic code reviewer with access to the repository files. " + "Your job is to walk each acceptance criterion (AC) and judge whether the production code fulfills it \u2014 fully, partially, or not at all. " + "Test coverage gaps and convention/lint issues are out of scope \u2014 adversarial review and lint/typecheck handle those.";
33110
+ SEMANTIC_OUTPUT_SCHEMA = `Respond with JSON only \u2014 no explanation text before or after:
32879
33111
  {
32880
33112
  "passed": boolean,
32881
33113
  "inspectedFiles": ["relative/path/you/actually/opened.ts"],
33114
+ "acks": [
33115
+ {
33116
+ "priorFinding": "<short identifier \u2014 file:line or a few words of its message>",
33117
+ "status": "addressed" | "never-an-issue",
33118
+ "note": "<why: the diff line that fixes it, or why the prior judgment was wrong>"
33119
+ }
33120
+ ],
32882
33121
  "findings": [
32883
33122
  {
32884
33123
  "severity": "error" | "warning" | "info" | "unverifiable",
33124
+ "category": ${SEMANTIC_CATEGORY_ENUM_LINE},
32885
33125
  "file": "path/to/file",
32886
33126
  "line": 42,
32887
33127
  "issue": "description of the issue",
@@ -32899,14 +33139,13 @@ Do NOT flag: style issues, naming conventions, import ordering, file length, or
32899
33139
  }
32900
33140
 
32901
33141
  Notes:
33142
+ - \`acks\` records prior findings you are NOT re-flagging \u2014 resolved by the diff, or withdrawn. Omit it (or use \`[]\`) when there are no prior findings. An acknowledgement must never appear in \`findings\`.
33143
+ - \`category\` is required on every finding \u2014 one of the axes listed above.
32902
33144
  - \`acIndex\` is required when severity is "error" (1-based, into the Acceptance Criteria list above).
32903
33145
  - \`acQuote\` is optional advisory metadata for human auditors \u2014 not validated.
32904
33146
  - Omit both for "warning", "info", "unverifiable".
32905
33147
  - \`inspectedFiles\` must list the relative paths you actually opened while reviewing. A \`passed:true\` verdict with an empty or absent \`inspectedFiles\` is invalid \u2014 walk each AC against the real files before passing.
32906
- If all ACs are correctly implemented after inspecting the code, respond with { "passed": true, "inspectedFiles": ["..."], "findings": [] }.`, ReviewPromptBuilder;
32907
- var init_review_builder = __esm(() => {
32908
- init_sections2();
32909
- SEMANTIC_ROLE = "You are a semantic code reviewer with access to the repository files. " + "Your job is to walk each acceptance criterion (AC) and judge whether the production code fulfills it \u2014 fully, partially, or not at all. " + "Test coverage gaps and convention/lint issues are out of scope \u2014 adversarial review and lint/typecheck handle those.";
33148
+ If all ACs are correctly implemented after inspecting the code, respond with { "passed": true, "inspectedFiles": ["..."], "findings": [] }.`;
32910
33149
  ReviewPromptBuilder = class ReviewPromptBuilder {
32911
33150
  buildSemanticReviewPrompt(story, semanticConfig, options) {
32912
33151
  const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
@@ -32956,7 +33195,7 @@ Respond with a condensed summary:
32956
33195
  - ${advisoryClause}
32957
33196
  - Keep \`verifiedBy\` for every finding. If \`verifiedBy.observed\` is long, abbreviate it to one line \u2014 never drop the field.
32958
33197
  Output ONLY a complete, valid JSON object. It must start with { and end with }.
32959
- Schema: {"passed": boolean, "findings": [{"severity": string, "category": string, "file": string, "line": number, "issue": string, "suggestion": string, "verifiedBy": {"command": string, "file": string, "line": number, "observed": string}}]}`;
33198
+ Schema: {"passed": boolean, "findings": [{"severity": string, "category": ${SEMANTIC_CATEGORY_ENUM_LINE}, "file": string, "line": number, "issue": string, "suggestion": string, "verifiedBy": {"command": string, "file": string, "line": number, "observed": string}}]}`;
32960
33199
  }
32961
33200
  static demandInspection() {
32962
33201
  return `Your previous review returned \`passed:true\` with no findings and an empty (or absent) \`inspectedFiles\` list. That means you did not open any of the changed files \u2014 a verdict reached without reading the code is not valid.
@@ -33011,6 +33250,7 @@ ${drops.map((d, i) => `${i + 1}. [${d.finding.severity}] ${d.finding.issue}`).jo
33011
33250
  Please re-review the code and re-issue any valid findings. For each finding you re-issue:
33012
33251
  - You MUST include a valid \`acIndex\` (1-based index into the AC list below)
33013
33252
  - You MUST include a \`verifiedBy\` field with verified evidence
33253
+ - You MUST include a \`category\`: ${SEMANTIC_CATEGORY_ENUM_LINE}
33014
33254
 
33015
33255
  ## Acceptance Criteria
33016
33256
  ${acList}
@@ -33145,6 +33385,7 @@ What new exported units lack corresponding test files?
33145
33385
  - Bodies that always pass: \`expect(true).toBe(true)\`, \`expect(x).toBe(x)\`, \`expect(1).toBe(1)\`, an empty test body, or \`assert(true)\`.
33146
33386
  - Tests skipped/disabled (\`it.skip\`, \`test.todo\`, \`xit\`, commented-out assertions) that an AC depends on.
33147
33387
  - Assertions that never exercise the implementation (e.g. asserting on a literal, not on a value the production code produced).
33388
+ - Source-inspection tests: reading a source file and asserting it contains a pattern, string, or symbol instead of invoking the code and asserting its runtime behavior.
33148
33389
 
33149
33390
  For each such finding: set \`acIndex\` to the AC the fake test purports to cover, \`acQuote\` to a verbatim substring of that AC, and \`verifiedBy.observed\` to the placeholder line itself (e.g. \`expect(true).toBe(true)\`). Do **not** downgrade these to \`warning\` \u2014 a green suite built on placeholder assertions is a failing implementation with hidden evidence.
33150
33391
 
@@ -33168,6 +33409,13 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
33168
33409
  {
33169
33410
  "passed": true | false,
33170
33411
  "inspectedFiles": ["relative/path/you/actually/opened.ts"],
33412
+ "acks": [
33413
+ {
33414
+ "priorFinding": "<short identifier of the prior finding \u2014 its file:line or a few words of its message>",
33415
+ "status": "addressed" | "never-an-issue",
33416
+ "note": "<why: the diff line that fixes it, or why the prior judgment was wrong>"
33417
+ }
33418
+ ],
33171
33419
  "findings": [
33172
33420
  {
33173
33421
  "severity": "error" | "warning" | "info" | "unverifiable",
@@ -33194,6 +33442,8 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
33194
33442
 
33195
33443
  **No rubber-stamping:** \`inspectedFiles\` must list the relative paths you actually opened with your tools while reviewing. A \`passed:true\` verdict with an empty or absent \`inspectedFiles\` is invalid \u2014 it means you never looked at the code. Fetch the diff and open the changed files before forming any verdict.
33196
33444
 
33445
+ \`acks\` records prior findings you are NOT re-flagging \u2014 resolved by the diff, or withdrawn. Omit it (or use \`[]\`) when there are no prior findings. An acknowledgement must never appear in \`findings\`.
33446
+
33197
33447
  Severity guide:
33198
33448
  - \`"error"\`: confident this will cause real failure or regression
33199
33449
  - \`"warning"\`: fragile or incomplete but may ship without immediate breakage
@@ -33738,8 +33988,8 @@ function stripMarkdownInline(s) {
33738
33988
  function extractLocusKeywords(finding) {
33739
33989
  const keywords = [];
33740
33990
  if (finding.file) {
33741
- const basename5 = finding.file.split("/").pop() ?? "";
33742
- const stem = basename5.replace(/\.[^.]+$/, "");
33991
+ const basename6 = finding.file.split("/").pop() ?? "";
33992
+ const stem = basename6.replace(/\.[^.]+$/, "");
33743
33993
  for (const part of stem.split(/[-_]/)) {
33744
33994
  if (part.length >= 3)
33745
33995
  keywords.push(part.toLowerCase());
@@ -35888,6 +36138,35 @@ var init_acceptance_fix = __esm(() => {
35888
36138
  };
35889
36139
  });
35890
36140
 
36141
+ // src/review/acks.ts
36142
+ function extractAcks(raw) {
36143
+ if (!Array.isArray(raw))
36144
+ return [];
36145
+ const acks = [];
36146
+ for (const entry of raw) {
36147
+ if (acks.length >= MAX_ACKS)
36148
+ break;
36149
+ if (typeof entry === "string") {
36150
+ if (entry !== "")
36151
+ acks.push({ priorFinding: entry, status: "unknown" });
36152
+ continue;
36153
+ }
36154
+ if (typeof entry !== "object" || entry === null)
36155
+ continue;
36156
+ const e = entry;
36157
+ const known = e.status === "addressed" || e.status === "never-an-issue";
36158
+ const note = typeof e.note === "string" ? e.note.slice(0, MAX_NOTE_CHARS) : "";
36159
+ acks.push({
36160
+ priorFinding: typeof e.priorFinding === "string" ? e.priorFinding : "",
36161
+ status: known ? e.status : "unknown",
36162
+ ...note !== "" && { note },
36163
+ ...!known && typeof e.status === "string" && { rawStatus: e.status }
36164
+ });
36165
+ }
36166
+ return acks;
36167
+ }
36168
+ var MAX_ACKS = 50, MAX_NOTE_CHARS = 500;
36169
+
35891
36170
  // src/review/ac-structural-counterfactual.ts
35892
36171
  function analyzeStructuralCounterfactual(finding, acceptanceCriteria, diffFiles) {
35893
36172
  const acIndexInRange = typeof finding.acIndex === "number" && finding.acIndex >= 1 && finding.acIndex <= acceptanceCriteria.length;
@@ -35925,7 +36204,12 @@ function validateAdversarialShape(parsed) {
35925
36204
  return null;
35926
36205
  if (!Array.isArray(obj.findings))
35927
36206
  return null;
35928
- return { passed: obj.passed, findings: obj.findings };
36207
+ const acks = extractAcks(obj.acks);
36208
+ return {
36209
+ passed: obj.passed,
36210
+ findings: obj.findings,
36211
+ ...acks.length > 0 && { acks }
36212
+ };
35929
36213
  }
35930
36214
  function formatFindings(findings) {
35931
36215
  return findings.map((f) => `[${f.severity}][${f.category}] ${f.file}:${f.line} \u2014 ${f.issue}
@@ -35980,7 +36264,21 @@ function validateLLMShape(parsed) {
35980
36264
  return null;
35981
36265
  if (!Array.isArray(obj.findings))
35982
36266
  return null;
35983
- return { passed: obj.passed, findings: obj.findings };
36267
+ const acks = extractAcks(obj.acks);
36268
+ return {
36269
+ passed: obj.passed,
36270
+ findings: obj.findings.filter(isFindingShaped).map(withNormalizedCategory),
36271
+ ...acks.length > 0 && { acks }
36272
+ };
36273
+ }
36274
+ function isFindingShaped(f) {
36275
+ return typeof f === "object" && f !== null && !Array.isArray(f);
36276
+ }
36277
+ function withNormalizedCategory(f) {
36278
+ const category = normalizeSemanticCategory(f.category);
36279
+ if (category === "")
36280
+ return f;
36281
+ return { ...f, category };
35984
36282
  }
35985
36283
  function parseLLMResponse(raw) {
35986
36284
  try {
@@ -36036,7 +36334,7 @@ function llmFindingToFinding(f, opts = {}) {
36036
36334
  return {
36037
36335
  source: "semantic-review",
36038
36336
  severity: normalizeSeverity2(f.severity),
36039
- category: "",
36337
+ category: normalizeSemanticCategory(f.category),
36040
36338
  file: f.file,
36041
36339
  line: f.line,
36042
36340
  message: f.issue,
@@ -36051,6 +36349,7 @@ function toReviewFindings(findings, opts = {}) {
36051
36349
  var UNVERIFIED_FINDING_PATTERNS;
36052
36350
  var init_semantic_helpers = __esm(() => {
36053
36351
  init_category_fix_target();
36352
+ init_semantic_categories();
36054
36353
  init_severity();
36055
36354
  UNVERIFIED_FINDING_PATTERNS = [
36056
36355
  "cannot verify",
@@ -36189,6 +36488,89 @@ var init_finding_filters = __esm(() => {
36189
36488
  init_ac_quote_validator();
36190
36489
  });
36191
36490
 
36491
+ // src/review/recurrence-demotion.ts
36492
+ function normalizeIssueText(s) {
36493
+ return s.replace(/`/g, "").replace(/\s+/g, " ").trim().toLowerCase().slice(0, MAX_ISSUE_PREFIX);
36494
+ }
36495
+ function normalizeFingerprintPath(file3) {
36496
+ return (file3 ?? "").replace(/\\/g, "/").replace(/^(?:\.{1,2}\/)+/, "");
36497
+ }
36498
+ function fingerprintFor(file3, category, text, acIndex) {
36499
+ const normFile = normalizeFingerprintPath(file3);
36500
+ if (typeof acIndex === "number" && Number.isInteger(acIndex) && acIndex >= 1) {
36501
+ return `${normFile}|ac${acIndex}`;
36502
+ }
36503
+ return `${normFile}|${category ?? ""}|${normalizeIssueText(text).slice(0, FP_ISSUE_PREFIX)}`;
36504
+ }
36505
+ function lookupPriorAppearance(priorCounts, finding) {
36506
+ const acKey = finding.acIndex === undefined ? undefined : priorCounts.get(fingerprintFor(finding.file, finding.category, finding.issue, finding.acIndex));
36507
+ const proseKey = priorCounts.get(fingerprintFor(finding.file, finding.category, finding.issue));
36508
+ if (!acKey)
36509
+ return proseKey;
36510
+ if (!proseKey)
36511
+ return acKey;
36512
+ return acKey.count >= proseKey.count ? acKey : proseKey;
36513
+ }
36514
+ function countPriorAppearances(priorIterations, source = "adversarial-review") {
36515
+ const counts = new Map;
36516
+ for (const it of priorIterations) {
36517
+ const seenThisIter = new Map;
36518
+ for (const f of it.findingsAfter ?? []) {
36519
+ if (f.source !== source)
36520
+ continue;
36521
+ const acIndex = typeof f.meta?.acIndex === "number" ? f.meta.acIndex : undefined;
36522
+ seenThisIter.set(fingerprintFor(f.file, f.category, f.message), f.severity);
36523
+ if (acIndex !== undefined) {
36524
+ seenThisIter.set(fingerprintFor(f.file, f.category, f.message, acIndex), f.severity);
36525
+ }
36526
+ }
36527
+ for (const [fp, sev] of seenThisIter) {
36528
+ const cur = counts.get(fp);
36529
+ counts.set(fp, { count: (cur?.count ?? 0) + 1, lastSeverity: sev });
36530
+ }
36531
+ }
36532
+ return counts;
36533
+ }
36534
+ function tagCoverageGap(findings) {
36535
+ return findings.map((f) => ({ ...f, meta: { ...f.meta ?? {}, coverageGap: true } }));
36536
+ }
36537
+ function classifyRecurrence(accepted, priorIterations, cfg, testFileMatch, threshold, source = "adversarial-review") {
36538
+ const blocking = [];
36539
+ const advisory = [];
36540
+ const demoted = [];
36541
+ if (!cfg.enabled) {
36542
+ for (const f of accepted)
36543
+ (isBlockingSeverity(f.severity, threshold) ? blocking : advisory).push(f);
36544
+ return { blocking, advisory, demoted };
36545
+ }
36546
+ const priorCounts = countPriorAppearances(priorIterations, source);
36547
+ for (const f of accepted) {
36548
+ if (f.category === "test-gap" && testFileMatch(f.file) && isBlockingSeverity(f.severity, threshold)) {
36549
+ blocking.push(f);
36550
+ continue;
36551
+ }
36552
+ if (!isBlockingSeverity(f.severity, threshold)) {
36553
+ advisory.push(f);
36554
+ continue;
36555
+ }
36556
+ const prior = lookupPriorAppearance(priorCounts, f);
36557
+ const n = (prior?.count ?? 0) + 1;
36558
+ const prevWasBlocking = prior !== undefined && isBlockingSeverity(prior.lastSeverity, threshold);
36559
+ if (n >= cfg.maxBlockingRounds + 1) {
36560
+ demoted.push(f);
36561
+ } else if (n === 1 || prevWasBlocking) {
36562
+ blocking.push(f);
36563
+ } else {
36564
+ advisory.push(f);
36565
+ }
36566
+ }
36567
+ return { blocking, advisory, demoted };
36568
+ }
36569
+ var MAX_ISSUE_PREFIX = 160, FP_ISSUE_PREFIX = 48;
36570
+ var init_recurrence_demotion = __esm(() => {
36571
+ init_adversarial_helpers();
36572
+ });
36573
+
36192
36574
  // src/review/requote-response.ts
36193
36575
  function parseRequoteResponse(output) {
36194
36576
  const parsed = tryParseLLMJson(output);
@@ -36236,6 +36618,17 @@ function isRecord(value) {
36236
36618
  }
36237
36619
  var init_requote_response = () => {};
36238
36620
 
36621
+ // src/operations/_review-fallback.ts
36622
+ function reviewExhaustedFallback(lastOutput, failOpen) {
36623
+ const unparsedPreview = previewOutput(lastOutput, UNPARSED_PREVIEW_BYTES);
36624
+ if (!/"passed"\s*:\s*false/.test(lastOutput))
36625
+ return { ...failOpen, unparsedPreview };
36626
+ return { ...failOpen, passed: false, failOpen: false, looksLikeFail: true, unparsedPreview };
36627
+ }
36628
+ var init__review_fallback = __esm(() => {
36629
+ init_retry();
36630
+ });
36631
+
36239
36632
  // src/operations/semantic-review.ts
36240
36633
  function withRepromptMarker(output, info) {
36241
36634
  const parsed = tryParseLLMJson(output);
@@ -36323,6 +36716,7 @@ async function performSemanticReground(turn, firstParsed, drops, ctx) {
36323
36716
  output: JSON.stringify({
36324
36717
  passed: false,
36325
36718
  findings: secondParsed.findings,
36719
+ ...secondParsed.acks && { acks: secondParsed.acks },
36326
36720
  _repromptInfo: { dropCount, outcome: "recovered-blocking", costUsd }
36327
36721
  }),
36328
36722
  estimatedCostUsd: costUsd
@@ -36335,6 +36729,7 @@ async function performSemanticReground(turn, firstParsed, drops, ctx) {
36335
36729
  output: JSON.stringify({
36336
36730
  passed: true,
36337
36731
  findings: [...firstAdvisory, ...secondAdvisory],
36732
+ ...secondParsed.acks && { acks: secondParsed.acks },
36338
36733
  _repromptInfo: { dropCount, outcome: "recovered-advisory-only", costUsd }
36339
36734
  }),
36340
36735
  estimatedCostUsd: costUsd
@@ -36433,7 +36828,7 @@ var FAIL_OPEN, SEMANTIC_REQUOTE_RECOVERED_EVENT = "review.semantic.finding.requo
36433
36828
  const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
36434
36829
  return {
36435
36830
  ...turn,
36436
- output: JSON.stringify({ passed, findings: requoted.findings }),
36831
+ output: JSON.stringify({ passed, findings: requoted.findings, ...parsed.acks && { acks: parsed.acks } }),
36437
36832
  estimatedCostUsd: (turn.estimatedCostUsd ?? 0) + requoted.extraCostUsd
36438
36833
  };
36439
36834
  }
@@ -36454,7 +36849,9 @@ var init_semantic_review = __esm(() => {
36454
36849
  init_logger2();
36455
36850
  init_prompts();
36456
36851
  init_finding_filters();
36852
+ init_recurrence_demotion();
36457
36853
  init_requote_response();
36854
+ init__review_fallback();
36458
36855
  FAIL_OPEN = {
36459
36856
  passed: true,
36460
36857
  findings: [],
@@ -36478,7 +36875,8 @@ var init_semantic_review = __esm(() => {
36478
36875
  invalid: () => ReviewPromptBuilder.jsonRetry(),
36479
36876
  truncated: () => ReviewPromptBuilder.jsonRetryCondensed({ blockingThreshold: input.blockingThreshold })
36480
36877
  },
36481
- exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], normalizedFindings: [], acDropped: [], looksLikeFail: true } : FAIL_OPEN,
36878
+ exhaustedFallback: (lastOutput) => reviewExhaustedFallback(lastOutput, FAIL_OPEN),
36879
+ outputPreviewBytes: UNPARSED_PREVIEW_BYTES,
36482
36880
  logContext: { blockingThreshold: input.blockingThreshold ?? "error" }
36483
36881
  }),
36484
36882
  hopBody: semanticReviewHopBody,
@@ -36507,9 +36905,11 @@ var init_semantic_review = __esm(() => {
36507
36905
  findings: parsed.findings,
36508
36906
  normalizedFindings: [],
36509
36907
  acDropped: [],
36510
- repromptEvent
36908
+ repromptEvent,
36909
+ ...parsed.acks && { acks: parsed.acks }
36511
36910
  };
36512
36911
  }
36912
+ const unparsedPreview = previewOutput(output, UNPARSED_PREVIEW_BYTES);
36513
36913
  if (/"passed"\s*:\s*false/.test(output)) {
36514
36914
  return {
36515
36915
  passed: false,
@@ -36517,10 +36917,11 @@ var init_semantic_review = __esm(() => {
36517
36917
  normalizedFindings: [],
36518
36918
  acDropped: [],
36519
36919
  looksLikeFail: true,
36920
+ unparsedPreview,
36520
36921
  repromptEvent
36521
36922
  };
36522
36923
  }
36523
- return FAIL_OPEN;
36924
+ return { ...FAIL_OPEN, unparsedPreview };
36524
36925
  },
36525
36926
  async verify(parsed, input, _verifyCtx) {
36526
36927
  if (parsed.failOpen || parsed.looksLikeFail)
@@ -36532,85 +36933,31 @@ var init_semantic_review = __esm(() => {
36532
36933
  const sanitized = sanitizeRefModeFindings(findings, input.mode, threshold);
36533
36934
  const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold, input.repoRoot);
36534
36935
  const { accepted, dropped } = filterByAcGroundingMinimal(substantiated, input.story.acceptanceCriteria);
36535
- const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
36936
+ const isTestFile3 = semanticTestFileMatch(input);
36937
+ const recurrenceCfg = input.semanticConfig.recurrenceDemotion ?? { enabled: false, maxBlockingRounds: 2 };
36938
+ const {
36939
+ blocking,
36940
+ advisory: subThreshold,
36941
+ demoted
36942
+ } = classifyRecurrence(accepted, input.priorSemanticIterations ?? [], recurrenceCfg, isTestFile3, threshold, "semantic-review");
36943
+ const advisoryFindings = [
36944
+ ...toReviewFindings(subThreshold.filter((f) => isBlockingSeverity(f.severity, threshold)), { isTestFile: isTestFile3 }),
36945
+ ...tagCoverageGap(toReviewFindings(demoted, { isTestFile: isTestFile3 }))
36946
+ ];
36536
36947
  const passed = blocking.length === 0 && (parsed.passed || accepted.length > 0);
36537
36948
  return {
36538
36949
  ...parsed,
36539
36950
  passed,
36540
36951
  findings: accepted,
36541
- normalizedFindings: toReviewFindings(blocking, { isTestFile: semanticTestFileMatch(input) }),
36952
+ normalizedFindings: toReviewFindings(blocking, { isTestFile: isTestFile3 }),
36953
+ advisoryFindings,
36542
36954
  acDropped: dropped
36543
36955
  };
36544
36956
  }
36545
36957
  };
36546
36958
  });
36547
36959
 
36548
- // src/review/recurrence-demotion.ts
36549
- function normalizeIssueText(s) {
36550
- return s.replace(/`/g, "").replace(/\s+/g, " ").trim().toLowerCase().slice(0, MAX_ISSUE_PREFIX);
36551
- }
36552
- function fingerprintFor(file3, category, text) {
36553
- const normFile = (file3 ?? "").replace(/\\/g, "/");
36554
- return `${normFile}|${category ?? ""}|${normalizeIssueText(text).slice(0, FP_ISSUE_PREFIX)}`;
36555
- }
36556
- function countPriorAppearances(priorIterations) {
36557
- const counts = new Map;
36558
- for (const it of priorIterations) {
36559
- const seenThisIter = new Map;
36560
- for (const f of it.findingsAfter ?? []) {
36561
- if (f.source !== "adversarial-review")
36562
- continue;
36563
- const fp = fingerprintFor(f.file, f.category, f.message);
36564
- seenThisIter.set(fp, f.severity);
36565
- }
36566
- for (const [fp, sev] of seenThisIter) {
36567
- const cur = counts.get(fp);
36568
- counts.set(fp, { count: (cur?.count ?? 0) + 1, lastSeverity: sev });
36569
- }
36570
- }
36571
- return counts;
36572
- }
36573
- function tagCoverageGap(findings) {
36574
- return findings.map((f) => ({ ...f, meta: { ...f.meta ?? {}, coverageGap: true } }));
36575
- }
36576
- function classifyRecurrence(accepted, priorIterations, cfg, testFileMatch, threshold) {
36577
- const blocking = [];
36578
- const advisory = [];
36579
- const demoted = [];
36580
- if (!cfg.enabled) {
36581
- for (const f of accepted)
36582
- (isBlockingSeverity(f.severity, threshold) ? blocking : advisory).push(f);
36583
- return { blocking, advisory, demoted };
36584
- }
36585
- const priorCounts = countPriorAppearances(priorIterations);
36586
- for (const f of accepted) {
36587
- if (f.category === "test-gap" && testFileMatch(f.file) && isBlockingSeverity(f.severity, threshold)) {
36588
- blocking.push(f);
36589
- continue;
36590
- }
36591
- if (!isBlockingSeverity(f.severity, threshold)) {
36592
- advisory.push(f);
36593
- continue;
36594
- }
36595
- const prior = priorCounts.get(fingerprintFor(f.file, f.category, f.issue));
36596
- const n = (prior?.count ?? 0) + 1;
36597
- const prevWasBlocking = prior !== undefined && isBlockingSeverity(prior.lastSeverity, threshold);
36598
- if (n >= cfg.maxBlockingRounds + 1) {
36599
- demoted.push(f);
36600
- } else if (n === 1 || prevWasBlocking) {
36601
- blocking.push(f);
36602
- } else {
36603
- advisory.push(f);
36604
- }
36605
- }
36606
- return { blocking, advisory, demoted };
36607
- }
36608
- var MAX_ISSUE_PREFIX = 160, FP_ISSUE_PREFIX = 48;
36609
- var init_recurrence_demotion = __esm(() => {
36610
- init_adversarial_helpers();
36611
- });
36612
-
36613
- // src/operations/adversarial-review.ts
36960
+ // src/operations/adversarial-reprompt-marker.ts
36614
36961
  function withRepromptMarker2(output, info) {
36615
36962
  const parsed = tryParseLLMJson(output);
36616
36963
  if (!parsed || typeof parsed !== "object")
@@ -36633,6 +36980,9 @@ function extractRepromptInfo2(raw) {
36633
36980
  outcome: i.outcome
36634
36981
  };
36635
36982
  }
36983
+ var init_adversarial_reprompt_marker = () => {};
36984
+
36985
+ // src/operations/adversarial-review.ts
36636
36986
  async function requoteBlockingAdversarialFindings(findings, ctx) {
36637
36987
  const threshold = ctx.input.blockingThreshold ?? "error";
36638
36988
  const maxRequotes = ctx.input.adversarialConfig.substantiation?.maxRequotes ?? DEFAULT_MAX_REQUOTES2;
@@ -36747,6 +37097,7 @@ async function performAdversarialReground(turn, firstParsed, drops, ctx) {
36747
37097
  output: JSON.stringify({
36748
37098
  passed: false,
36749
37099
  findings: secondParsed.findings,
37100
+ ...secondParsed.acks && { acks: secondParsed.acks },
36750
37101
  _repromptInfo: { dropCount, outcome: "recovered-blocking", costUsd }
36751
37102
  }),
36752
37103
  estimatedCostUsd: costUsd
@@ -36759,6 +37110,7 @@ async function performAdversarialReground(turn, firstParsed, drops, ctx) {
36759
37110
  output: JSON.stringify({
36760
37111
  passed: true,
36761
37112
  findings: [...firstAdvisory, ...secondAdvisory],
37113
+ ...secondParsed.acks && { acks: secondParsed.acks },
36762
37114
  _repromptInfo: { dropCount, outcome: "recovered-advisory-only", costUsd }
36763
37115
  }),
36764
37116
  estimatedCostUsd: costUsd
@@ -36794,7 +37146,8 @@ var FAIL_OPEN2, ADVERSARIAL_REQUOTE_RECOVERED_EVENT = "review.adversarial.findin
36794
37146
  invalid: () => ReviewPromptBuilder.jsonRetry(),
36795
37147
  truncated: () => ReviewPromptBuilder.jsonRetryCondensed({ blockingThreshold: input.blockingThreshold })
36796
37148
  },
36797
- exhaustedFallback: (lastOutput) => /"passed"\s*:\s*false/.test(lastOutput) ? { passed: false, findings: [], normalizedFindings: [], acDropped: [], looksLikeFail: true } : FAIL_OPEN2,
37149
+ exhaustedFallback: (lastOutput) => reviewExhaustedFallback(lastOutput, FAIL_OPEN2),
37150
+ outputPreviewBytes: UNPARSED_PREVIEW_BYTES,
36798
37151
  logContext: { blockingThreshold: input.blockingThreshold ?? "error" }
36799
37152
  }), adversarialReviewOp;
36800
37153
  var init_adversarial_review = __esm(() => {
@@ -36806,6 +37159,8 @@ var init_adversarial_review = __esm(() => {
36806
37159
  init_finding_filters();
36807
37160
  init_recurrence_demotion();
36808
37161
  init_requote_response();
37162
+ init__review_fallback();
37163
+ init_adversarial_reprompt_marker();
36809
37164
  FAIL_OPEN2 = {
36810
37165
  passed: true,
36811
37166
  findings: [],
@@ -36850,7 +37205,7 @@ var init_adversarial_review = __esm(() => {
36850
37205
  const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
36851
37206
  return {
36852
37207
  ...turn,
36853
- output: JSON.stringify({ passed, findings: requoted.findings }),
37208
+ output: JSON.stringify({ passed, findings: requoted.findings, ...parsed.acks && { acks: parsed.acks } }),
36854
37209
  estimatedCostUsd: (turn.estimatedCostUsd ?? 0) + requoted.extraCostUsd
36855
37210
  };
36856
37211
  }
@@ -36885,7 +37240,8 @@ var init_adversarial_review = __esm(() => {
36885
37240
  findings: parsed.findings,
36886
37241
  normalizedFindings: [],
36887
37242
  acDropped: [],
36888
- repromptEvent
37243
+ repromptEvent,
37244
+ ...parsed.acks && { acks: parsed.acks }
36889
37245
  };
36890
37246
  }
36891
37247
  if (/"passed"\s*:\s*false/.test(output) && !/"findings"\s*:\s*\[\s*\{/.test(output)) {
@@ -38726,7 +39082,6 @@ var init_ground = __esm(() => {
38726
39082
  init_errors();
38727
39083
  init_logger2();
38728
39084
  init_prompts();
38729
- init_truncation();
38730
39085
  groundOp = {
38731
39086
  kind: "run",
38732
39087
  name: "ground",
@@ -42163,11 +42518,11 @@ var init_findings = __esm(() => {
42163
42518
  init_cycle();
42164
42519
  });
42165
42520
 
42166
- // src/review/adversarial-iteration-store.ts
42167
- function getAdversarialIterations(store, storyId) {
42521
+ // src/review/review-iteration-store.ts
42522
+ function getReviewIterations(store, storyId) {
42168
42523
  return store.get(storyId) ?? [];
42169
42524
  }
42170
- function recordAdversarialIteration(store, storyId, roundFindings) {
42525
+ function recordReviewIteration(store, storyId, roundFindings) {
42171
42526
  const prior = store.get(storyId) ?? [];
42172
42527
  const findingsBefore = prior.length > 0 ? prior[prior.length - 1].findingsAfter : [];
42173
42528
  const findingsAfter = [...roundFindings];
@@ -42183,7 +42538,7 @@ function recordAdversarialIteration(store, storyId, roundFindings) {
42183
42538
  };
42184
42539
  store.set(storyId, [...prior, iteration]);
42185
42540
  }
42186
- var init_adversarial_iteration_store = __esm(() => {
42541
+ var init_review_iteration_store = __esm(() => {
42187
42542
  init_findings();
42188
42543
  });
42189
42544
 
@@ -42222,12 +42577,50 @@ function recordAdversarialAudit(opts) {
42222
42577
  blockingThreshold: opts.blockingThreshold,
42223
42578
  result: opts.result,
42224
42579
  advisoryFindings: opts.advisoryFindings,
42580
+ acks: opts.acks,
42225
42581
  diffAvailable: opts.diffAvailable,
42226
42582
  adversarialDropAnalysis: opts.adversarialDropAnalysis,
42227
42583
  adversarialAcceptAnalysis: opts.adversarialAcceptAnalysis
42228
42584
  });
42229
42585
  }
42230
42586
 
42587
+ // src/review/adversarial-counterfactual-telemetry.ts
42588
+ function buildCounterfactualTelemetry({
42589
+ acDropped,
42590
+ blockingFindings,
42591
+ acceptanceCriteria,
42592
+ diffFiles
42593
+ }) {
42594
+ const adversarialDropAnalysis = acDropped.map((d) => ({
42595
+ finding: {
42596
+ file: d.finding.file ?? "<unknown>",
42597
+ line: d.finding.line ?? 0,
42598
+ severity: d.finding.severity,
42599
+ category: d.finding.category ?? "<unknown>",
42600
+ issue: d.finding.issue
42601
+ },
42602
+ dropCode: d.code,
42603
+ acIndex: d.finding.acIndex,
42604
+ rawCategory: d.finding.category ?? "",
42605
+ counterfactual: analyzeStructuralCounterfactual({ acIndex: d.finding.acIndex, category: d.finding.category, file: d.finding.file }, acceptanceCriteria, diffFiles)
42606
+ }));
42607
+ const adversarialAcceptAnalysis = blockingFindings.map((f) => ({
42608
+ finding: {
42609
+ file: f.file,
42610
+ line: f.line,
42611
+ severity: f.severity,
42612
+ category: f.category
42613
+ },
42614
+ acIndex: f.acIndex,
42615
+ rawCategory: f.category,
42616
+ counterfactual: analyzeStructuralCounterfactual({ acIndex: f.acIndex, category: f.category, file: f.file }, acceptanceCriteria, diffFiles)
42617
+ }));
42618
+ return { adversarialDropAnalysis, adversarialAcceptAnalysis };
42619
+ }
42620
+ var init_adversarial_counterfactual_telemetry = __esm(() => {
42621
+ init_ac_structural_counterfactual();
42622
+ });
42623
+
42231
42624
  // src/review/diff-utils.ts
42232
42625
  var {spawn: spawn3 } = globalThis.Bun;
42233
42626
  async function resolveNaxIgnorePathspecExcludes(workdir, options) {
@@ -42626,7 +43019,7 @@ var package_default;
42626
43019
  var init_package = __esm(() => {
42627
43020
  package_default = {
42628
43021
  name: "@nathapp/nax",
42629
- version: "0.75.5",
43022
+ version: "0.76.0",
42630
43023
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42631
43024
  type: "module",
42632
43025
  bin: {
@@ -42730,8 +43123,8 @@ var init_version = __esm(() => {
42730
43123
  NAX_VERSION = package_default.version;
42731
43124
  NAX_COMMIT = (() => {
42732
43125
  try {
42733
- if (/^[0-9a-f]{6,10}$/.test("c8f74c5f"))
42734
- return "c8f74c5f";
43126
+ if (/^[0-9a-f]{6,10}$/.test("e7721ea5"))
43127
+ return "e7721ea5";
42735
43128
  } catch {}
42736
43129
  try {
42737
43130
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42778,6 +43171,9 @@ function toPersistedEntry(entry, epochMs) {
42778
43171
  blockingThreshold: entry.blockingThreshold ?? "error",
42779
43172
  result: entry.result,
42780
43173
  advisoryFindings: entry.advisoryFindings ?? null,
43174
+ acks: entry.acks ?? null,
43175
+ acDropped: entry.acDropped ?? null,
43176
+ ...entry.parsed ? {} : { unparsedPreview: entry.unparsedPreview ?? null },
42781
43177
  diffAvailable: entry.diffAvailable ?? null,
42782
43178
  adversarialDropAnalysis: entry.adversarialDropAnalysis ?? null,
42783
43179
  adversarialAcceptAnalysis: entry.adversarialAcceptAnalysis ?? null
@@ -43146,6 +43542,7 @@ async function runAdversarialReview(opts) {
43146
43542
  ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
43147
43543
  ];
43148
43544
  const acDropped = opResult.acDropped ?? [];
43545
+ const acks = opResult.acks;
43149
43546
  let diffFiles;
43150
43547
  let diffAvailable;
43151
43548
  if (diff && diff.length > 0) {
@@ -43163,30 +43560,12 @@ async function runAdversarialReview(opts) {
43163
43560
  diffAvailable = true;
43164
43561
  }
43165
43562
  }
43166
- const adversarialDropAnalysis = acDropped.map((d) => ({
43167
- finding: {
43168
- file: d.finding.file ?? "<unknown>",
43169
- line: d.finding.line ?? 0,
43170
- severity: d.finding.severity,
43171
- category: d.finding.category ?? "<unknown>",
43172
- issue: d.finding.issue
43173
- },
43174
- dropCode: d.code,
43175
- acIndex: d.finding.acIndex,
43176
- rawCategory: d.finding.category ?? "",
43177
- counterfactual: analyzeStructuralCounterfactual({ acIndex: d.finding.acIndex, category: d.finding.category, file: d.finding.file }, story.acceptanceCriteria, diffFiles)
43178
- }));
43179
- const adversarialAcceptAnalysis = blockingFindings.map((f) => ({
43180
- finding: {
43181
- file: f.file,
43182
- line: f.line,
43183
- severity: f.severity,
43184
- category: f.category
43185
- },
43186
- acIndex: f.acIndex,
43187
- rawCategory: f.category,
43188
- counterfactual: analyzeStructuralCounterfactual({ acIndex: f.acIndex, category: f.category, file: f.file }, story.acceptanceCriteria, diffFiles)
43189
- }));
43563
+ const { adversarialDropAnalysis, adversarialAcceptAnalysis } = buildCounterfactualTelemetry({
43564
+ acDropped,
43565
+ blockingFindings,
43566
+ acceptanceCriteria: story.acceptanceCriteria,
43567
+ diffFiles
43568
+ });
43190
43569
  if (advisoryFindings.length > 0) {
43191
43570
  logger?.debug("review", `Adversarial review: ${advisoryFindings.length} advisory findings (below threshold '${threshold}')`, {
43192
43571
  storyId: story.id,
@@ -43228,7 +43607,8 @@ async function runAdversarialReview(opts) {
43228
43607
  advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
43229
43608
  diffAvailable,
43230
43609
  adversarialDropAnalysis,
43231
- adversarialAcceptAnalysis
43610
+ adversarialAcceptAnalysis,
43611
+ acks
43232
43612
  });
43233
43613
  const output = blockingFindings.length > 0 ? `Adversarial review failed:
43234
43614
 
@@ -43262,6 +43642,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
43262
43642
  storyId: story.id,
43263
43643
  featureName,
43264
43644
  parsed: true,
43645
+ acks,
43265
43646
  failOpen: false,
43266
43647
  passed: true,
43267
43648
  passReason: "ac_quote_not_substring_demoted",
@@ -43299,6 +43680,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
43299
43680
  storyId: story.id,
43300
43681
  featureName,
43301
43682
  parsed: true,
43683
+ acks,
43302
43684
  failOpen: false,
43303
43685
  passed: false,
43304
43686
  blockingThreshold: threshold,
@@ -43329,6 +43711,7 @@ ${dropSummary}`,
43329
43711
  storyId: story.id,
43330
43712
  featureName,
43331
43713
  parsed: true,
43714
+ acks,
43332
43715
  failOpen: false,
43333
43716
  passed: true,
43334
43717
  blockingThreshold: threshold,
@@ -43359,7 +43742,7 @@ var init_adversarial = __esm(() => {
43359
43742
  init_logger2();
43360
43743
  init_adversarial_review();
43361
43744
  init_call();
43362
- init_ac_structural_counterfactual();
43745
+ init_adversarial_counterfactual_telemetry();
43363
43746
  init_adversarial_helpers();
43364
43747
  init_diff_utils();
43365
43748
  init_finding_projection();
@@ -43819,7 +44202,8 @@ function recordSemanticDebateAudit(opts) {
43819
44202
  passed: opts.passed,
43820
44203
  blockingThreshold: opts.blockingThreshold,
43821
44204
  result: opts.result,
43822
- advisoryFindings: opts.advisoryFindings
44205
+ advisoryFindings: opts.advisoryFindings,
44206
+ acks: opts.acks
43823
44207
  });
43824
44208
  }
43825
44209
  async function runSemanticDebate(opts) {
@@ -43873,12 +44257,16 @@ async function runSemanticDebate(opts) {
43873
44257
  const debateCost = debateResult.totalCostUsd ?? 0;
43874
44258
  const resolverPassed = debateResult.outcome === "passed";
43875
44259
  const allFindings = [];
44260
+ const acks = [];
43876
44261
  for (const p of debateResult.proposals) {
43877
44262
  const parsed = parseLLMResponse(p.output);
43878
44263
  if (parsed) {
43879
44264
  allFindings.push(...parsed.findings);
44265
+ if (parsed.acks)
44266
+ acks.push(...parsed.acks.slice(0, MAX_ACKS - acks.length));
43880
44267
  }
43881
44268
  }
44269
+ const debateAcks = acks.length > 0 ? acks : undefined;
43882
44270
  const seen = new Set;
43883
44271
  const deduped = [];
43884
44272
  for (const f of allFindings) {
@@ -43912,6 +44300,7 @@ async function runSemanticDebate(opts) {
43912
44300
  storyId: story.id,
43913
44301
  featureName,
43914
44302
  parsed: true,
44303
+ acks: debateAcks,
43915
44304
  passed: false,
43916
44305
  blockingThreshold: debateThreshold,
43917
44306
  result: {
@@ -43944,6 +44333,7 @@ ${formatFindings2(debateBlocking)}`,
43944
44333
  storyId: story.id,
43945
44334
  featureName,
43946
44335
  parsed: true,
44336
+ acks: debateAcks,
43947
44337
  passed: true,
43948
44338
  blockingThreshold: debateThreshold,
43949
44339
  result: {
@@ -43970,6 +44360,7 @@ ${formatFindings2(debateBlocking)}`,
43970
44360
  storyId: story.id,
43971
44361
  featureName,
43972
44362
  parsed: true,
44363
+ acks: debateAcks,
43973
44364
  passed: true,
43974
44365
  blockingThreshold: debateThreshold,
43975
44366
  result: {
@@ -44013,7 +44404,8 @@ function recordSemanticAudit(opts) {
44013
44404
  passed: opts.passed,
44014
44405
  blockingThreshold: opts.blockingThreshold,
44015
44406
  result: opts.result,
44016
- advisoryFindings: opts.advisoryFindings
44407
+ advisoryFindings: opts.advisoryFindings,
44408
+ acks: opts.acks
44017
44409
  });
44018
44410
  }
44019
44411
  async function runSemanticReview(opts) {
@@ -44291,6 +44683,7 @@ async function runSemanticReview(opts) {
44291
44683
  }
44292
44684
  const threshold = blockingThreshold ?? "error";
44293
44685
  const allFindings = opResult.findings;
44686
+ const acks = opResult.acks;
44294
44687
  const blockingFindings = allFindings.filter((f) => isBlockingSeverity(f.severity, threshold));
44295
44688
  const advisoryFindings = allFindings.filter((f) => !isBlockingSeverity(f.severity, threshold));
44296
44689
  if (advisoryFindings.length > 0) {
@@ -44328,6 +44721,7 @@ ${formatFindings2(blockingFindings)}`;
44328
44721
  failOpen: false,
44329
44722
  passed: false,
44330
44723
  blockingThreshold: threshold,
44724
+ acks,
44331
44725
  result: {
44332
44726
  passed: false,
44333
44727
  findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
@@ -44358,6 +44752,7 @@ ${formatFindings2(blockingFindings)}`;
44358
44752
  storyId: story.id,
44359
44753
  featureName,
44360
44754
  parsed: true,
44755
+ acks,
44361
44756
  failOpen: false,
44362
44757
  passed: false,
44363
44758
  blockingThreshold: threshold,
@@ -44383,6 +44778,7 @@ ${formatFindings2(blockingFindings)}`;
44383
44778
  storyId: story.id,
44384
44779
  featureName,
44385
44780
  parsed: true,
44781
+ acks,
44386
44782
  failOpen: false,
44387
44783
  passed: true,
44388
44784
  blockingThreshold: threshold,
@@ -44749,7 +45145,7 @@ var init_review = __esm(() => {
44749
45145
  init_category_fix_target();
44750
45146
  init_finding_filters();
44751
45147
  init_ac_quote_validator();
44752
- init_adversarial_iteration_store();
45148
+ init_review_iteration_store();
44753
45149
  init_ac_structural_counterfactual();
44754
45150
  init_adversarial();
44755
45151
  init_semantic_evidence();
@@ -44761,6 +45157,7 @@ var init_review = __esm(() => {
44761
45157
  init_runner2();
44762
45158
  init_requote_response();
44763
45159
  init_severity();
45160
+ init_semantic_categories();
44764
45161
  });
44765
45162
 
44766
45163
  // src/prompts/builders/rectifier-builder-helpers.ts
@@ -46778,6 +47175,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46778
47175
  workdir,
46779
47176
  pipelineStage: stage,
46780
47177
  modelDef,
47178
+ ...resolvedRunOptions.modelDef !== undefined ? {} : { modelTier: effectiveTier },
46781
47179
  timeoutSeconds: resolvedRunOptions.timeoutSeconds ?? config2.execution?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds,
46782
47180
  featureName,
46783
47181
  storyId: story.id,
@@ -46785,6 +47183,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46785
47183
  });
46786
47184
  }
46787
47185
  } else {
47186
+ const pinned = hopKind.kind === "primary" && resolvedRunOptions.modelDef !== undefined;
46788
47187
  const modelDef = hopKind.kind === "primary" ? resolvedRunOptions.modelDef ?? resolveModelForAgent(config2.models, agentName, effectiveTier, defaultAgent) : resolveModelForAgent(config2.models, agentName, effectiveTier, defaultAgent);
46789
47188
  handle = await sessionManager.openSession(sessionName, {
46790
47189
  agentName,
@@ -46792,6 +47191,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46792
47191
  workdir,
46793
47192
  pipelineStage: stage,
46794
47193
  modelDef,
47194
+ ...pinned ? {} : { modelTier: effectiveTier },
46795
47195
  timeoutSeconds: resolvedRunOptions.timeoutSeconds ?? config2.execution?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds,
46796
47196
  featureName,
46797
47197
  storyId: story.id,
@@ -46954,6 +47354,7 @@ async function callOp(ctx, op, input) {
46954
47354
  const sessionName = sessionRole2 && ctx.packageDir ? computeAcpHandle(ctx.packageDir, ctx.featureName, ctx.storyId, sessionRole2) : undefined;
46955
47355
  const completeOptions = {
46956
47356
  modelDef: resolved.modelDef,
47357
+ ...resolved.modelTier !== undefined ? { modelTier: resolved.modelTier } : {},
46957
47358
  jsonMode: completeOp.jsonMode ?? false,
46958
47359
  pipelineStage: op.stage,
46959
47360
  storyId: ctx.storyId,
@@ -48009,7 +48410,7 @@ var init_agent_stream_logging = __esm(() => {
48009
48410
  });
48010
48411
 
48011
48412
  // src/runtime/middleware/cost.ts
48012
- function attachCostSubscriber(bus, aggregator, runId) {
48413
+ function attachCostSubscriber(bus, aggregator, runId, projectKey) {
48013
48414
  const offDispatch = bus.onDispatch((event) => {
48014
48415
  const tu = event.tokenUsage;
48015
48416
  const wireExactCostUsd = event.exactCostUsd;
@@ -48022,9 +48423,15 @@ function attachCostSubscriber(bus, aggregator, runId) {
48022
48423
  const costEvent = {
48023
48424
  ts: event.timestamp,
48024
48425
  runId,
48426
+ ...projectKey !== undefined ? { projectKey } : {},
48427
+ schemaVersion: COST_ROW_SCHEMA_VERSION,
48025
48428
  agentName: event.agentName,
48026
- model: "unknown",
48429
+ model: event.model ?? "unknown",
48430
+ ...event.modelTier !== undefined ? { modelTier: event.modelTier } : {},
48431
+ ...event.profile !== undefined ? { profile: event.profile } : {},
48027
48432
  stage: event.stage,
48433
+ sessionRole: event.sessionRole,
48434
+ ...event.featureName !== undefined ? { featureName: event.featureName } : {},
48028
48435
  storyId: event.storyId,
48029
48436
  callId: event.callId,
48030
48437
  scopeId: event.scopeId,
@@ -48038,14 +48445,18 @@ function attachCostSubscriber(bus, aggregator, runId) {
48038
48445
  exactCostUsd,
48039
48446
  costUsd: exactCostUsd,
48040
48447
  confidence,
48448
+ pricingSource: hasWireExactCost ? "wire" : resolvePricingSource(event.model),
48041
48449
  durationMs: event.durationMs
48042
48450
  };
48043
48451
  aggregator.record(costEvent);
48044
48452
  });
48045
48453
  const offError = bus.onDispatchError((event) => {
48046
48454
  const errorEvent = {
48455
+ kind: "error",
48047
48456
  ts: event.timestamp,
48048
48457
  runId,
48458
+ ...projectKey !== undefined ? { projectKey } : {},
48459
+ schemaVersion: COST_ROW_SCHEMA_VERSION,
48049
48460
  agentName: event.agentName,
48050
48461
  stage: event.stage,
48051
48462
  storyId: event.storyId,
@@ -48074,6 +48485,10 @@ function attachCostSubscriber(bus, aggregator, runId) {
48074
48485
  offCompleted();
48075
48486
  };
48076
48487
  }
48488
+ var COST_ROW_SCHEMA_VERSION = 2;
48489
+ var init_cost2 = __esm(() => {
48490
+ init_agents();
48491
+ });
48077
48492
 
48078
48493
  // src/runtime/middleware/audit.ts
48079
48494
  function attachAuditSubscriber(bus, auditor, runId) {
@@ -48172,6 +48587,9 @@ function attachReviewAuditSubscriber(bus, auditor, runId) {
48172
48587
  blockingThreshold: event.blockingThreshold,
48173
48588
  result: event.result,
48174
48589
  advisoryFindings: event.advisoryFindings,
48590
+ acks: event.acks ? [...event.acks] : undefined,
48591
+ acDropped: event.acDropped ? [...event.acDropped] : undefined,
48592
+ unparsedPreview: event.unparsedPreview,
48175
48593
  diffAvailable: event.diffAvailable,
48176
48594
  adversarialDropAnalysis: event.adversarialDropAnalysis,
48177
48595
  adversarialAcceptAnalysis: event.adversarialAcceptAnalysis
@@ -48187,7 +48605,7 @@ function attachReviewAuditSubscriber(bus, auditor, runId) {
48187
48605
  function scheduleTickIfNeeded(tickRef, tick, intervalMs) {
48188
48606
  if (tickRef.handle !== null)
48189
48607
  return;
48190
- tickRef.handle = setTimeout(tick, intervalMs);
48608
+ tickRef.handle = _idleWatchdogDeps.setTimeout(tick, intervalMs);
48191
48609
  }
48192
48610
  function handleObserveTimeout(state, reason, idleDurationMs, nonToolCallIdleMs) {
48193
48611
  if (state.warnedForCurrentIdlePeriod)
@@ -48215,7 +48633,7 @@ async function handleCancelTimeout(state, reason, controllerRegistry, maxRetryAt
48215
48633
  return;
48216
48634
  }
48217
48635
  state.cancelAttempts++;
48218
- state.lastActivityAt = Date.now();
48636
+ state.lastActivityAt = _idleWatchdogDeps.now();
48219
48637
  getSafeLogger()?.warn("idle-watchdog", reason === "tool_call_only_idle_timeout_exceeded" ? "Canceling tool-call-only idle call" : "Canceling idle call", {
48220
48638
  storyId: state.storyId,
48221
48639
  key: reason,
@@ -48249,17 +48667,17 @@ function handleWarnThenCancelTimeout(state, reason, controllerRegistry, maxRetry
48249
48667
  });
48250
48668
  state.inGracePeriod = true;
48251
48669
  state.graceReason = reason;
48252
- state.graceTimer = setTimeout(async () => {
48670
+ state.graceTimer = _idleWatchdogDeps.setTimeout(async () => {
48253
48671
  if (!activeStates.has(state.callId))
48254
48672
  return;
48255
48673
  state.inGracePeriod = false;
48256
48674
  state.graceTimer = undefined;
48257
48675
  state.graceReason = undefined;
48258
- const currentReason = getTimeoutReason(state, Date.now(), idleTimeoutMs, toolCallOnlyTimeoutMs);
48676
+ const currentReason = getTimeoutReason(state, _idleWatchdogDeps.now(), idleTimeoutMs, toolCallOnlyTimeoutMs);
48259
48677
  if (currentReason !== reason)
48260
48678
  return;
48261
48679
  state.cancelAttempts++;
48262
- state.lastActivityAt = Date.now();
48680
+ state.lastActivityAt = _idleWatchdogDeps.now();
48263
48681
  const cancel = controllerRegistry.get(state.callId);
48264
48682
  if (cancel)
48265
48683
  await cancel().catch(() => {});
@@ -48267,7 +48685,7 @@ function handleWarnThenCancelTimeout(state, reason, controllerRegistry, maxRetry
48267
48685
  }
48268
48686
  function clearGrace(state) {
48269
48687
  if (state.inGracePeriod && state.graceTimer !== undefined) {
48270
- clearTimeout(state.graceTimer);
48688
+ _idleWatchdogDeps.clearTimeout(state.graceTimer);
48271
48689
  state.graceTimer = undefined;
48272
48690
  state.inGracePeriod = false;
48273
48691
  state.graceReason = undefined;
@@ -48303,7 +48721,7 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48303
48721
  const tickRef = { handle: null };
48304
48722
  function tick() {
48305
48723
  tickRef.handle = null;
48306
- const now = Date.now();
48724
+ const now = _idleWatchdogDeps.now();
48307
48725
  for (const [, state] of activeStates) {
48308
48726
  if (state.inGracePeriod)
48309
48727
  continue;
@@ -48326,7 +48744,7 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48326
48744
  const unsubscribe = agentStreamEvents.onAgentStream((event) => {
48327
48745
  switch (event.kind) {
48328
48746
  case "agent.call_started": {
48329
- const now = Date.now();
48747
+ const now = _idleWatchdogDeps.now();
48330
48748
  activeStates.set(event.callId, {
48331
48749
  callId: event.callId,
48332
48750
  agentName: event.agentName,
@@ -48398,11 +48816,11 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48398
48816
  const state = activeStates.get(event.callId);
48399
48817
  if (state) {
48400
48818
  if (state.graceTimer !== undefined)
48401
- clearTimeout(state.graceTimer);
48819
+ _idleWatchdogDeps.clearTimeout(state.graceTimer);
48402
48820
  activeStates.delete(event.callId);
48403
48821
  }
48404
48822
  if (activeStates.size === 0 && tickRef.handle !== null) {
48405
- clearTimeout(tickRef.handle);
48823
+ _idleWatchdogDeps.clearTimeout(tickRef.handle);
48406
48824
  tickRef.handle = null;
48407
48825
  }
48408
48826
  break;
@@ -48413,17 +48831,23 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48413
48831
  unsubscribe();
48414
48832
  for (const state of activeStates.values()) {
48415
48833
  if (state.graceTimer !== undefined)
48416
- clearTimeout(state.graceTimer);
48834
+ _idleWatchdogDeps.clearTimeout(state.graceTimer);
48417
48835
  }
48418
48836
  activeStates.clear();
48419
48837
  if (tickRef.handle !== null) {
48420
- clearTimeout(tickRef.handle);
48838
+ _idleWatchdogDeps.clearTimeout(tickRef.handle);
48421
48839
  tickRef.handle = null;
48422
48840
  }
48423
48841
  };
48424
48842
  }
48843
+ var _idleWatchdogDeps;
48425
48844
  var init_idle_watchdog = __esm(() => {
48426
48845
  init_logger2();
48846
+ _idleWatchdogDeps = {
48847
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
48848
+ clearTimeout: (id) => clearTimeout(id),
48849
+ now: () => Date.now()
48850
+ };
48427
48851
  });
48428
48852
 
48429
48853
  // src/runtime/middleware/index.ts
@@ -48431,6 +48855,7 @@ var init_middleware = __esm(() => {
48431
48855
  init_cancellation();
48432
48856
  init_logging();
48433
48857
  init_agent_stream_logging();
48858
+ init_cost2();
48434
48859
  init_idle_watchdog();
48435
48860
  });
48436
48861
 
@@ -48988,6 +49413,11 @@ var init_manager_sweep = __esm(() => {
48988
49413
  DEFAULT_ORPHAN_TTL_MS = 4 * 60 * 60 * 1000;
48989
49414
  });
48990
49415
 
49416
+ // src/session/model-selection.ts
49417
+ function selectModel(opts) {
49418
+ return { modelDef: opts.modelDef, ...opts.modelTier ? { modelTier: opts.modelTier } : {} };
49419
+ }
49420
+
48991
49421
  // src/session/naming.ts
48992
49422
  var exports_naming = {};
48993
49423
  __export(exports_naming, {
@@ -49293,7 +49723,7 @@ class SessionManager {
49293
49723
  agentName: opts.agentName,
49294
49724
  workdir: opts.workdir,
49295
49725
  resolvedPermissions,
49296
- modelDef: opts.modelDef,
49726
+ ...selectModel(opts),
49297
49727
  timeoutSeconds: opts.timeoutSeconds,
49298
49728
  onPidSpawned: this._pidRegistry ? (pid) => this._pidRegistry?.register(pid) : undefined,
49299
49729
  onPidExited: this._pidRegistry ? (pid) => this._pidRegistry?.unregister(pid) : undefined,
@@ -49568,8 +49998,8 @@ async function triageFlakyFindings(input) {
49568
49998
  result.push({ ...f });
49569
49999
  return { findings: result, quarantineReport: { keys, reasons } };
49570
50000
  }
49571
- const changedTestSet = new Set(diff.changedTestFiles.map(basename5));
49572
- const mappedTestSet = new Set(diff.mappedTestFiles.map(basename5));
50001
+ const changedTestSet = new Set(diff.changedTestFiles.map(basename6));
50002
+ const mappedTestSet = new Set(diff.mappedTestFiles.map(basename6));
49573
50003
  const candidates = findings.filter((f) => isProbeCandidate(f, changedTestSet, mappedTestSet));
49574
50004
  if (candidates.length > flakeDetection.maxProbesPerGate) {
49575
50005
  logger?.info("flake-triage", `Skipping flake triage \u2014 ${candidates.length} candidates exceed maxProbesPerGate=${flakeDetection.maxProbesPerGate}`);
@@ -49633,7 +50063,7 @@ async function triageFlakyFindings(input) {
49633
50063
  }
49634
50064
  return { findings: result, quarantineReport: { keys, reasons } };
49635
50065
  }
49636
- function basename5(path7) {
50066
+ function basename6(path7) {
49637
50067
  const i = path7.lastIndexOf("/");
49638
50068
  return i === -1 ? path7 : path7.slice(i + 1);
49639
50069
  }
@@ -49644,7 +50074,7 @@ function isProbeCandidate(finding, changedTestSet, mappedTestSet) {
49644
50074
  return false;
49645
50075
  if (!finding.rule)
49646
50076
  return false;
49647
- const base = basename5(finding.file);
50077
+ const base = basename6(finding.file);
49648
50078
  if (changedTestSet.has(base) || changedTestSet.has(finding.file))
49649
50079
  return false;
49650
50080
  if (mappedTestSet.has(base) || mappedTestSet.has(finding.file))
@@ -49778,6 +50208,7 @@ __export(exports_runtime, {
49778
50208
  attachAgentIdleWatchdog: () => attachAgentIdleWatchdog,
49779
50209
  _reviewAuditDeps: () => _reviewAuditDeps,
49780
50210
  _promptAuditorDeps: () => _promptAuditorDeps,
50211
+ _idleWatchdogDeps: () => _idleWatchdogDeps,
49781
50212
  _costAggDeps: () => _costAggDeps,
49782
50213
  ReviewAuditor: () => ReviewAuditor,
49783
50214
  PromptAuditor: () => PromptAuditor,
@@ -49787,7 +50218,7 @@ __export(exports_runtime, {
49787
50218
  CostAggregator: () => CostAggregator,
49788
50219
  AgentStreamEventBus: () => AgentStreamEventBus
49789
50220
  });
49790
- import { basename as basename6, join as join31 } from "path";
50221
+ import { basename as basename7, join as join31 } from "path";
49791
50222
  function createRuntime(config2, workdir, opts) {
49792
50223
  const runId = crypto.randomUUID();
49793
50224
  const controller = new AbortController;
@@ -49799,7 +50230,7 @@ function createRuntime(config2, workdir, opts) {
49799
50230
  const configLoader = createConfigLoader(config2);
49800
50231
  const dispatchEvents = new DispatchEventBus;
49801
50232
  const agentStreamEvents = opts?.agentStreamEvents ?? new AgentStreamEventBus;
49802
- const projectKey = config2.name?.trim() || basename6(workdir);
50233
+ const projectKey = config2.name?.trim() || basename7(workdir);
49803
50234
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
49804
50235
  const globalDir = globalOutputDir();
49805
50236
  const curatorRollupPathValue = curatorRollupPath(globalDir, config2.curator?.rollupPath);
@@ -49854,7 +50285,7 @@ function createRuntime(config2, workdir, opts) {
49854
50285
  agentManager.configureRuntime({ pidRegistry });
49855
50286
  }
49856
50287
  const offLogging = attachLoggingSubscriber(dispatchEvents, runId);
49857
- const offCost = attachCostSubscriber(dispatchEvents, costAggregator, runId);
50288
+ const offCost = attachCostSubscriber(dispatchEvents, costAggregator, runId, getProjectKey(config2, workdir));
49858
50289
  const offAudit = attachAuditSubscriber(dispatchEvents, promptAuditor, runId);
49859
50290
  const offReviewAudit = attachReviewAuditSubscriber(dispatchEvents, reviewAuditor, runId);
49860
50291
  const offAgentStreamLogging = attachAgentStreamLogging(agentStreamEvents, runId);
@@ -49863,6 +50294,7 @@ function createRuntime(config2, workdir, opts) {
49863
50294
  const logger = getLogger();
49864
50295
  const quarantineMemo = createQuarantineMemo();
49865
50296
  const adversarialIterations = new Map;
50297
+ const semanticIterations = new Map;
49866
50298
  const rectificationOscillations = new Map;
49867
50299
  let closed = false;
49868
50300
  return {
@@ -49886,6 +50318,7 @@ function createRuntime(config2, workdir, opts) {
49886
50318
  logger,
49887
50319
  quarantineMemo,
49888
50320
  adversarialIterations,
50321
+ semanticIterations,
49889
50322
  rectificationOscillations,
49890
50323
  get signal() {
49891
50324
  return controller.signal;
@@ -52781,7 +53214,8 @@ var init_telegram = __esm(() => {
52781
53214
  init_zod();
52782
53215
  init_logger2();
52783
53216
  _telegramPluginDeps = {
52784
- fetch: globalThis.fetch.bind(globalThis)
53217
+ fetch: globalThis.fetch.bind(globalThis),
53218
+ basePollBackoffMs: 1000
52785
53219
  };
52786
53220
  NUMERIC_CHAT_ID = /^-?\d+$/;
52787
53221
  TelegramConfigSchema = exports_external.object({
@@ -52795,7 +53229,7 @@ var init_telegram = __esm(() => {
52795
53229
  chatId = null;
52796
53230
  pendingMessages = new Map;
52797
53231
  lastUpdateId = 0;
52798
- backoffMs = 1000;
53232
+ backoffMs = _telegramPluginDeps.basePollBackoffMs;
52799
53233
  maxBackoffMs = 30000;
52800
53234
  static MAX_DRAIN_PAGES = 10;
52801
53235
  static INTERACTIVE_REQUEST_TYPES = new Set([
@@ -52905,7 +53339,7 @@ ${partLabel}${chunks[i]}`;
52905
53339
  this.clearInlineKeyboard(update.callback_query.message.message_id);
52906
53340
  }
52907
53341
  this.pendingMessages.delete(requestId);
52908
- this.backoffMs = 1000;
53342
+ this.backoffMs = _telegramPluginDeps.basePollBackoffMs;
52909
53343
  return response;
52910
53344
  }
52911
53345
  if (update.callback_query) {
@@ -52976,7 +53410,7 @@ ${partLabel}${chunks[i]}`;
52976
53410
  rejected: raw.length - updates.length
52977
53411
  });
52978
53412
  }
52979
- this.backoffMs = 1000;
53413
+ this.backoffMs = _telegramPluginDeps.basePollBackoffMs;
52980
53414
  return { ok: true, updates, rawCount: raw.length };
52981
53415
  } catch (err) {
52982
53416
  this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
@@ -55788,10 +56222,10 @@ __export(exports_status_cost, {
55788
56222
  displayCostMetrics: () => displayCostMetrics,
55789
56223
  _costReportEmitDeps: () => _costReportEmitDeps
55790
56224
  });
55791
- import { basename as basename7 } from "path";
56225
+ import { basename as basename8 } from "path";
55792
56226
  async function resolveProject(workdir) {
55793
56227
  const config2 = await loadConfig(workdir).catch(() => null);
55794
- const project = config2?.name?.trim() || basename7(workdir);
56228
+ const project = config2?.name?.trim() || basename8(workdir);
55795
56229
  const outputDir = projectOutputDir(project, config2?.outputDir);
55796
56230
  return { project, outputDir };
55797
56231
  }
@@ -56056,7 +56490,7 @@ __export(exports_status_features, {
56056
56490
  _statusFeaturesDeps: () => _statusFeaturesDeps
56057
56491
  });
56058
56492
  import { existsSync as existsSync17, readdirSync as readdirSync3 } from "fs";
56059
- import { basename as basename8, join as join45, resolve as resolve14 } from "path";
56493
+ import { basename as basename9, join as join45, resolve as resolve14 } from "path";
56060
56494
  function isPidAlive(pid) {
56061
56495
  try {
56062
56496
  process.kill(pid, 0);
@@ -56079,7 +56513,7 @@ async function loadStatusFile(featureDir) {
56079
56513
  }
56080
56514
  async function loadProjectStatusFile(projectDir) {
56081
56515
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
56082
- const projectKey = config2?.name?.trim() || basename8(projectDir);
56516
+ const projectKey = config2?.name?.trim() || basename9(projectDir);
56083
56517
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
56084
56518
  const statusPath = join45(outputDir, "status.json");
56085
56519
  if (!existsSync17(statusPath)) {
@@ -56150,7 +56584,7 @@ async function getFeatureSummary(featureName, featureDir) {
56150
56584
  }
56151
56585
  async function displayAllFeatures(projectDir) {
56152
56586
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
56153
- const projectKey = config2?.name?.trim() || basename8(projectDir);
56587
+ const projectKey = config2?.name?.trim() || basename9(projectDir);
56154
56588
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
56155
56589
  const featuresDir = join45(outputDir, "features");
56156
56590
  if (!existsSync17(featuresDir)) {
@@ -56373,7 +56807,7 @@ async function displayFeatureStatus(options = {}) {
56373
56807
  if (options.dir) {
56374
56808
  const projectDir = resolve14(options.dir);
56375
56809
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
56376
- const projectKey = config2?.name?.trim() || basename8(projectDir);
56810
+ const projectKey = config2?.name?.trim() || basename9(projectDir);
56377
56811
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
56378
56812
  featureDir = join45(outputDir, "features", options.feature);
56379
56813
  } else {
@@ -57004,7 +57438,7 @@ var init_acceptance3 = __esm(() => {
57004
57438
  const allOutputParts = [];
57005
57439
  let anyError = false;
57006
57440
  let errorExitCode = 0;
57007
- let hardeningRetries = 0;
57441
+ let hardeningPromoted = 0;
57008
57442
  for (const { testPath, packageDir, testFramework, commandOverride } of testGroups) {
57009
57443
  const testFile = Bun.file(testPath);
57010
57444
  const exists = await testFile.exists();
@@ -57105,7 +57539,7 @@ ${stderr}`;
57105
57539
  runtime: ctx.runtime,
57106
57540
  abortSignal: ctx.abortSignal
57107
57541
  });
57108
- hardeningRetries = result.promoted.length;
57542
+ hardeningPromoted = result.promoted.length;
57109
57543
  } catch (err) {
57110
57544
  logger.debug("acceptance", "Hardening pass failed (non-blocking)", {
57111
57545
  storyId: ctx.story.id,
@@ -57118,7 +57552,8 @@ ${stderr}`;
57118
57552
  packageDir: ctx.workdir,
57119
57553
  passed: true,
57120
57554
  failedACs: [],
57121
- retries: hardeningRetries,
57555
+ retries: ctx.acceptanceRetries ?? 0,
57556
+ hardeningPromoted,
57122
57557
  durationMs
57123
57558
  });
57124
57559
  return { action: "continue" };
@@ -57134,7 +57569,8 @@ ${stderr}`;
57134
57569
  packageDir: ctx.workdir,
57135
57570
  passed: false,
57136
57571
  failedACs: allFailedACs,
57137
- retries: hardeningRetries,
57572
+ retries: ctx.acceptanceRetries ?? 0,
57573
+ hardeningPromoted,
57138
57574
  durationMs
57139
57575
  });
57140
57576
  if (anyError) {
@@ -59246,11 +59682,12 @@ function toReviewDecisionPayload(opName, output) {
59246
59682
  const reviewer = opName === "semantic-review" ? "semantic" : opName === "adversarial-review" ? "adversarial" : null;
59247
59683
  if (!reviewer)
59248
59684
  return null;
59685
+ const unparsedPreview = typeof record2.unparsedPreview === "string" ? record2.unparsedPreview : undefined;
59249
59686
  if (record2.failOpen === true) {
59250
- return { reviewer, parsed: false, passed: true, failOpen: true, result: null };
59687
+ return { reviewer, parsed: false, passed: true, failOpen: true, result: null, unparsedPreview };
59251
59688
  }
59252
59689
  if (record2.looksLikeFail === true) {
59253
- return { reviewer, parsed: false, passed: false, looksLikeFail: true, result: null };
59690
+ return { reviewer, parsed: false, passed: false, looksLikeFail: true, result: null, unparsedPreview };
59254
59691
  }
59255
59692
  if (typeof record2.passed !== "boolean" || !Array.isArray(record2.findings)) {
59256
59693
  return null;
@@ -59272,7 +59709,8 @@ function toReviewDecisionPayload(opName, output) {
59272
59709
  parsed: true,
59273
59710
  passed: record2.passed,
59274
59711
  result: { passed: record2.passed, findings: record2.findings },
59275
- acDropped
59712
+ acDropped,
59713
+ ...Array.isArray(record2.advisoryFindings) ? { advisoryFindings: record2.advisoryFindings } : {}
59276
59714
  };
59277
59715
  }
59278
59716
  function emitReviewDecision(ctx, opName, output) {
@@ -59293,7 +59731,10 @@ function emitReviewDecision(ctx, opName, output) {
59293
59731
  looksLikeFail: payload.parsed ? undefined : payload.looksLikeFail,
59294
59732
  failOpen: payload.parsed ? false : payload.failOpen,
59295
59733
  passed: payload.passed,
59296
- result: payload.result
59734
+ result: payload.result,
59735
+ advisoryFindings: payload.parsed ? payload.advisoryFindings : undefined,
59736
+ acDropped: payload.parsed ? payload.acDropped : undefined,
59737
+ unparsedPreview: payload.parsed ? undefined : payload.unparsedPreview
59297
59738
  });
59298
59739
  }
59299
59740
  function logUnifiedReviewPhaseStart(storyId, opName) {
@@ -59430,13 +59871,19 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59430
59871
  dispatchInput = await refreshReviewInputForDispatch(opName, dispatchInput);
59431
59872
  let advIterationBefore = 0;
59432
59873
  if (opName === "adversarial-review" && ctx.storyId) {
59433
- const priorIterations = getAdversarialIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59874
+ const priorIterations = getReviewIterations(ctx.runtime.adversarialIterations, ctx.storyId);
59434
59875
  advIterationBefore = priorIterations.length;
59435
59876
  dispatchInput = {
59436
59877
  ...dispatchInput,
59437
59878
  priorAdversarialIterations: priorIterations
59438
59879
  };
59439
59880
  }
59881
+ if (opName === "semantic-review" && ctx.storyId) {
59882
+ dispatchInput = {
59883
+ ...dispatchInput,
59884
+ priorSemanticIterations: getReviewIterations(ctx.runtime.semanticIterations, ctx.storyId)
59885
+ };
59886
+ }
59440
59887
  if (isTddPhase) {
59441
59888
  logger?.info("tdd", `-> Session: ${opName}`, { storyId: ctx.storyId, role: opName, ...progressData });
59442
59889
  } else if (isThreeSession && opName === "full-suite-gate") {
@@ -59458,11 +59905,18 @@ async function runPhase(ctx, slot, phaseCosts, phaseOutputs, isThreeSession = fa
59458
59905
  emitReviewDecision(ctx, opName, output);
59459
59906
  if (opName === "adversarial-review" && ctx.storyId) {
59460
59907
  const advOut = output;
59461
- recordAdversarialIteration(ctx.runtime.adversarialIterations, ctx.storyId, [
59908
+ recordReviewIteration(ctx.runtime.adversarialIterations, ctx.storyId, [
59462
59909
  ...advOut.normalizedFindings ?? [],
59463
59910
  ...advOut.advisoryFindings ?? []
59464
59911
  ]);
59465
59912
  }
59913
+ if (opName === "semantic-review" && ctx.storyId) {
59914
+ const semOut = output;
59915
+ recordReviewIteration(ctx.runtime.semanticIterations, ctx.storyId, [
59916
+ ...semOut.normalizedFindings ?? [],
59917
+ ...semOut.advisoryFindings ?? []
59918
+ ]);
59919
+ }
59466
59920
  logUnifiedReviewPhaseResult(ctx.storyId, opName, output);
59467
59921
  logDeterministicPhaseOutcome(ctx.storyId, opName, output, Date.now() - phaseStartedAt, isTddPhase, slot.op.stage, progressData);
59468
59922
  outcome = derivePhaseOutcome(output);
@@ -60224,6 +60678,7 @@ var init_story_orchestrator = __esm(() => {
60224
60678
  init_rectification();
60225
60679
  init_nbf_flake_triage();
60226
60680
  init_run_phase();
60681
+ init_review_decision();
60227
60682
  init_types9();
60228
60683
  });
60229
60684
 
@@ -60573,7 +61028,6 @@ async function assemblePlanInputsFromCtx(ctx) {
60573
61028
  diff: prepared.diff,
60574
61029
  excludePatterns: prepared.excludePatterns,
60575
61030
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-semantic"),
60576
- priorSemanticIterations: ctx.priorSemanticIterations,
60577
61031
  resolvedTestPatterns,
60578
61032
  blockingThreshold: ctx.config.review.blockingThreshold,
60579
61033
  _refresh: {
@@ -60611,7 +61065,6 @@ async function assemblePlanInputsFromCtx(ctx) {
60611
61065
  testGlobs: prepared.testGlobs,
60612
61066
  refExcludePatterns: prepared.refExcludePatterns,
60613
61067
  featureCtxBlock: buildFeatureCtxBlock(ctx, "reviewer-adversarial"),
60614
- priorAdversarialIterations: ctx.priorAdversarialIterations,
60615
61068
  resolvedTestPatterns,
60616
61069
  blockingThreshold: ctx.config.review.blockingThreshold,
60617
61070
  _refresh: {
@@ -62113,7 +62566,7 @@ async function fanOutReporters(reporters, hook, invoke) {
62113
62566
  }
62114
62567
  }
62115
62568
  }
62116
- function wireReporters(bus, pluginRegistry, runId, startTime) {
62569
+ function wireReporters(bus, pluginRegistry, runId, startTime, projectKey) {
62117
62570
  const logger = getSafeLogger();
62118
62571
  const safe = (name, fn) => {
62119
62572
  return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
@@ -62163,7 +62616,8 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
62163
62616
  runId,
62164
62617
  feature: ev.feature,
62165
62618
  totalStories: ev.totalStories,
62166
- startTime: new Date(startTime).toISOString()
62619
+ startTime: new Date(startTime).toISOString(),
62620
+ project: projectKey
62167
62621
  });
62168
62622
  } catch (err) {
62169
62623
  logger?.warn("plugins", `Reporter '${r.name}' onRunStart failed`, { error: err });
@@ -62690,7 +63144,8 @@ var init_gitignore = __esm(() => {
62690
63144
  "**/_nax_acceptance_test.py",
62691
63145
  "**/_nax_suggested_test.py",
62692
63146
  "**/.nax/features/*/",
62693
- ".nax/prompt-audit/"
63147
+ ".nax/prompt-audit/",
63148
+ ".nax/finish-audit/"
62694
63149
  ];
62695
63150
  });
62696
63151
 
@@ -62703,7 +63158,7 @@ __export(exports_init_context, {
62703
63158
  generatePackageContextTemplate: () => generatePackageContextTemplate,
62704
63159
  generateContextTemplate: () => generateContextTemplate
62705
63160
  });
62706
- import { basename as basename10, join as join54 } from "path";
63161
+ import { basename as basename11, join as join54 } from "path";
62707
63162
  async function bunFileExists(path16) {
62708
63163
  return Bun.file(path16).exists();
62709
63164
  }
@@ -62798,7 +63253,7 @@ async function scanProject(projectRoot) {
62798
63253
  const readmeSnippet = await readReadmeSnippet(projectRoot);
62799
63254
  const entryPoints = await detectEntryPoints(projectRoot);
62800
63255
  const configFiles = await detectConfigFiles(projectRoot);
62801
- const projectName = packageManifest?.name || basename10(projectRoot);
63256
+ const projectName = packageManifest?.name || basename11(projectRoot);
62802
63257
  return {
62803
63258
  projectName,
62804
63259
  fileTree,
@@ -64221,7 +64676,8 @@ async function collectFromMetrics(context) {
64221
64676
  const success2 = boolValue(story.success, false);
64222
64677
  const storyId = stringValue(story.storyId ?? story.id, "unknown");
64223
64678
  const obs = {
64224
- schemaVersion: 1,
64679
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64680
+ projectKey: context.projectKey,
64225
64681
  runId: context.runId,
64226
64682
  featureId: stringValue(currentRun.feature, context.feature),
64227
64683
  storyId,
@@ -64241,6 +64697,24 @@ async function collectFromMetrics(context) {
64241
64697
  } catch {}
64242
64698
  return observations;
64243
64699
  }
64700
+ function withinRun(timestamp, runStartedAt) {
64701
+ if (runStartedAt === undefined)
64702
+ return true;
64703
+ const ts = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
64704
+ if (Number.isNaN(ts))
64705
+ return true;
64706
+ return ts >= runStartedAt;
64707
+ }
64708
+ async function writtenThisRun(filePath, runStartedAt) {
64709
+ if (runStartedAt === undefined)
64710
+ return true;
64711
+ try {
64712
+ const stat = await Bun.file(filePath).stat();
64713
+ return stat.mtimeMs >= runStartedAt;
64714
+ } catch {
64715
+ return true;
64716
+ }
64717
+ }
64244
64718
  function findingRuleId(finding) {
64245
64719
  return stringValue(finding.ruleId ?? finding.rule ?? finding.checkId ?? finding.category, "unknown");
64246
64720
  }
@@ -64266,17 +64740,22 @@ async function collectFromReviewAudit(context) {
64266
64740
  const audit = asRecord3(await readJsonFile(fullPath));
64267
64741
  if (!audit)
64268
64742
  continue;
64743
+ const featureId = stringValue(audit.featureName ?? audit.featureId, context.feature);
64744
+ if (context.runStartedAt !== undefined && featureId !== context.feature)
64745
+ continue;
64746
+ if (!withinRun(audit.timestamp, context.runStartedAt))
64747
+ continue;
64269
64748
  const result = asRecord3(audit.result);
64270
64749
  const findings = asArray(result?.findings);
64271
64750
  const storyId = stringValue(audit.storyId, "unknown");
64272
- const featureId = stringValue(audit.featureName ?? audit.featureId, context.feature);
64273
64751
  for (const rawFinding of findings) {
64274
64752
  const finding = asRecord3(rawFinding);
64275
64753
  if (!finding)
64276
64754
  continue;
64277
64755
  const ruleId = findingRuleId(finding);
64278
64756
  const obs = {
64279
- schemaVersion: 1,
64757
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64758
+ projectKey: context.projectKey,
64280
64759
  runId: context.runId,
64281
64760
  featureId,
64282
64761
  storyId,
@@ -64287,6 +64766,7 @@ async function collectFromReviewAudit(context) {
64287
64766
  ruleId,
64288
64767
  checkId: ruleId,
64289
64768
  severity: stringValue(finding.severity, "info"),
64769
+ category: optionalString(finding.category),
64290
64770
  file: stringValue(finding.file),
64291
64771
  line: numberValue(finding.line, 0),
64292
64772
  message: findingMessage(finding)
@@ -64302,6 +64782,7 @@ async function collectFromReviewAudit(context) {
64302
64782
  async function collectFromContextManifests(context) {
64303
64783
  const observations = [];
64304
64784
  const featuresDir = path18.join(context.workdir, ".nax", "features");
64785
+ let skippedManifests = 0;
64305
64786
  try {
64306
64787
  const glob = new Bun.Glob("*/stories/*/context-manifest-*.json");
64307
64788
  for await (const file3 of glob.scan({ cwd: featuresDir, absolute: false })) {
@@ -64310,15 +64791,21 @@ async function collectFromContextManifests(context) {
64310
64791
  const parts = file3.split("/");
64311
64792
  const featureId = parts[0] ?? context.feature;
64312
64793
  const storyId = parts[2] ?? "unknown";
64794
+ if (!await writtenThisRun(fullPath, context.runStartedAt)) {
64795
+ skippedManifests += 1;
64796
+ continue;
64797
+ }
64313
64798
  const manifest = asRecord3(await readJsonFile(fullPath));
64314
64799
  if (!manifest)
64315
64800
  continue;
64316
64801
  const ts = now();
64317
64802
  const chunkSummaries = asRecord3(manifest.chunkSummaries) ?? {};
64803
+ const chunkTokens = asRecord3(manifest.chunkTokens) ?? {};
64318
64804
  for (const chunkId of asArray(manifest.includedChunks)) {
64319
64805
  const id = String(chunkId);
64320
64806
  const obs = {
64321
- schemaVersion: 1,
64807
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64808
+ projectKey: context.projectKey,
64322
64809
  runId: context.runId,
64323
64810
  featureId,
64324
64811
  storyId,
@@ -64328,7 +64815,7 @@ async function collectFromContextManifests(context) {
64328
64815
  payload: {
64329
64816
  chunkId: id,
64330
64817
  label: stringValue(chunkSummaries[id], id),
64331
- tokens: 0
64818
+ tokens: numberValue(chunkTokens[id], 0)
64332
64819
  }
64333
64820
  };
64334
64821
  observations.push(obs);
@@ -64339,7 +64826,8 @@ async function collectFromContextManifests(context) {
64339
64826
  continue;
64340
64827
  const id = stringValue(excluded.id, "unknown");
64341
64828
  const obs = {
64342
- schemaVersion: 1,
64829
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64830
+ projectKey: context.projectKey,
64343
64831
  runId: context.runId,
64344
64832
  featureId,
64345
64833
  storyId,
@@ -64359,7 +64847,8 @@ async function collectFromContextManifests(context) {
64359
64847
  if (!provider || stringValue(provider.status) !== "empty")
64360
64848
  continue;
64361
64849
  const obs = {
64362
- schemaVersion: 1,
64850
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64851
+ projectKey: context.projectKey,
64363
64852
  runId: context.runId,
64364
64853
  featureId,
64365
64854
  storyId,
@@ -64390,7 +64879,8 @@ function entryFeatureId(context, entry, data) {
64390
64879
  function collectPullCall(context, entry, data) {
64391
64880
  const toolName = stringValue(data.tool ?? data.toolName, "unknown");
64392
64881
  return {
64393
- schemaVersion: 1,
64882
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64883
+ projectKey: context.projectKey,
64394
64884
  runId: context.runId,
64395
64885
  featureId: entryFeatureId(context, entry, data),
64396
64886
  storyId: entryStoryId(entry, data),
@@ -64409,7 +64899,8 @@ function collectPullCall(context, entry, data) {
64409
64899
  }
64410
64900
  function collectAcceptanceVerdict(context, entry, data) {
64411
64901
  return {
64412
- schemaVersion: 1,
64902
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64903
+ projectKey: context.projectKey,
64413
64904
  runId: context.runId,
64414
64905
  featureId: entryFeatureId(context, entry, data),
64415
64906
  storyId: entryStoryId(entry, data),
@@ -64427,7 +64918,8 @@ function collectAcceptanceVerdict(context, entry, data) {
64427
64918
  }
64428
64919
  function collectRectify(context, entry, data) {
64429
64920
  return {
64430
- schemaVersion: 1,
64921
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64922
+ projectKey: context.projectKey,
64431
64923
  runId: context.runId,
64432
64924
  featureId: entryFeatureId(context, entry, data),
64433
64925
  storyId: entryStoryId(entry, data),
@@ -64442,7 +64934,8 @@ function collectRectify(context, entry, data) {
64442
64934
  }
64443
64935
  function collectEscalation(context, entry, data) {
64444
64936
  return {
64445
- schemaVersion: 1,
64937
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64938
+ projectKey: context.projectKey,
64446
64939
  runId: context.runId,
64447
64940
  featureId: entryFeatureId(context, entry, data),
64448
64941
  storyId: entryStoryId(entry, data),
@@ -64458,7 +64951,8 @@ function collectEscalation(context, entry, data) {
64458
64951
  function collectFixCycleIteration(context, entry, data) {
64459
64952
  const outcome = optionalString(data.outcome);
64460
64953
  return {
64461
- schemaVersion: 1,
64954
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64955
+ projectKey: context.projectKey,
64462
64956
  runId: context.runId,
64463
64957
  featureId: entryFeatureId(context, entry, data),
64464
64958
  storyId: entryStoryId(entry, data),
@@ -64478,7 +64972,8 @@ function collectFixCycleIteration(context, entry, data) {
64478
64972
  }
64479
64973
  function collectFixCycleExit(context, entry, data) {
64480
64974
  return {
64481
- schemaVersion: 1,
64975
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64976
+ projectKey: context.projectKey,
64482
64977
  runId: context.runId,
64483
64978
  featureId: entryFeatureId(context, entry, data),
64484
64979
  storyId: entryStoryId(entry, data),
@@ -64493,7 +64988,8 @@ function collectFixCycleExit(context, entry, data) {
64493
64988
  }
64494
64989
  function collectFixCycleRetry(context, entry, data) {
64495
64990
  return {
64496
- schemaVersion: 1,
64991
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64992
+ projectKey: context.projectKey,
64497
64993
  runId: context.runId,
64498
64994
  featureId: entryFeatureId(context, entry, data),
64499
64995
  storyId: entryStoryId(entry, data),
@@ -64547,6 +65043,7 @@ async function collectObservations(context) {
64547
65043
  ]);
64548
65044
  return [...metricsObs, ...auditObs, ...manifestObs, ...jsonlObs];
64549
65045
  }
65046
+ var OBSERVATION_SCHEMA_VERSION = 3;
64550
65047
  var init_collect = () => {};
64551
65048
 
64552
65049
  // src/plugins/builtin/curator/heuristics.ts
@@ -64563,6 +65060,12 @@ function mergeThresholds(thresholds) {
64563
65060
  function uniqueStoryIds(storyIds) {
64564
65061
  return [...new Set(storyIds)];
64565
65062
  }
65063
+ function crossFeatureKey(category, message) {
65064
+ return `${category ?? ""}|${normalizeIssueText(message).slice(0, CROSS_FEATURE_MESSAGE_PREFIX)}`;
65065
+ }
65066
+ function truncate3(s, max) {
65067
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
65068
+ }
64566
65069
  function firstLine2(message) {
64567
65070
  return message.split(`
64568
65071
  `)[0] ?? message;
@@ -64571,37 +65074,45 @@ function h1RepeatedReviewFinding(observations, threshold) {
64571
65074
  const findings = observations.filter((o) => o.kind === "review-finding");
64572
65075
  const groups = new Map;
64573
65076
  for (const obs of findings) {
64574
- const ruleId = obs.payload.ruleId;
64575
- const message = obs.payload.message;
64576
- const existing = groups.get(ruleId);
64577
- if (existing) {
64578
- existing.storyIds.push(obs.storyId);
64579
- const sampleKey = firstLine2(message);
64580
- if (existing.samples.length < 2 && sampleKey && !existing.samples.includes(sampleKey)) {
64581
- existing.samples.push(sampleKey);
64582
- }
64583
- } else {
64584
- const sampleKey = firstLine2(message);
64585
- groups.set(ruleId, { storyIds: [obs.storyId], samples: sampleKey ? [sampleKey] : [] });
65077
+ const { file: file3, category, message } = obs.payload;
65078
+ if (!file3 && !category)
65079
+ continue;
65080
+ const key = crossFeatureKey(category, message);
65081
+ let group = groups.get(key);
65082
+ if (!group) {
65083
+ group = { featureIds: new Set, sites: new Set, files: new Set, samples: [] };
65084
+ groups.set(key, group);
65085
+ group.category = category;
64586
65086
  }
65087
+ group.featureIds.add(obs.featureId);
65088
+ group.sites.add(`${obs.featureId}/${obs.storyId}`);
65089
+ if (file3)
65090
+ group.files.add(file3);
65091
+ const sample = firstLine2(message);
65092
+ if (sample && group.samples.length < 2 && !group.samples.includes(sample))
65093
+ group.samples.push(sample);
64587
65094
  }
64588
65095
  const proposals = [];
64589
- for (const [ruleId, { storyIds, samples }] of groups.entries()) {
64590
- if (storyIds.length < threshold)
65096
+ for (const group of groups.values()) {
65097
+ const featureCount = group.featureIds.size;
65098
+ if (featureCount < threshold)
64591
65099
  continue;
64592
- const count = storyIds.length;
64593
- const severity2 = count >= 4 ? "HIGH" : "MED";
64594
- const unique = uniqueStoryIds(storyIds);
64595
- const sampleSection = samples.length > 0 ? `
64596
- Examples: ${samples.join(" | ")}` : "";
65100
+ const features = [...group.featureIds];
65101
+ const sites = [...group.sites];
65102
+ const files = [...group.files];
65103
+ const gist = group.samples[0] ? truncate3(group.samples[0], DESCRIPTION_GIST_CHARS) : "(no description)";
65104
+ const categoryLabel = group.category ? `${group.category}: ` : "";
65105
+ const fileSection = files.length > 0 ? ` Files: ${files.slice(0, MAX_EVIDENCE_FILES).join(", ")}.` : "";
65106
+ const sampleSection = group.samples.length > 0 ? `
65107
+ Examples: ${group.samples.join(" | ")}` : "";
64597
65108
  proposals.push({
64598
65109
  id: "H1",
64599
- severity: severity2,
65110
+ severity: featureCount >= threshold * HIGH_SEVERITY_MULTIPLE ? "HIGH" : "MED",
64600
65111
  target: { canonicalFile: ".nax/rules/curator-suggestions.md", action: "add" },
64601
- description: `Repeated review finding: ${ruleId} appeared ${count}x across stories`,
64602
- evidence: `Rule ${ruleId} fired ${count}\xD7 in stories: ${unique.join(", ")}${sampleSection}`,
65112
+ description: `Recurring across ${featureCount} features \u2014 ${categoryLabel}${gist}`,
65113
+ evidence: `Seen in ${featureCount} features: ${features.join(", ")} (sites: ${sites.join(", ")}).${fileSection}${sampleSection}`,
64603
65114
  sourceKinds: ["review-finding"],
64604
- storyIds: unique
65115
+ storyIds: sites
64605
65116
  });
64606
65117
  }
64607
65118
  return proposals;
@@ -64772,8 +65283,9 @@ function runHeuristics(observations, thresholds) {
64772
65283
  ...h6FixCycleUnchanged(observations, t.unchangedOutcome)
64773
65284
  ];
64774
65285
  }
64775
- var DEFAULT_THRESHOLDS2;
65286
+ var DEFAULT_THRESHOLDS2, HIGH_SEVERITY_MULTIPLE = 2, DESCRIPTION_GIST_CHARS = 90, MAX_EVIDENCE_FILES = 4, CROSS_FEATURE_MESSAGE_PREFIX = 48;
64776
65287
  var init_heuristics = __esm(() => {
65288
+ init_review();
64777
65289
  DEFAULT_THRESHOLDS2 = {
64778
65290
  repeatedFinding: 2,
64779
65291
  emptyKeyword: 2,
@@ -64846,7 +65358,7 @@ function renderProposals(proposals, runId, observationCount) {
64846
65358
  const storyList = p.storyIds.join(", ");
64847
65359
  lines.push(`- [ ] [${p.severity}] ${p.id}: ${p.description} \u2014 stories: ${storyList}`);
64848
65360
  if (p.evidence) {
64849
- lines.push(` _Evidence: ${p.evidence}_`);
65361
+ lines.push(` _Evidence: ${p.evidence.replace(/\s*\n\s*/g, " \xB7 ")}_`);
64850
65362
  }
64851
65363
  }
64852
65364
  lines.push("");
@@ -64856,6 +65368,26 @@ function renderProposals(proposals, runId, observationCount) {
64856
65368
  `);
64857
65369
  }
64858
65370
 
65371
+ // src/plugins/builtin/curator/jsonl-stream.ts
65372
+ async function* streamJsonlLines(file3) {
65373
+ const decoder = new TextDecoder;
65374
+ let carry = "";
65375
+ for await (const chunk of file3.stream()) {
65376
+ carry += decoder.decode(chunk, { stream: true });
65377
+ let nl = carry.indexOf(`
65378
+ `);
65379
+ while (nl !== -1) {
65380
+ yield carry.slice(0, nl);
65381
+ carry = carry.slice(nl + 1);
65382
+ nl = carry.indexOf(`
65383
+ `);
65384
+ }
65385
+ }
65386
+ carry += decoder.decode();
65387
+ if (carry.length > 0)
65388
+ yield carry;
65389
+ }
65390
+
64859
65391
  // src/plugins/builtin/curator/rollup.ts
64860
65392
  import { appendFile as appendFile3, mkdir as mkdir9, writeFile } from "fs/promises";
64861
65393
  import * as path19 from "path";
@@ -64876,7 +65408,72 @@ async function appendToRollup(observations, rollupPath) {
64876
65408
  await appendFile3(rollupPath, newLines);
64877
65409
  } catch {}
64878
65410
  }
64879
- var init_rollup = () => {};
65411
+ function emptyWindow() {
65412
+ return { observations: [], runIds: [], truncated: false, unattributedRows: 0 };
65413
+ }
65414
+ async function parseTail(file3, startedMidFile, projectKey) {
65415
+ const observations = [];
65416
+ let unattributed = 0;
65417
+ let first = true;
65418
+ for await (const line of streamJsonlLines(file3)) {
65419
+ if (first) {
65420
+ first = false;
65421
+ if (startedMidFile)
65422
+ continue;
65423
+ }
65424
+ if (!line.trim())
65425
+ continue;
65426
+ try {
65427
+ const obs = JSON.parse(line);
65428
+ if (obs.projectKey === projectKey)
65429
+ observations.push(obs);
65430
+ else if (!obs.projectKey)
65431
+ unattributed += 1;
65432
+ } catch {}
65433
+ }
65434
+ return { observations, unattributed };
65435
+ }
65436
+ function newestRunIds(observations, windowRuns) {
65437
+ const keep = new Set;
65438
+ for (let i = observations.length - 1;i >= 0 && keep.size < windowRuns; i -= 1) {
65439
+ const runId = observations[i]?.runId;
65440
+ if (runId)
65441
+ keep.add(runId);
65442
+ }
65443
+ return keep;
65444
+ }
65445
+ async function readHeuristicWindow(rollupPath, windowRuns, options) {
65446
+ const maxTail = Math.max(1, options.maxTailBytes ?? MAX_WINDOW_TAIL_BYTES);
65447
+ try {
65448
+ const file3 = Bun.file(rollupPath);
65449
+ if (!await file3.exists())
65450
+ return emptyWindow();
65451
+ const size = file3.size;
65452
+ let tail = Math.max(1, Math.min(options.tailBytes ?? INITIAL_WINDOW_TAIL_BYTES, maxTail));
65453
+ while (true) {
65454
+ const start = Math.max(0, size - tail);
65455
+ const { observations, unattributed } = await parseTail(start > 0 ? file3.slice(start) : file3, start > 0, options.projectKey);
65456
+ const keep = newestRunIds(observations, windowRuns);
65457
+ const exhausted = start === 0;
65458
+ if (keep.size >= windowRuns || exhausted || tail >= maxTail) {
65459
+ return {
65460
+ observations: observations.filter((o) => keep.has(o.runId)),
65461
+ runIds: [...keep],
65462
+ truncated: keep.size < windowRuns && !exhausted,
65463
+ unattributedRows: unattributed
65464
+ };
65465
+ }
65466
+ tail = Math.max(1, Math.min(tail * 2, maxTail));
65467
+ }
65468
+ } catch {
65469
+ return emptyWindow();
65470
+ }
65471
+ }
65472
+ var INITIAL_WINDOW_TAIL_BYTES, MAX_WINDOW_TAIL_BYTES;
65473
+ var init_rollup = __esm(() => {
65474
+ INITIAL_WINDOW_TAIL_BYTES = 8 * 1024 * 1024;
65475
+ MAX_WINDOW_TAIL_BYTES = 64 * 1024 * 1024;
65476
+ });
64880
65477
 
64881
65478
  // src/plugins/builtin/curator/index.ts
64882
65479
  import { mkdir as mkdir10 } from "fs/promises";
@@ -64921,11 +65518,12 @@ function getCuratorThresholds(context) {
64921
65518
  unchangedOutcome: raw.unchangedOutcome ?? DEFAULT_THRESHOLDS3.unchangedOutcome
64922
65519
  };
64923
65520
  }
64924
- var PLUGIN_NAME3 = "nax-curator", PLUGIN_VERSION3 = "0.1.0", DEFAULT_THRESHOLDS3, curatorAction, curatorPlugin;
65521
+ var PLUGIN_NAME3 = "nax-curator", PLUGIN_VERSION3 = "0.1.0", DEFAULT_THRESHOLDS3, curatorAction, HEURISTIC_WINDOW_RUNS = 20, curatorPlugin;
64925
65522
  var init_curator = __esm(() => {
64926
65523
  init_collect();
64927
65524
  init_heuristics();
64928
65525
  init_rollup();
65526
+ init_rollup();
64929
65527
  DEFAULT_THRESHOLDS3 = {
64930
65528
  repeatedFinding: 2,
64931
65529
  emptyKeyword: 2,
@@ -64958,12 +65556,22 @@ var init_curator = __esm(() => {
64958
65556
  await Bun.write(observationsPath, observations.map((o) => JSON.stringify(o)).join(`
64959
65557
  `) + (observations.length > 0 ? `
64960
65558
  ` : ""));
65559
+ await appendToRollup(observations, rollupPath);
64961
65560
  const thresholds = getCuratorThresholds(context);
64962
- const proposals = runHeuristics(observations, thresholds);
65561
+ const window2 = await readHeuristicWindow(rollupPath, HEURISTIC_WINDOW_RUNS, {
65562
+ projectKey: curatorContext.projectKey
65563
+ });
65564
+ if (window2.truncated) {
65565
+ context.logger.warn("Curator window truncated at the byte ceiling", {
65566
+ runsFound: window2.runIds.length,
65567
+ runsRequested: HEURISTIC_WINDOW_RUNS,
65568
+ unattributedRows: window2.unattributedRows
65569
+ });
65570
+ }
65571
+ const proposals = runHeuristics(window2.observations.length > 0 ? window2.observations : observations, thresholds);
64963
65572
  const markdown = renderProposals(proposals, context.runId, observations.length);
64964
65573
  const proposalsMdPath = path20.join(runDir, "curator-proposals.md");
64965
65574
  await Bun.write(proposalsMdPath, markdown);
64966
- await appendToRollup(observations, rollupPath);
64967
65575
  }
64968
65576
  return {
64969
65577
  success: true,
@@ -64996,15 +65604,21 @@ function selectFinish(config2) {
64996
65604
  return;
64997
65605
  return finishConfigSelector.select(config2)?.finish;
64998
65606
  }
65607
+ function resolveFlowAgent(config2, explicit) {
65608
+ if (typeof explicit === "string" && explicit.length > 0)
65609
+ return explicit;
65610
+ return resolveDefaultAgent(config2 ?? {});
65611
+ }
64999
65612
  function getFinishAutoFlowConfig(ctx) {
65000
65613
  const autoFlow = selectFinish(ctx.config)?.autoFlow;
65001
65614
  if (!autoFlow)
65002
- return DEFAULT_FINISH_AUTO_FLOW_CONFIG;
65615
+ return { ...DEFAULT_FINISH_AUTO_FLOW_CONFIG, defaultAgent: resolveFlowAgent(ctx.config, null) };
65003
65616
  const defaults = DEFAULT_FINISH_AUTO_FLOW_CONFIG;
65004
65617
  return {
65005
65618
  enabled: autoFlow.enabled === true,
65006
65619
  flowPath: autoFlow.flowPath ?? defaults.flowPath,
65007
- defaultAgent: autoFlow.defaultAgent ?? null,
65620
+ defaultAgent: resolveFlowAgent(ctx.config, autoFlow.defaultAgent),
65621
+ model: autoFlow.model ?? null,
65008
65622
  reviewers: {
65009
65623
  spec: autoFlow.reviewers?.spec ?? null,
65010
65624
  quality: autoFlow.reviewers?.quality ?? null
@@ -65028,11 +65642,12 @@ function telegramCreds(config2) {
65028
65642
  }
65029
65643
  var DEFAULT_FINISH_AUTO_FLOW_CONFIG;
65030
65644
  var init_config2 = __esm(() => {
65645
+ init_agents();
65031
65646
  init_config();
65032
65647
  DEFAULT_FINISH_AUTO_FLOW_CONFIG = {
65033
65648
  enabled: false,
65034
65649
  flowPath: "flows/nax-finish/nax-finish.flow.ts",
65035
- defaultAgent: null,
65650
+ model: null,
65036
65651
  reviewers: { spec: null, quality: null },
65037
65652
  escalate: { telegram: true },
65038
65653
  notify: { mode: "escalation" },
@@ -65127,14 +65742,21 @@ async function defaultRun2(cmd, opts) {
65127
65742
  clearTimeout(timer);
65128
65743
  }
65129
65744
  }
65130
- async function defaultReadResult(workdir) {
65131
- const f = Bun.file(path21.join(workdir, ".nax", "nax-finish-result.json"));
65745
+ function finishAuditDir(ctx) {
65746
+ const root = ctx.outputDir ?? path21.join(ctx.workdir, ".nax");
65747
+ return path21.join(root, "finish-audit", ctx.feature);
65748
+ }
65749
+ function finishResultPath(ctx, runId) {
65750
+ return path21.join(finishAuditDir(ctx), `${runId}.result.json`);
65751
+ }
65752
+ async function defaultReadResult(resultPath) {
65753
+ const f = Bun.file(resultPath);
65132
65754
  if (!await f.exists())
65133
65755
  return null;
65134
65756
  return JSON.parse(await f.text());
65135
65757
  }
65136
- async function defaultClearResult(workdir) {
65137
- const file3 = Bun.file(path21.join(workdir, ".nax", "nax-finish-result.json"));
65758
+ async function defaultClearResult(resultPath) {
65759
+ const file3 = Bun.file(resultPath);
65138
65760
  if (await file3.exists())
65139
65761
  await file3.delete();
65140
65762
  }
@@ -65159,18 +65781,19 @@ async function resolveFlowPath(workdir, flowPath, deps = _naxFinishDeps) {
65159
65781
  }
65160
65782
  return null;
65161
65783
  }
65162
- function buildFlowArgv(flowPath, inputJson, defaultAgent, stepMs) {
65163
- const stepTimeout = stepMs && stepMs > 0 ? ["--timeout", String(Math.ceil(stepMs / 1000))] : [];
65784
+ function buildFlowArgv(flowPath, inputJson, opts = {}) {
65785
+ const stepTimeout = opts.stepMs && opts.stepMs > 0 ? ["--timeout", String(Math.ceil(opts.stepMs / 1000))] : [];
65164
65786
  return [
65165
65787
  "acpx",
65166
65788
  "--approve-all",
65167
65789
  ...stepTimeout,
65790
+ ...opts.model ? ["--model", opts.model] : [],
65168
65791
  "flow",
65169
65792
  "run",
65170
65793
  flowPath,
65171
65794
  "--input-json",
65172
65795
  inputJson,
65173
- ...defaultAgent ? ["--default-agent", defaultAgent] : []
65796
+ ...opts.defaultAgent ? ["--default-agent", opts.defaultAgent] : []
65174
65797
  ];
65175
65798
  }
65176
65799
  function buildFlowEnv(cfg) {
@@ -65208,22 +65831,29 @@ async function executeFinishFlow(options) {
65208
65831
  escalateTelegram
65209
65832
  };
65210
65833
  }
65211
- await _naxFinishDeps.clearResult(ctx.workdir);
65834
+ const resultPath = finishResultPath(ctx, ctx.runId);
65835
+ await _naxFinishDeps.clearResult(resultPath);
65212
65836
  const input = {
65213
65837
  feature: ctx.feature,
65214
65838
  workdir: ctx.workdir,
65215
65839
  branch: ctx.branch,
65216
65840
  prdPath: ctx.prdPath,
65841
+ auditDir: finishAuditDir(ctx),
65842
+ runId: ctx.runId,
65217
65843
  escalateTelegram,
65218
65844
  timeouts: { acceptanceMs: cfg.timeouts.acceptanceMs, gateMs: cfg.timeouts.gateMs }
65219
65845
  };
65220
- const cmd = buildFlowArgv(flowPath, JSON.stringify(input), cfg.defaultAgent, cfg.timeouts.stepMs);
65846
+ const cmd = buildFlowArgv(flowPath, JSON.stringify(input), {
65847
+ defaultAgent: cfg.defaultAgent,
65848
+ stepMs: cfg.timeouts.stepMs,
65849
+ model: cfg.model
65850
+ });
65221
65851
  const res = await _naxFinishDeps.run(cmd, {
65222
65852
  cwd: ctx.workdir,
65223
65853
  env: buildFlowEnv(cfg),
65224
65854
  timeoutMs: cfg.timeouts.flowMs
65225
65855
  });
65226
- const result = await _naxFinishDeps.readResult(ctx.workdir);
65856
+ const result = await _naxFinishDeps.readResult(resultPath);
65227
65857
  if (!result)
65228
65858
  return missingResultOutcome(ctx, res, escalateTelegram);
65229
65859
  return {
@@ -65601,7 +66231,7 @@ function startHeartbeat(opts) {
65601
66231
  let stopped = false;
65602
66232
  let timer;
65603
66233
  const armTimer = () => {
65604
- timer = setTimeout(() => {
66234
+ timer = _heartbeatDeps.setTimeout(() => {
65605
66235
  try {
65606
66236
  Promise.resolve(onTick(getSnapshot())).catch((err) => getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
65607
66237
  error: err instanceof Error ? err.message : String(err)
@@ -65620,7 +66250,7 @@ function startHeartbeat(opts) {
65620
66250
  stop() {
65621
66251
  stopped = true;
65622
66252
  if (timer !== undefined)
65623
- clearTimeout(timer);
66253
+ _heartbeatDeps.clearTimeout(timer);
65624
66254
  }
65625
66255
  };
65626
66256
  }
@@ -65664,10 +66294,14 @@ function buildHeartbeatMetricsPayload(p) {
65664
66294
  ]
65665
66295
  };
65666
66296
  }
65667
- var STAGE2 = "otel-reporter-heartbeat";
66297
+ var STAGE2 = "otel-reporter-heartbeat", _heartbeatDeps;
65668
66298
  var init_heartbeat = __esm(() => {
65669
66299
  init_logger2();
65670
66300
  init_otlp();
66301
+ _heartbeatDeps = {
66302
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
66303
+ clearTimeout: (id) => clearTimeout(id)
66304
+ };
65671
66305
  });
65672
66306
 
65673
66307
  // src/plugins/builtin/otel-reporter/ids.ts
@@ -65697,7 +66331,7 @@ function toLogRecord(entry) {
65697
66331
  const nonScalars = {};
65698
66332
  for (const [key, value] of Object.entries(data)) {
65699
66333
  if (typeof value === "string") {
65700
- attributes.push(attr(`nax.data.${key}`, truncate3(value)));
66334
+ attributes.push(attr(`nax.data.${key}`, truncate4(value)));
65701
66335
  } else if (typeof value === "number") {
65702
66336
  if (Number.isFinite(value)) {
65703
66337
  attributes.push(attr(`nax.data.${key}`, value));
@@ -65711,7 +66345,7 @@ function toLogRecord(entry) {
65711
66345
  }
65712
66346
  }
65713
66347
  if (Object.keys(nonScalars).length > 0) {
65714
- attributes.push(attr("nax.data_json", truncate3(JSON.stringify(nonScalars))));
66348
+ attributes.push(attr("nax.data_json", truncate4(JSON.stringify(nonScalars))));
65715
66349
  }
65716
66350
  return {
65717
66351
  body: { stringValue: entry.message },
@@ -65734,7 +66368,7 @@ function buildLogsPayload(entries, resource) {
65734
66368
  ]
65735
66369
  };
65736
66370
  }
65737
- function truncate3(value) {
66371
+ function truncate4(value) {
65738
66372
  if (value.length <= DATA_JSON_MAX)
65739
66373
  return value;
65740
66374
  const marker = TRUNCATION_MARKER;
@@ -66702,11 +67336,11 @@ function getSafeLogger6() {
66702
67336
  return getSafeLogger();
66703
67337
  }
66704
67338
  function extractPluginName(pluginPath) {
66705
- const basename12 = path22.basename(pluginPath);
66706
- if (basename12 === "index.ts" || basename12 === "index.js" || basename12 === "index.mjs") {
67339
+ const basename13 = path22.basename(pluginPath);
67340
+ if (basename13 === "index.ts" || basename13 === "index.js" || basename13 === "index.mjs") {
66707
67341
  return path22.basename(path22.dirname(pluginPath));
66708
67342
  }
66709
- return basename12.replace(/\.(ts|js|mjs)$/, "");
67343
+ return basename13.replace(/\.(ts|js|mjs)$/, "");
66710
67344
  }
66711
67345
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins, isTestFileFn, reporters) {
66712
67346
  const loadedPlugins = [];
@@ -67379,9 +68013,9 @@ var init_hooks = __esm(() => {
67379
68013
  // src/execution/crash-heartbeat.ts
67380
68014
  import { appendFileSync as appendFileSync2 } from "fs";
67381
68015
  async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath) {
67382
- const logger = _heartbeatDeps.getSafeLogger();
68016
+ const logger = _heartbeatDeps2.getSafeLogger();
67383
68017
  while (gen === _heartbeatGen && _heartbeatActive) {
67384
- await _heartbeatDeps.sleep(60000);
68018
+ await _heartbeatDeps2.sleep(60000);
67385
68019
  if (gen !== _heartbeatGen || !_heartbeatActive)
67386
68020
  break;
67387
68021
  try {
@@ -67410,11 +68044,11 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
67410
68044
  }
67411
68045
  }
67412
68046
  function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
67413
- const logger = _heartbeatDeps.getSafeLogger();
68047
+ const logger = _heartbeatDeps2.getSafeLogger();
67414
68048
  _heartbeatActive = true;
67415
68049
  const gen = ++_heartbeatGen;
67416
68050
  heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
67417
- _heartbeatDeps.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
68051
+ _heartbeatDeps2.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
67418
68052
  error: err instanceof Error ? err.message : String(err)
67419
68053
  });
67420
68054
  });
@@ -67427,10 +68061,10 @@ function stopHeartbeat() {
67427
68061
  getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
67428
68062
  }
67429
68063
  }
67430
- var _heartbeatDeps, _heartbeatGen = 0, _heartbeatActive = false;
68064
+ var _heartbeatDeps2, _heartbeatGen = 0, _heartbeatActive = false;
67431
68065
  var init_crash_heartbeat = __esm(() => {
67432
68066
  init_logger2();
67433
- _heartbeatDeps = {
68067
+ _heartbeatDeps2 = {
67434
68068
  sleep: async (ms) => Bun.sleep(ms),
67435
68069
  getSafeLogger
67436
68070
  };
@@ -68205,6 +68839,7 @@ function buildAcceptanceContext(ctx, prd) {
68205
68839
  agentManager: ctx.agentManager,
68206
68840
  sessionManager: ctx.sessionManager,
68207
68841
  acceptanceTestPaths: ctx.acceptanceTestPaths,
68842
+ acceptanceRetries: ctx.acceptanceRetries ?? 0,
68208
68843
  runtime: ctx.runtime,
68209
68844
  abortSignal: ctx.abortSignal
68210
68845
  };
@@ -68301,8 +68936,9 @@ async function runAcceptanceLoop(ctx) {
68301
68936
  logger?.info("acceptance", "All stories complete, running acceptance validation");
68302
68937
  const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
68303
68938
  while (acceptanceRetries < maxRetries) {
68939
+ const attemptCtx = { ...ctx, acceptanceRetries };
68304
68940
  const firstStory = prd.userStories[0];
68305
- const acceptanceContext = buildAcceptanceContext(ctx, prd);
68941
+ const acceptanceContext = buildAcceptanceContext(attemptCtx, prd);
68306
68942
  const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
68307
68943
  if (acceptanceResult.action === "continue") {
68308
68944
  logger?.info("acceptance", "Acceptance validation passed!");
@@ -68393,17 +69029,14 @@ async function runAcceptanceLoop(ctx) {
68393
69029
  confidence: diagnosis.confidence,
68394
69030
  attempt: acceptanceRetries
68395
69031
  });
68396
- const cycleResult = await runAcceptanceFixCycle(ctx, prd, pkgFailures, diagnosis, effectivePath, testCommand, {
68397
- packageDir: pkg.packageDir,
68398
- testPath: effectivePath
68399
- });
69032
+ const cycleResult = await runAcceptanceFixCycle(attemptCtx, prd, pkgFailures, diagnosis, effectivePath, testCommand, { packageDir: pkg.packageDir, testPath: effectivePath });
68400
69033
  totalCost2 += cycleResult.costUsd ?? 0;
68401
69034
  totalInternalIterations += cycleResult.iterations.length;
68402
69035
  const pkgResolved = cycleResult.exitReason === "resolved" || cycleResult.finalFindings.length === 0;
68403
69036
  if (!pkgResolved)
68404
69037
  remainingFindings.push(...cycleResult.finalFindings);
68405
69038
  }
68406
- const finalCheck = await runAcceptanceTestsOnce(ctx, prd);
69039
+ const finalCheck = await runAcceptanceTestsOnce(attemptCtx, prd);
68407
69040
  const success2 = finalCheck.passed && remainingFindings.length === 0;
68408
69041
  const failureMessages = !success2 ? finalCheck.failedACs.length > 0 ? finalCheck.failedACs : remainingFindings.length > 0 ? remainingFindings.map((f) => f.message) : ["acceptance validation failed (unknown cause)"] : undefined;
68409
69042
  return buildResult(success2, prd, totalCost2, iterations, storiesCompleted, prdDirty, failureMessages, acceptanceRetries + totalInternalIterations);
@@ -69711,10 +70344,10 @@ var init_ensure_package_dirs = __esm(() => {
69711
70344
 
69712
70345
  // src/pipeline/subscribers/events-writer.ts
69713
70346
  import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
69714
- import { basename as basename15, join as join86 } from "path";
70347
+ import { basename as basename16, join as join86 } from "path";
69715
70348
  function wireEventsWriter(bus, feature, runId, workdir) {
69716
70349
  const logger = getSafeLogger();
69717
- const project = basename15(workdir);
70350
+ const project = basename16(workdir);
69718
70351
  const eventsDir = join86(getEventsRootDir(), project);
69719
70352
  const eventsFile = join86(eventsDir, "events.jsonl");
69720
70353
  let dirReady = false;
@@ -69897,10 +70530,10 @@ var init_interaction2 = __esm(() => {
69897
70530
 
69898
70531
  // src/pipeline/subscribers/registry.ts
69899
70532
  import { mkdir as mkdir14, writeFile as writeFile2 } from "fs/promises";
69900
- import { basename as basename16, join as join87 } from "path";
70533
+ import { basename as basename17, join as join87 } from "path";
69901
70534
  function wireRegistry(bus, feature, runId, workdir, outputDir) {
69902
70535
  const logger = getSafeLogger();
69903
- const project = basename16(workdir);
70536
+ const project = basename17(workdir);
69904
70537
  const runDir = join87(getRunsDir(), `${project}-${feature}-${runId}`);
69905
70538
  const metaFile = join87(runDir, "meta.json");
69906
70539
  const unsub = bus.on("run:started", (_ev) => {
@@ -71317,8 +71950,6 @@ function releaseHeavyPipelineContext(ctx) {
71317
71950
  ctx.constitution = undefined;
71318
71951
  ctx.acceptanceFailures = undefined;
71319
71952
  ctx.autofixPriorIterations = undefined;
71320
- ctx.priorSemanticIterations = undefined;
71321
- ctx.priorAdversarialIterations = undefined;
71322
71953
  ctx.reviewFindings = undefined;
71323
71954
  ctx.selfVerification = undefined;
71324
71955
  ctx.tddIsolations = undefined;
@@ -71977,7 +72608,7 @@ async function executeUnified(ctx, initialPrd) {
71977
72608
  _prevRunUnsubscribers = [];
71978
72609
  const thisRunUnsubscribers = [
71979
72610
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature),
71980
- wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime),
72611
+ wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime, ctx.runtime.projectKey),
71981
72612
  wireInteraction(pipelineEventBus, ctx.interactionChain, ctx.config),
71982
72613
  wireEventsWriter(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir),
71983
72614
  wireRegistry(pipelineEventBus, ctx.feature, ctx.runId, ctx.workdir, ctx.runtime.outputDir)
@@ -73822,7 +74453,8 @@ function buildPostRunContext(opts, durationMs, logger) {
73822
74453
  projectKey,
73823
74454
  curatorRollupPath: curatorRollupPath2,
73824
74455
  logFilePath,
73825
- config: config2
74456
+ config: config2,
74457
+ startTime
73826
74458
  } = opts;
73827
74459
  const counts = countStories(prd);
73828
74460
  return {
@@ -73833,6 +74465,7 @@ function buildPostRunContext(opts, durationMs, logger) {
73833
74465
  branch,
73834
74466
  version: version2,
73835
74467
  totalDurationMs: durationMs,
74468
+ runStartedAt: startTime,
73836
74469
  totalCost: totalCost2,
73837
74470
  storySummary: {
73838
74471
  completed: storiesCompleted,
@@ -74160,6 +74793,7 @@ var exports_execution = {};
74160
74793
  __export(exports_execution, {
74161
74794
  writeExitSummary: () => writeExitSummary,
74162
74795
  withIncreasingFailuresBail: () => withIncreasingFailuresBail,
74796
+ toReviewDecisionPayload: () => toReviewDecisionPayload,
74163
74797
  synthesizeBackfillMetric: () => synthesizeBackfillMetric,
74164
74798
  stopHeartbeat: () => stopHeartbeat,
74165
74799
  startHeartbeat: () => startHeartbeat2,
@@ -103797,6 +104431,88 @@ var init_bakeoff = __esm(() => {
103797
104431
  init_report2();
103798
104432
  });
103799
104433
 
104434
+ // src/plugins/builtin/curator/rollup-prune.ts
104435
+ import { rename as rename4, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
104436
+ import { appendFile as appendFile6 } from "fs/promises";
104437
+ async function scanProjectRunIds(rollupPath, projectKey) {
104438
+ const maxTsByRunId = new Map;
104439
+ for await (const line of streamJsonlLines(Bun.file(rollupPath))) {
104440
+ if (!line.trim())
104441
+ continue;
104442
+ let obs;
104443
+ try {
104444
+ obs = JSON.parse(line);
104445
+ } catch {
104446
+ continue;
104447
+ }
104448
+ if (obs.projectKey !== projectKey)
104449
+ continue;
104450
+ const existing = maxTsByRunId.get(obs.runId);
104451
+ if (existing === undefined || obs.ts > existing)
104452
+ maxTsByRunId.set(obs.runId, obs.ts);
104453
+ }
104454
+ return [...maxTsByRunId.entries()].sort((a, b) => a[1] > b[1] ? -1 : a[1] < b[1] ? 1 : 0).map(([runId]) => runId);
104455
+ }
104456
+ async function pruneRollup(input) {
104457
+ const { rollupPath, projectKey, keepRunIds, dropUnattributed = false } = input;
104458
+ const tmpPath = `${rollupPath}.gc-tmp`;
104459
+ const result2 = { kept: 0, dropped: 0, keptOtherProjects: 0, keptUnattributed: 0 };
104460
+ let buffer = "";
104461
+ const flush = async () => {
104462
+ if (buffer.length === 0)
104463
+ return;
104464
+ await appendFile6(tmpPath, buffer);
104465
+ buffer = "";
104466
+ };
104467
+ try {
104468
+ await writeFile3(tmpPath, "");
104469
+ for await (const line of streamJsonlLines(Bun.file(rollupPath))) {
104470
+ if (!line.trim())
104471
+ continue;
104472
+ let obs = null;
104473
+ try {
104474
+ obs = JSON.parse(line);
104475
+ } catch {
104476
+ obs = null;
104477
+ }
104478
+ let keep;
104479
+ if (obs === null) {
104480
+ keep = !dropUnattributed;
104481
+ if (keep)
104482
+ result2.keptUnattributed++;
104483
+ } else if (obs.projectKey === undefined) {
104484
+ keep = !dropUnattributed;
104485
+ if (keep)
104486
+ result2.keptUnattributed++;
104487
+ } else if (obs.projectKey !== projectKey) {
104488
+ keep = true;
104489
+ result2.keptOtherProjects++;
104490
+ } else {
104491
+ keep = keepRunIds.has(obs.runId);
104492
+ }
104493
+ if (!keep) {
104494
+ result2.dropped++;
104495
+ continue;
104496
+ }
104497
+ result2.kept++;
104498
+ buffer += `${line}
104499
+ `;
104500
+ if (buffer.length >= FLUSH_BYTES)
104501
+ await flush();
104502
+ }
104503
+ await flush();
104504
+ await rename4(tmpPath, rollupPath);
104505
+ } catch (err) {
104506
+ await unlink4(tmpPath).catch(() => {});
104507
+ throw err;
104508
+ }
104509
+ return result2;
104510
+ }
104511
+ var FLUSH_BYTES;
104512
+ var init_rollup_prune = __esm(() => {
104513
+ FLUSH_BYTES = 4 * 1024 * 1024;
104514
+ });
104515
+
103800
104516
  // src/commands/curator.ts
103801
104517
  var exports_curator = {};
103802
104518
  __export(exports_curator, {
@@ -103804,14 +104520,12 @@ __export(exports_curator, {
103804
104520
  curatorGc: () => curatorGc,
103805
104521
  curatorDryrun: () => curatorDryrun,
103806
104522
  curatorCommit: () => curatorCommit,
104523
+ _testing: () => _testing,
103807
104524
  _curatorCmdDeps: () => _curatorCmdDeps
103808
104525
  });
103809
104526
  import { readdirSync as readdirSync9 } from "fs";
103810
- import { unlink as unlink4 } from "fs/promises";
103811
- import { basename as basename18, join as join100 } from "path";
103812
- function getProjectKey(config2, projectDir) {
103813
- return config2.name?.trim() || basename18(projectDir);
103814
- }
104527
+ import { unlink as unlink5 } from "fs/promises";
104528
+ import { join as join100 } from "path";
103815
104529
  function listRunIds(runsDir) {
103816
104530
  try {
103817
104531
  return readdirSync9(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
@@ -104053,34 +104767,25 @@ async function curatorGc(options) {
104053
104767
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
104054
104768
  const gDir = _curatorCmdDeps.globalOutputDir();
104055
104769
  const rollupPath = _curatorCmdDeps.curatorRollupPath(gDir, config2.curator?.rollupPath);
104056
- const rollupText = await _curatorCmdDeps.readFile(rollupPath).catch(() => null);
104057
- if (rollupText === null) {
104770
+ if (!await _curatorCmdDeps.fileExists(rollupPath)) {
104058
104771
  console.log(`[gc] No rollup file found at ${rollupPath}. Nothing to prune.`);
104059
104772
  return;
104060
104773
  }
104061
- const lines = rollupText.trim().split(`
104062
- `).filter(Boolean);
104063
- const observations = lines.map((l) => JSON.parse(l));
104064
- const maxTsByRunId = new Map;
104065
- for (const obs of observations) {
104066
- const existing = maxTsByRunId.get(obs.runId);
104067
- if (!existing || obs.ts > existing) {
104068
- maxTsByRunId.set(obs.runId, obs.ts);
104069
- }
104070
- }
104774
+ const projectKey = getProjectKey(config2, resolved.projectDir);
104775
+ const uniqueRunIds = await _curatorCmdDeps.scanProjectRunIds(rollupPath, projectKey);
104071
104776
  const keep = options.keep ?? DEFAULT_KEEP;
104072
- const uniqueRunIds = [...maxTsByRunId.entries()].sort((a, b) => a[1] > b[1] ? -1 : a[1] < b[1] ? 1 : 0).map(([runId]) => runId);
104073
- if (uniqueRunIds.length <= keep) {
104074
- console.log(`[gc] ${uniqueRunIds.length} unique run(s) in rollup \u2014 at or below keep=${keep}. Nothing to prune.`);
104777
+ const sweep = options.sweepUnattributed === true;
104778
+ if (uniqueRunIds.length <= keep && !sweep) {
104779
+ console.log(`[gc] ${uniqueRunIds.length} unique run(s) for ${projectKey} in rollup \u2014 at or below keep=${keep}. Nothing to prune.`);
104075
104780
  return;
104076
104781
  }
104077
104782
  const keepSet = new Set(uniqueRunIds.slice(0, keep));
104078
- const filtered = observations.filter((obs) => keepSet.has(obs.runId));
104079
- const newContent = `${filtered.map((obs) => JSON.stringify(obs)).join(`
104080
- `)}
104081
- `;
104082
- await _curatorCmdDeps.writeFile(rollupPath, newContent);
104083
- const projectKey = getProjectKey(config2, resolved.projectDir);
104783
+ const result2 = await _curatorCmdDeps.pruneRollup({
104784
+ rollupPath,
104785
+ projectKey,
104786
+ keepRunIds: keepSet,
104787
+ dropUnattributed: sweep
104788
+ });
104084
104789
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
104085
104790
  const perRunsDir = join100(outputDir, "runs");
104086
104791
  for (const runId of uniqueRunIds) {
@@ -104090,12 +104795,20 @@ async function curatorGc(options) {
104090
104795
  await _curatorCmdDeps.removeFile(join100(runDir, "curator-proposals.md"));
104091
104796
  }
104092
104797
  }
104093
- console.log(`[gc] Pruned rollup to ${keep} most recent runs (was ${uniqueRunIds.length}).`);
104798
+ const droppedRuns = Math.max(0, uniqueRunIds.length - keepSet.size);
104799
+ console.log(`[gc] Pruned rollup for ${projectKey}: kept ${keepSet.size} of ${uniqueRunIds.length} run(s), dropped ${result2.dropped} row(s).`);
104800
+ console.log(`[gc] Preserved ${result2.keptOtherProjects} row(s) from other projects and ${result2.keptUnattributed} unattributed row(s).`);
104801
+ if (!sweep && result2.keptUnattributed > 0) {
104802
+ console.log(`[gc] ${result2.keptUnattributed} unattributed row(s) predate project scoping (#1429) and can never be read back. Run with --sweep-unattributed to drop them machine-wide.`);
104803
+ }
104804
+ if (droppedRuns === 0 && sweep)
104805
+ console.log("[gc] No run-level pruning was needed; only the unattributed sweep ran.");
104094
104806
  }
104095
- var _curatorCmdDeps, DEFAULT_KEEP = 50;
104807
+ var _curatorCmdDeps, _testing, DEFAULT_KEEP = 50;
104096
104808
  var init_curator2 = __esm(() => {
104097
104809
  init_config();
104098
104810
  init_heuristics();
104811
+ init_rollup_prune();
104099
104812
  init_paths2();
104100
104813
  init_common();
104101
104814
  _curatorCmdDeps = {
@@ -104105,6 +104818,9 @@ var init_curator2 = __esm(() => {
104105
104818
  globalOutputDir: () => globalOutputDir(),
104106
104819
  curatorRollupPath: (gDir, override) => curatorRollupPath(gDir, override),
104107
104820
  readFile: async (p) => Bun.file(p).text(),
104821
+ fileExists: async (p) => Bun.file(p).exists(),
104822
+ scanProjectRunIds: (rollupPath, projectKey) => scanProjectRunIds(rollupPath, projectKey),
104823
+ pruneRollup: (input) => pruneRollup(input),
104108
104824
  writeFile: async (p, content) => {
104109
104825
  await Bun.write(p, content);
104110
104826
  },
@@ -104115,7 +104831,7 @@ var init_curator2 = __esm(() => {
104115
104831
  },
104116
104832
  removeFile: async (p) => {
104117
104833
  try {
104118
- await unlink4(p);
104834
+ await unlink5(p);
104119
104835
  } catch {}
104120
104836
  },
104121
104837
  openInEditor: async (filePath) => {
@@ -104126,13 +104842,14 @@ var init_curator2 = __esm(() => {
104126
104842
  }
104127
104843
  }
104128
104844
  };
104845
+ _testing = { parseCheckedProposals };
104129
104846
  });
104130
104847
 
104131
104848
  // bin/nax.ts
104132
104849
  init_source();
104133
104850
  import { existsSync as existsSync39, mkdirSync as mkdirSync8 } from "fs";
104134
104851
  import { homedir as homedir3 } from "os";
104135
- import { basename as basename19, join as join101 } from "path";
104852
+ import { basename as basename20, join as join101 } from "path";
104136
104853
 
104137
104854
  // node_modules/commander/esm.mjs
104138
104855
  var import__ = __toESM(require_commander(), 1);
@@ -104789,12 +105506,12 @@ init_errors();
104789
105506
  init_logger2();
104790
105507
  init_runtime();
104791
105508
  import { existsSync as existsSync18, readdirSync as readdirSync4 } from "fs";
104792
- import { basename as basename9, join as join46 } from "path";
105509
+ import { basename as basename10, join as join46 } from "path";
104793
105510
  async function resolveOutputDir(workdir, override) {
104794
105511
  if (override)
104795
105512
  return override;
104796
105513
  const config2 = await loadConfig(workdir).catch(() => null);
104797
- const projectKey = config2?.name?.trim() || basename9(workdir);
105514
+ const projectKey = config2?.name?.trim() || basename10(workdir);
104798
105515
  return projectOutputDir(projectKey, config2?.outputDir);
104799
105516
  }
104800
105517
  async function parseRunLog(logPath) {
@@ -105829,7 +106546,7 @@ async function contextInspectCommand(options) {
105829
106546
  init_canonical_loader();
105830
106547
  init_errors();
105831
106548
  import { mkdir as mkdir11 } from "fs/promises";
105832
- import { basename as basename13, join as join72 } from "path";
106549
+ import { basename as basename14, join as join72 } from "path";
105833
106550
  var _rulesCLIDeps = {
105834
106551
  readFile: async (path24) => Bun.file(path24).text(),
105835
106552
  writeFile: async (path24, content) => {
@@ -105914,6 +106631,32 @@ function neutralizeContent(content) {
105914
106631
  apply(/\p{Extended_Pictographic}/gu, "");
105915
106632
  return { content: result.trim(), replacements };
105916
106633
  }
106634
+ function translateLegacyFrontmatter(content) {
106635
+ const fm = /^---\n([\s\S]*?)\n---\n/.exec(content);
106636
+ if (!fm?.[1])
106637
+ return { content, translated: false };
106638
+ const block = fm[1];
106639
+ if (!/^paths:/m.test(block) || /^appliesTo:/m.test(block))
106640
+ return { content, translated: false };
106641
+ const rewritten = block.replace(/^paths:/m, "appliesTo:");
106642
+ const head = content.slice(0, fm.index);
106643
+ const tail = content.slice(fm.index + fm[0].length);
106644
+ return { content: `${head}---
106645
+ ${rewritten}
106646
+ ---
106647
+ ${tail}`, translated: true };
106648
+ }
106649
+ function withReviewNotice(content, replacements) {
106650
+ if (replacements <= 0)
106651
+ return content;
106652
+ const notice = `<!-- NOTE: ${replacements} neutralization(s) applied \u2014 review before committing -->
106653
+
106654
+ `;
106655
+ const fm = /^---\n[\s\S]*?\n---\n/.exec(content);
106656
+ if (!fm)
106657
+ return notice + content;
106658
+ return content.slice(0, fm[0].length) + notice + content.slice(fm[0].length).replace(/^\n+/, "");
106659
+ }
105917
106660
  async function collectMigrationSources(workdir) {
105918
106661
  const sources = [];
105919
106662
  const claudeMdPath = join72(workdir, "CLAUDE.md");
@@ -105929,7 +106672,7 @@ async function collectMigrationSources(workdir) {
105929
106672
  try {
105930
106673
  const content = await _rulesCLIDeps.readFile(filePath);
105931
106674
  if (content.trim()) {
105932
- sources.push({ sourcePath: filePath, targetFileName: basename13(filePath), content });
106675
+ sources.push({ sourcePath: filePath, targetFileName: basename14(filePath), content });
105933
106676
  }
105934
106677
  } catch {}
105935
106678
  }
@@ -105960,11 +106703,9 @@ async function rulesMigrateCommand(options) {
105960
106703
  skipped++;
105961
106704
  continue;
105962
106705
  }
105963
- const { content: neutralized, replacements } = neutralizeContent(content);
105964
- const notice = replacements > 0 ? `<!-- NOTE: ${replacements} neutralization(s) applied \u2014 review before committing -->
105965
-
105966
- ` : "";
105967
- const output = notice + neutralized;
106706
+ const { content: scoped2 } = translateLegacyFrontmatter(content);
106707
+ const { content: neutralized, replacements } = neutralizeContent(scoped2);
106708
+ const output = withReviewNotice(neutralized, replacements);
105968
106709
  if (options.dryRun) {
105969
106710
  console.log(`[dry-run] Would write ${targetFileName} from ${sourcePath} (${replacements} replacements)`);
105970
106711
  } else {
@@ -106040,6 +106781,7 @@ async function resolveRunProfileOverride(opts) {
106040
106781
  }
106041
106782
  // src/cli/features-resolve.ts
106042
106783
  init_config();
106784
+ init_test_runners();
106043
106785
  import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
106044
106786
  import { join as join74, relative as relative17 } from "path";
106045
106787
 
@@ -106098,6 +106840,15 @@ async function resolveGroupCommand(repoRoot, packageDir, rootCommand) {
106098
106840
  }
106099
106841
 
106100
106842
  // src/cli/features-resolve.ts
106843
+ async function resolveTestPatterns(workdir) {
106844
+ try {
106845
+ const config2 = await loadConfig(workdir);
106846
+ const resolved = await resolveTestFilePatterns(config2, workdir);
106847
+ return { regex: resolved.regex.map((r) => r.source), resolution: resolved.resolution };
106848
+ } catch {
106849
+ return;
106850
+ }
106851
+ }
106101
106852
  async function isNonEmptyFile(absolutePath) {
106102
106853
  if (!existsSync28(absolutePath))
106103
106854
  return false;
@@ -106202,6 +106953,7 @@ async function resolveFeatureSpec(name, workdir) {
106202
106953
  featureName: name,
106203
106954
  specSource: source2,
106204
106955
  acceptance: await resolveFeatureAcceptance(name, workdir),
106956
+ testPatterns: await resolveTestPatterns(workdir),
106205
106957
  message: `resolved spec: ${source2.path}`
106206
106958
  };
106207
106959
  }
@@ -106248,6 +107000,7 @@ async function resolveFeatureSpec(name, workdir) {
106248
107000
  featureName: onlyName,
106249
107001
  specSource: source,
106250
107002
  acceptance: await resolveFeatureAcceptance(onlyName, workdir),
107003
+ testPatterns: await resolveTestPatterns(workdir),
106251
107004
  message: `resolved spec: ${source.path}`
106252
107005
  };
106253
107006
  }
@@ -106267,7 +107020,7 @@ init_runtime();
106267
107020
  init_json_file();
106268
107021
  init_routing();
106269
107022
  import { mkdirSync as mkdirSync6 } from "fs";
106270
- import { basename as basename14, join as join75 } from "path";
107023
+ import { basename as basename15, join as join75 } from "path";
106271
107024
  var _routingCalibrateDeps = {
106272
107025
  loadRunMetrics: (outputDir) => loadRunMetrics(outputDir),
106273
107026
  readConfig: (workdir) => loadConfig(workdir),
@@ -106325,7 +107078,7 @@ async function runRoutingCalibrateCli(options, deps = _routingCalibrateDeps) {
106325
107078
  function resolveOutputDir2(workdir, override, prior) {
106326
107079
  if (override)
106327
107080
  return override;
106328
- const key = prior?.name?.trim() || basename14(workdir);
107081
+ const key = prior?.name?.trim() || basename15(workdir);
106329
107082
  return projectOutputDir(key, prior?.outputDir);
106330
107083
  }
106331
107084
  function mergeComplexityRouting(prior, adjustments) {
@@ -107311,7 +108064,7 @@ init_errors();
107311
108064
  init_checkpoint();
107312
108065
  init_runtime();
107313
108066
  import { existsSync as existsSync37 } from "fs";
107314
- import { basename as basename17, join as join95 } from "path";
108067
+ import { basename as basename18, join as join95 } from "path";
107315
108068
  async function defaultCheckpointExists(featureDir) {
107316
108069
  if (!featureDir || !existsSync37(featureDir))
107317
108070
  return false;
@@ -107390,7 +108143,7 @@ function registerResumeCommand(program2) {
107390
108143
  const globalNaxDir = globalConfigDir();
107391
108144
  const hooks = await loadHooksConfig2(naxDir, globalNaxDir);
107392
108145
  applyResumeModeDeps2(opts.featureDir ?? "", "auto");
107393
- const projectKey = config2.name?.trim() || basename17(cmdOpts.dir);
108146
+ const projectKey = config2.name?.trim() || basename18(cmdOpts.dir);
107394
108147
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
107395
108148
  const statusFilePath = join95(outputDir, "status.json");
107396
108149
  const result = await run2({
@@ -115841,7 +116594,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
115841
116594
  process.exit(1);
115842
116595
  }
115843
116596
  resetLogger();
115844
- const projectKey = config2.name?.trim() || basename19(workdir);
116597
+ const projectKey = config2.name?.trim() || basename20(workdir);
115845
116598
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
115846
116599
  const runsDir = join101(outputDir, "features", options.feature, "runs");
115847
116600
  mkdirSync8(runsDir, { recursive: true });
@@ -116462,12 +117215,13 @@ curator.command("dryrun").description("Re-run heuristics on an existing observat
116462
117215
  process.exit(1);
116463
117216
  }
116464
117217
  });
116465
- curator.command("gc").description("Prune old rows from the curator rollup JSONL").option("-p, --project <path>", "Project directory (default: CWD)").option("--keep <N>", "Number of most recent runIds to keep (default: 50)", "50").action(async (options) => {
117218
+ curator.command("gc").description("Prune old rows from the curator rollup JSONL").option("-p, --project <path>", "Project directory (default: CWD)").option("--keep <N>", "Number of most recent runIds to keep (default: 50)", "50").option("--sweep-unattributed", "Also drop rows with no projectKey (pre-#1429 history, unreadable by any project). Machine-wide \u2014 affects every project sharing the rollup.").action(async (options) => {
116466
117219
  const { curatorGc: curatorGc2 } = await Promise.resolve().then(() => (init_curator2(), exports_curator));
116467
117220
  try {
116468
117221
  await curatorGc2({
116469
117222
  project: options.project,
116470
- keep: options.keep !== undefined ? Number.parseInt(options.keep, 10) : undefined
117223
+ keep: options.keep !== undefined ? Number.parseInt(options.keep, 10) : undefined,
117224
+ sweepUnattributed: options.sweepUnattributed === true
116471
117225
  });
116472
117226
  } catch (err) {
116473
117227
  console.error(source_default.red(`Error: ${err.message}`));