@nathapp/nax 0.75.6 → 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
@@ -17823,6 +17823,7 @@ var init_schemas3 = __esm(() => {
17823
17823
  enabled: exports_external.boolean().default(false),
17824
17824
  flowPath: exports_external.string().default("flows/nax-finish/nax-finish.flow.ts"),
17825
17825
  defaultAgent: exports_external.string().nullable().default(null),
17826
+ model: exports_external.string().min(1, "model must be non-empty").nullable().default(null),
17826
17827
  reviewers: exports_external.object({
17827
17828
  spec: exports_external.string().nullable().default(null),
17828
17829
  quality: exports_external.string().nullable().default(null)
@@ -17839,6 +17840,7 @@ var init_schemas3 = __esm(() => {
17839
17840
  enabled: false,
17840
17841
  flowPath: "flows/nax-finish/nax-finish.flow.ts",
17841
17842
  defaultAgent: null,
17843
+ model: null,
17842
17844
  reviewers: { spec: null, quality: null },
17843
17845
  escalate: { telegram: true },
17844
17846
  notify: { mode: "escalation" },
@@ -17849,6 +17851,7 @@ var init_schemas3 = __esm(() => {
17849
17851
  enabled: false,
17850
17852
  flowPath: "flows/nax-finish/nax-finish.flow.ts",
17851
17853
  defaultAgent: null,
17854
+ model: null,
17852
17855
  reviewers: { spec: null, quality: null },
17853
17856
  escalate: { telegram: true },
17854
17857
  notify: { mode: "escalation" },
@@ -19639,6 +19642,13 @@ function reshapeSelector(name, fn) {
19639
19642
  return { name, select: fn };
19640
19643
  }
19641
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
+
19642
19652
  // src/config/selectors.ts
19643
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;
19644
19654
  var init_selectors = __esm(() => {
@@ -19659,7 +19669,7 @@ var init_selectors = __esm(() => {
19659
19669
  mutationCheckConfigSelector = reshapeSelector("mutation-check", (c) => c.execution?.mutationCheck);
19660
19670
  rectificationGateConfigSelector = pickSelector("rectification-gate", "execution", "models", "agent", "quality", "review");
19661
19671
  agentConfigSelector = pickSelector("agent", "agent");
19662
- agentManagerConfigSelector = pickSelector("agent-manager", "agent", "execution");
19672
+ agentManagerConfigSelector = pickSelector("agent-manager", "agent", "execution", "profile");
19663
19673
  interactionConfigSelector = pickSelector("interaction", "interaction");
19664
19674
  precheckConfigSelector = pickSelector("precheck", "precheck", "quality", "execution", "prompts", "review", "project");
19665
19675
  qualityConfigSelector = pickSelector("quality", "quality", "execution");
@@ -19670,7 +19680,7 @@ var init_selectors = __esm(() => {
19670
19680
  contextToolRuntimeConfigSelector = pickSelector("context-tool-runtime", "context", "execution", "project", "quality");
19671
19681
  promptLoaderConfigSelector = pickSelector("prompt-loader", "prompts", "context", "project");
19672
19682
  llmRoutingConfigSelector = pickSelector("llm-routing", "routing", "models", "agent", "tdd", "execution", "precheck");
19673
- finishConfigSelector = pickSelector("finish", "finish", "interaction", "quality");
19683
+ finishConfigSelector = pickSelector("finish", "finish", "interaction", "quality", "agent");
19674
19684
  });
19675
19685
 
19676
19686
  // src/config/loader-runtime.ts
@@ -19950,6 +19960,7 @@ __export(exports_config, {
19950
19960
  interactionConfigSelector: () => interactionConfigSelector,
19951
19961
  globalConfigPath: () => globalConfigPath,
19952
19962
  globalConfigDir: () => globalConfigDir,
19963
+ getProjectKey: () => getProjectKey,
19953
19964
  getAcQualityRules: () => getAcQualityRules,
19954
19965
  finishConfigSelector: () => finishConfigSelector,
19955
19966
  findProjectDir: () => findProjectDir,
@@ -20001,6 +20012,7 @@ var init_config = __esm(() => {
20001
20012
  init_path_security();
20002
20013
  init_paths();
20003
20014
  init_profile();
20015
+ init_project_key();
20004
20016
  init_selectors();
20005
20017
  init_test_strategy();
20006
20018
  });
@@ -20113,6 +20125,11 @@ function estimateCostFromTokenUsage(usage, model) {
20113
20125
  const cacheCreationCost = (usage.cacheCreationInputTokens ?? 0) * cacheCreationRate;
20114
20126
  return inputCost + outputCost + cacheReadCost + cacheCreationCost;
20115
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
+ }
20116
20133
  var init_calculate = __esm(() => {
20117
20134
  init_pricing();
20118
20135
  });
@@ -21116,6 +21133,8 @@ class AcpSessionHandleImpl {
21116
21133
  id;
21117
21134
  agentName;
21118
21135
  protocolIds;
21136
+ modelDef;
21137
+ modelTier;
21119
21138
  _client;
21120
21139
  _session;
21121
21140
  _sessionName;
@@ -21133,6 +21152,8 @@ class AcpSessionHandleImpl {
21133
21152
  this._resumed = opts.resumed;
21134
21153
  this._timeoutSeconds = opts.timeoutSeconds;
21135
21154
  this._modelDef = opts.modelDef;
21155
+ this.modelDef = opts.modelDef;
21156
+ this.modelTier = opts.modelTier;
21136
21157
  this._permissionMode = opts.permissionMode;
21137
21158
  }
21138
21159
  }
@@ -21518,6 +21539,7 @@ class AcpAgentAdapter {
21518
21539
  resumed: ensured.resumed,
21519
21540
  timeoutSeconds,
21520
21541
  modelDef,
21542
+ modelTier: opts.modelTier,
21521
21543
  permissionMode: resolvedPermissions.mode
21522
21544
  });
21523
21545
  } catch (error48) {
@@ -22113,6 +22135,92 @@ function formatSessionName(req) {
22113
22135
  }
22114
22136
  var init_session_name = () => {};
22115
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
+
22116
22224
  // src/agents/retry/default-strategy.ts
22117
22225
  var MAX_RETRIES = 3, defaultRetryStrategy;
22118
22226
  var init_default_strategy = __esm(() => {
@@ -22740,52 +22848,33 @@ class AgentManager {
22740
22848
  ...rawResult,
22741
22849
  protocolIds: rawResult.protocolIds ?? handle.protocolIds
22742
22850
  };
22743
- const event = {
22744
- kind: "session-turn",
22745
- sessionName: handle.id,
22851
+ const event = buildSessionTurnEvent({
22852
+ handle,
22746
22853
  sessionRole,
22747
22854
  prompt,
22748
- response: result.output,
22855
+ result,
22749
22856
  agentName,
22750
22857
  stage,
22751
- storyId: opts.storyId,
22752
- featureName: opts.featureName,
22753
- workdir: opts.workdir,
22754
- projectDir: opts.projectDir,
22858
+ opts,
22755
22859
  resolvedPermissions,
22756
- tokenUsage: result.tokenUsage,
22757
- estimatedCostUsd: result.estimatedCostUsd,
22758
- exactCostUsd: result.exactCostUsd,
22759
- durationMs: Date.now() - start,
22760
- timestamp: Date.now(),
22761
- turn: result.internalRoundTrips ?? 1,
22762
- protocolIds: {
22763
- sessionId: handle.protocolIds?.sessionId ?? null,
22764
- recordId: handle.protocolIds?.recordId ?? null
22765
- },
22766
- ...result.interactions?.length ? { interactions: result.interactions } : {},
22767
- origin: "runAsSession",
22768
- ...opts.callId !== undefined ? { callId: opts.callId } : {},
22769
- ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
22770
- };
22860
+ profile: this._config.profile,
22861
+ startedAt: start
22862
+ });
22771
22863
  this._dispatchEvents.emitDispatch(event);
22772
22864
  return result;
22773
22865
  } catch (err) {
22774
- const errEvent = {
22775
- kind: "error",
22866
+ const errEvent = buildDispatchErrorEvent({
22776
22867
  origin: "runAsSession",
22777
22868
  agentName,
22778
22869
  stage,
22779
22870
  storyId: opts.storyId,
22780
- errorCode: err instanceof NaxError ? err.code : "DISPATCH_ERROR",
22781
- errorMessage: errorMessage(err),
22871
+ error: err,
22782
22872
  prompt,
22783
- durationMs: Date.now() - start,
22784
- timestamp: Date.now(),
22785
22873
  resolvedPermissions,
22786
- ...opts.callId !== undefined ? { callId: opts.callId } : {},
22787
- ...opts.scopeId !== undefined ? { scopeId: opts.scopeId } : {}
22788
- };
22874
+ callId: opts.callId,
22875
+ scopeId: opts.scopeId,
22876
+ startedAt: start
22877
+ });
22789
22878
  this._dispatchEvents.emitDispatchError(errEvent);
22790
22879
  throw err;
22791
22880
  }
@@ -22804,44 +22893,35 @@ class AgentManager {
22804
22893
  const start = Date.now();
22805
22894
  try {
22806
22895
  const outcome = await this.completeWithFallback(prompt, augmented, agentName);
22807
- const event = {
22808
- kind: "complete",
22896
+ const event = buildCompleteEvent({
22809
22897
  sessionName,
22810
- sessionRole: options.sessionRole ?? "auto",
22811
22898
  prompt,
22812
22899
  response: outcome.result.output,
22813
22900
  agentName,
22814
22901
  stage,
22815
- storyId: options.storyId,
22816
- featureName: options.featureName,
22817
- workdir: options.workdir,
22902
+ options,
22818
22903
  resolvedPermissions,
22819
22904
  tokenUsage: outcome.result.tokenUsage,
22820
22905
  estimatedCostUsd: outcome.result.estimatedCostUsd,
22821
22906
  exactCostUsd: outcome.result.exactCostUsd,
22822
- durationMs: Date.now() - start,
22823
- timestamp: Date.now(),
22824
- ...options.callId !== undefined ? { callId: options.callId } : {},
22825
- ...options.scopeId !== undefined ? { scopeId: options.scopeId } : {}
22826
- };
22907
+ profile: this._config.profile,
22908
+ startedAt: start
22909
+ });
22827
22910
  this._dispatchEvents.emitDispatch(event);
22828
22911
  return outcome.result;
22829
22912
  } catch (err) {
22830
- const errEvent = {
22831
- kind: "error",
22913
+ const errEvent = buildDispatchErrorEvent({
22832
22914
  origin: "completeAs",
22833
22915
  agentName,
22834
22916
  stage,
22835
22917
  storyId: options.storyId,
22836
- errorCode: err instanceof NaxError ? err.code : "DISPATCH_ERROR",
22837
- errorMessage: errorMessage(err),
22918
+ error: err,
22838
22919
  prompt,
22839
- durationMs: Date.now() - start,
22840
- timestamp: Date.now(),
22841
22920
  resolvedPermissions,
22842
- ...options.callId !== undefined ? { callId: options.callId } : {},
22843
- ...options.scopeId !== undefined ? { scopeId: options.scopeId } : {}
22844
- };
22921
+ callId: options.callId,
22922
+ scopeId: options.scopeId,
22923
+ startedAt: start
22924
+ });
22845
22925
  this._dispatchEvents.emitDispatchError(errEvent);
22846
22926
  throw err;
22847
22927
  }
@@ -22864,6 +22944,7 @@ var init_manager = __esm(() => {
22864
22944
  init_dispatch_events();
22865
22945
  init_session_name();
22866
22946
  init_bun_deps();
22947
+ init_manager_dispatch();
22867
22948
  init_registry();
22868
22949
  init_default_strategy();
22869
22950
  init_hop_retry_policy();
@@ -23553,6 +23634,56 @@ var init_digest = __esm(() => {
23553
23634
  SCOPE_ORDER2 = ["project", "feature", "story", "session", "retrieved"];
23554
23635
  });
23555
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
+
23556
23687
  // src/context/engine/packing.ts
23557
23688
  function packChunks(chunks, budgetTokens, availableBudgetTokens) {
23558
23689
  const effectiveBudget = availableBudgetTokens !== undefined ? Math.min(budgetTokens, availableBudgetTokens) : budgetTokens;
@@ -25773,7 +25904,7 @@ async function captureWorkingTreeChanges(workdir, baseRef, scopePrefix) {
25773
25904
  return [];
25774
25905
  const runDiff = async (args) => {
25775
25906
  const fullArgs = scopePrefix ? [...args, "--", `${scopePrefix}/`] : args;
25776
- const { stdout, exitCode } = await gitWithTimeout(fullArgs, workdir, TIMEOUT_RETRY_GIT_TIMEOUT_MS);
25907
+ const { stdout, exitCode } = await gitWithTimeout(fullArgs, workdir, _gitDeps.timeoutRetryGitTimeoutMs);
25777
25908
  if (exitCode !== 0)
25778
25909
  return [];
25779
25910
  return stdout.trim().split(`
@@ -25822,16 +25953,20 @@ async function captureDiffSummary(workdir, baseRef, scopePrefix) {
25822
25953
  return "";
25823
25954
  }
25824
25955
  }
25825
- 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;
25826
25957
  var init_git = __esm(() => {
25827
25958
  init_logger2();
25828
25959
  init_bun_deps();
25829
- _gitDeps = { spawn, getSafeLogger };
25960
+ _gitDeps = {
25961
+ spawn,
25962
+ getSafeLogger,
25963
+ timeoutRetryGitTimeoutMs: TIMEOUT_RETRY_GIT_TIMEOUT_MS
25964
+ };
25830
25965
  });
25831
25966
 
25832
25967
  // src/utils/path-filters.ts
25833
25968
  import { join as join8, relative as relative2 } from "path";
25834
- function basename3(path) {
25969
+ function basename4(path) {
25835
25970
  const stripped = path.startsWith("./") ? path.slice(2) : path;
25836
25971
  const idx = stripped.lastIndexOf("/");
25837
25972
  return idx === -1 ? stripped : stripped.slice(idx + 1);
@@ -25961,7 +26096,7 @@ function isNaxInternalPath(path) {
25961
26096
  return true;
25962
26097
  if (path.includes("/.nax/"))
25963
26098
  return true;
25964
- return LOCKFILE_BASENAMES.has(basename3(path));
26099
+ return LOCKFILE_BASENAMES.has(basename4(path));
25965
26100
  }
25966
26101
  function filterNaxInternalPaths(paths, ignoreMatchers = []) {
25967
26102
  return paths.filter((path) => !isNaxInternalPath(path) && !ignoreMatchers.some((matcher) => matcher.test(path)));
@@ -26005,8 +26140,8 @@ async function getGitRootMemo(workdir) {
26005
26140
  return result ?? null;
26006
26141
  }
26007
26142
  function extractBasenamePattern(pattern) {
26008
- const basename4 = pattern.slice(pattern.lastIndexOf("/") + 1);
26009
- const parts = basename4.split("*");
26143
+ const basename5 = pattern.slice(pattern.lastIndexOf("/") + 1);
26144
+ const parts = basename5.split("*");
26010
26145
  if (parts.length !== 2)
26011
26146
  return null;
26012
26147
  const [prefix, suffix] = parts;
@@ -26018,8 +26153,8 @@ function extractSearchTerms(sourceFile) {
26018
26153
  const withoutPrefix = sourceFile.replace(/^(?:.*\/)?src\//, "");
26019
26154
  const withoutExt = withoutPrefix.replace(/\.[^.]+$/, "");
26020
26155
  const parts = withoutExt.split("/");
26021
- const basename4 = parts[parts.length - 1];
26022
- return [`/${basename4}`, withoutExt];
26156
+ const basename5 = parts[parts.length - 1];
26157
+ return [`/${basename5}`, withoutExt];
26023
26158
  }
26024
26159
  async function importGrepFallback(sourceFiles, workdir, testFilePatterns, maxScanFiles = MAX_GREP_TEST_FILES) {
26025
26160
  if (sourceFiles.length === 0 || testFilePatterns.length === 0)
@@ -27839,33 +27974,21 @@ class ContextOrchestrator {
27839
27974
  const digest = buildDigest(packed);
27840
27975
  const dTokens = digestTokens(digest);
27841
27976
  const buildMs = _orchestratorDeps.now() - startMs;
27842
- const staleChunkIds = packed.filter((c) => c.staleCandidate).map((c) => c.id);
27843
- const chunkSummaries = {};
27844
- for (const c of packed) {
27845
- chunkSummaries[c.id] = c.content.slice(0, 300);
27846
- }
27847
- const manifest = {
27977
+ const manifest = buildManifest({
27848
27978
  requestId,
27849
- stage: request.stage,
27850
- totalBudgetTokens: request.budgetTokens,
27851
- usedTokens: usedTokens + dTokens,
27852
- includedChunks: packed.map((c) => c.id),
27853
- excludedChunks: [
27854
- ...roleFiltered.map((c) => ({ id: c.id, reason: "role-filter" })),
27855
- ...belowMin.map((c) => ({ id: c.id, reason: "below-min-score" })),
27856
- ...dedupeDropped.map((id) => ({ id, reason: "dedupe" })),
27857
- ...budgetExcludedIds.map((id) => ({ id, reason: "budget" }))
27858
- ],
27859
- floorItems: floorPackedIds,
27860
- floorOverageItems: floorOverageIds.length > 0 ? floorOverageIds : undefined,
27979
+ request,
27980
+ packed,
27981
+ usedTokens,
27861
27982
  digestTokens: dTokens,
27862
27983
  buildMs,
27863
27984
  providerResults,
27864
- repoRoot: request.repoRoot,
27865
- packageDir: request.packageDir,
27866
- ...Object.keys(chunkSummaries).length > 0 && { chunkSummaries },
27867
- ...staleChunkIds.length > 0 && { staleChunks: staleChunkIds }
27868
- };
27985
+ roleFiltered,
27986
+ belowMin,
27987
+ dedupeDropped,
27988
+ budgetExcludedIds,
27989
+ floorPackedIds,
27990
+ floorOverageIds
27991
+ });
27869
27992
  logger.debug("context-v2", "Bundle assembled", {
27870
27993
  storyId: request.storyId,
27871
27994
  stage: request.stage,
@@ -27922,6 +28045,7 @@ class ContextOrchestrator {
27922
28045
  ...prior.manifest,
27923
28046
  requestId: _orchestratorDeps.uuid(),
27924
28047
  includedChunks: packedChunks.map((c) => c.id),
28048
+ chunkTokens: Object.fromEntries(packedChunks.map((c) => [c.id, c.tokens])),
27925
28049
  usedTokens: Math.max(0, prior.manifest.usedTokens - prior.manifest.digestTokens + dTokens + extraTokens),
27926
28050
  digestTokens: dTokens,
27927
28051
  buildMs: 0,
@@ -27959,7 +28083,7 @@ var init_orchestrator = __esm(() => {
27959
28083
  });
27960
28084
 
27961
28085
  // src/context/rules/canonical-loader.ts
27962
- import { basename as basename4, join as join12 } from "path";
28086
+ import { basename as basename5, join as join12 } from "path";
27963
28087
  function parseRuleAllowMarker(line) {
27964
28088
  const allowed = new Set;
27965
28089
  RULE_ALLOW_MARKER.lastIndex = 0;
@@ -28099,7 +28223,7 @@ async function loadCanonicalRules(workdir, options = {}) {
28099
28223
  const filePaths = allFilePaths.filter((filePath) => {
28100
28224
  const normalized = filePath.replaceAll("\\", "/");
28101
28225
  const normalizedRulesDir = rulesDir.replaceAll("\\", "/");
28102
- 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);
28103
28227
  return relativePath.split("/").length <= 2;
28104
28228
  });
28105
28229
  if (allFilePaths.length > filePaths.length) {
@@ -28115,8 +28239,8 @@ async function loadCanonicalRules(workdir, options = {}) {
28115
28239
  for (const filePath of filePaths) {
28116
28240
  const normalizedPath = filePath.replaceAll("\\", "/");
28117
28241
  const normalizedRulesDir = rulesDir.replaceAll("\\", "/");
28118
- const relativePath = normalizedPath.startsWith(`${normalizedRulesDir}/`) ? normalizedPath.slice(normalizedRulesDir.length + 1) : basename4(normalizedPath);
28119
- const fileName = basename4(filePath);
28242
+ const relativePath = normalizedPath.startsWith(`${normalizedRulesDir}/`) ? normalizedPath.slice(normalizedRulesDir.length + 1) : basename5(normalizedPath);
28243
+ const fileName = basename5(filePath);
28120
28244
  let content;
28121
28245
  try {
28122
28246
  content = await _canonicalLoaderDeps.readFile(filePath);
@@ -29778,8 +29902,8 @@ function deriveTestPatterns(contextFiles, resolvedGlobs) {
29778
29902
  const suffixes = resolvedGlobs ? extractGlobSuffixes(resolvedGlobs) : DEFAULT_TS_DERIVE_SUFFIXES;
29779
29903
  const effectiveSuffixes = suffixes.length > 0 ? suffixes : DEFAULT_TS_DERIVE_SUFFIXES;
29780
29904
  for (const filePath of contextFiles) {
29781
- const basename5 = path.basename(filePath);
29782
- const basenameNoExt = basename5.replace(/\.[^.]+$/, "");
29905
+ const basename6 = path.basename(filePath);
29906
+ const basenameNoExt = basename6.replace(/\.[^.]+$/, "");
29783
29907
  for (const suffix of effectiveSuffixes) {
29784
29908
  patterns.add(`${basenameNoExt}${suffix}`);
29785
29909
  }
@@ -29841,8 +29965,8 @@ async function scanTestFiles(options) {
29841
29965
  const files = [];
29842
29966
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
29843
29967
  if (allowedBasenames !== null) {
29844
- const basename5 = path.basename(filePath);
29845
- if (!allowedBasenames.has(basename5)) {
29968
+ const basename6 = path.basename(filePath);
29969
+ if (!allowedBasenames.has(basename6)) {
29846
29970
  continue;
29847
29971
  }
29848
29972
  }
@@ -31601,6 +31725,11 @@ isolation scope: Only create or modify files in the test/ directory. Tests must
31601
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}`;
31602
31726
  }
31603
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
+ }
31604
31733
  return `${header}
31605
31734
 
31606
31735
  isolation scope: Implement source code in src/ to make tests pass. Do not modify test files. Run tests frequently to track progress.${footer}`;
@@ -32167,12 +32296,36 @@ Include the story ID when known \u2014 \`feat(<story-id>): <description>\`.
32167
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.`;
32168
32297
  }
32169
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
+
32170
32322
  // src/prompts/sections/index.ts
32171
32323
  var init_sections2 = __esm(() => {
32172
32324
  init_hermetic();
32173
32325
  init_role_task();
32174
32326
  init_story();
32175
32327
  init_acceptance();
32328
+ init_test_quality();
32176
32329
  });
32177
32330
 
32178
32331
  // src/prompts/loader.ts
@@ -32315,7 +32468,7 @@ class TddPromptBuilder {
32315
32468
  if (this.role === "verifier" && this.story_) {
32316
32469
  acc.add(this.s("verdict", buildVerdictSection(this.story_)));
32317
32470
  }
32318
- const isolation = this.options.isolation;
32471
+ const isolation = this.role === "implementer" && this.options.variant === "lite" ? "lite" : this.options.isolation;
32319
32472
  acc.add(this.s("isolation", buildIsolationSection(this.role, isolation, this.testCommand_)));
32320
32473
  const tddLang = buildTddLanguageSection(this.loaderConfig_?.project?.language);
32321
32474
  if (tddLang)
@@ -32331,6 +32484,9 @@ class TddPromptBuilder {
32331
32484
  const guardrails = buildBehavioralGuardrailsSection(this.role, guardrailLevel, guardrailVariant, guardrailIsolation);
32332
32485
  if (guardrails)
32333
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));
32334
32490
  if (this.role !== "verifier") {
32335
32491
  const selfVerify = buildSelfVerificationSection(this.role, this.selfVerification_);
32336
32492
  if (selfVerify)
@@ -32782,6 +32938,32 @@ var init_debate_builder = __esm(() => {
32782
32938
  RE_REVIEW_JSON_DIRECTIVE = `Respond with JSON: { passed: boolean; findings: Array<${FINDING_SCHEMA}>; findingReasoning: { [ruleId: string]: string }; deltaSummary: string }`;
32783
32939
  });
32784
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
+
32785
32967
  // src/prompts/builders/prior-iterations-builder.ts
32786
32968
  function buildPriorIterationsBlock(iterations) {
32787
32969
  if (iterations.length === 0)
@@ -32840,9 +33022,13 @@ function renderVerdictTemplate(iterations) {
32840
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.` : "";
32841
33023
  return [
32842
33024
  `**Required:** before adding any new finding, classify each of the ${total} prior finding(s) above as one of:`,
32843
- "- `addressed` \u2014 the current diff resolves it (cite the diff line that fixes it in your `message` field)",
32844
- "- `still-blocking` \u2014 the implementer did not fix it; re-flag it with the IDENTICAL `file`, `line`, `category`, and substantively the same `message` wording",
32845
- `- \`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
+ "",
32846
33032
  `Then surface any genuinely new findings.${unchangedNote}`
32847
33033
  ].join(`
32848
33034
  `);
@@ -32906,13 +33092,36 @@ Flag issues only when you have confirmed:
32906
33092
  3. New code has dead paths that will never execute (stubs, noops, unreachable branches)
32907
33093
  4. New code is not wired into callers/exports (verified by grepping for usage)
32908
33094
 
32909
- 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:
32910
33111
  {
32911
33112
  "passed": boolean,
32912
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
+ ],
32913
33121
  "findings": [
32914
33122
  {
32915
33123
  "severity": "error" | "warning" | "info" | "unverifiable",
33124
+ "category": ${SEMANTIC_CATEGORY_ENUM_LINE},
32916
33125
  "file": "path/to/file",
32917
33126
  "line": 42,
32918
33127
  "issue": "description of the issue",
@@ -32930,14 +33139,13 @@ Do NOT flag: style issues, naming conventions, import ordering, file length, or
32930
33139
  }
32931
33140
 
32932
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.
32933
33144
  - \`acIndex\` is required when severity is "error" (1-based, into the Acceptance Criteria list above).
32934
33145
  - \`acQuote\` is optional advisory metadata for human auditors \u2014 not validated.
32935
33146
  - Omit both for "warning", "info", "unverifiable".
32936
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.
32937
- If all ACs are correctly implemented after inspecting the code, respond with { "passed": true, "inspectedFiles": ["..."], "findings": [] }.`, ReviewPromptBuilder;
32938
- var init_review_builder = __esm(() => {
32939
- init_sections2();
32940
- 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": [] }.`;
32941
33149
  ReviewPromptBuilder = class ReviewPromptBuilder {
32942
33150
  buildSemanticReviewPrompt(story, semanticConfig, options) {
32943
33151
  const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
@@ -32987,7 +33195,7 @@ Respond with a condensed summary:
32987
33195
  - ${advisoryClause}
32988
33196
  - Keep \`verifiedBy\` for every finding. If \`verifiedBy.observed\` is long, abbreviate it to one line \u2014 never drop the field.
32989
33197
  Output ONLY a complete, valid JSON object. It must start with { and end with }.
32990
- 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}}]}`;
32991
33199
  }
32992
33200
  static demandInspection() {
32993
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.
@@ -33042,6 +33250,7 @@ ${drops.map((d, i) => `${i + 1}. [${d.finding.severity}] ${d.finding.issue}`).jo
33042
33250
  Please re-review the code and re-issue any valid findings. For each finding you re-issue:
33043
33251
  - You MUST include a valid \`acIndex\` (1-based index into the AC list below)
33044
33252
  - You MUST include a \`verifiedBy\` field with verified evidence
33253
+ - You MUST include a \`category\`: ${SEMANTIC_CATEGORY_ENUM_LINE}
33045
33254
 
33046
33255
  ## Acceptance Criteria
33047
33256
  ${acList}
@@ -33176,6 +33385,7 @@ What new exported units lack corresponding test files?
33176
33385
  - Bodies that always pass: \`expect(true).toBe(true)\`, \`expect(x).toBe(x)\`, \`expect(1).toBe(1)\`, an empty test body, or \`assert(true)\`.
33177
33386
  - Tests skipped/disabled (\`it.skip\`, \`test.todo\`, \`xit\`, commented-out assertions) that an AC depends on.
33178
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.
33179
33389
 
33180
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.
33181
33391
 
@@ -33199,6 +33409,13 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
33199
33409
  {
33200
33410
  "passed": true | false,
33201
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
+ ],
33202
33419
  "findings": [
33203
33420
  {
33204
33421
  "severity": "error" | "warning" | "info" | "unverifiable",
@@ -33225,6 +33442,8 @@ Respond with ONLY a JSON object \u2014 no preamble, no explanation outside the J
33225
33442
 
33226
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.
33227
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
+
33228
33447
  Severity guide:
33229
33448
  - \`"error"\`: confident this will cause real failure or regression
33230
33449
  - \`"warning"\`: fragile or incomplete but may ship without immediate breakage
@@ -33769,8 +33988,8 @@ function stripMarkdownInline(s) {
33769
33988
  function extractLocusKeywords(finding) {
33770
33989
  const keywords = [];
33771
33990
  if (finding.file) {
33772
- const basename5 = finding.file.split("/").pop() ?? "";
33773
- const stem = basename5.replace(/\.[^.]+$/, "");
33991
+ const basename6 = finding.file.split("/").pop() ?? "";
33992
+ const stem = basename6.replace(/\.[^.]+$/, "");
33774
33993
  for (const part of stem.split(/[-_]/)) {
33775
33994
  if (part.length >= 3)
33776
33995
  keywords.push(part.toLowerCase());
@@ -35919,6 +36138,35 @@ var init_acceptance_fix = __esm(() => {
35919
36138
  };
35920
36139
  });
35921
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
+
35922
36170
  // src/review/ac-structural-counterfactual.ts
35923
36171
  function analyzeStructuralCounterfactual(finding, acceptanceCriteria, diffFiles) {
35924
36172
  const acIndexInRange = typeof finding.acIndex === "number" && finding.acIndex >= 1 && finding.acIndex <= acceptanceCriteria.length;
@@ -35956,7 +36204,12 @@ function validateAdversarialShape(parsed) {
35956
36204
  return null;
35957
36205
  if (!Array.isArray(obj.findings))
35958
36206
  return null;
35959
- 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
+ };
35960
36213
  }
35961
36214
  function formatFindings(findings) {
35962
36215
  return findings.map((f) => `[${f.severity}][${f.category}] ${f.file}:${f.line} \u2014 ${f.issue}
@@ -36011,7 +36264,21 @@ function validateLLMShape(parsed) {
36011
36264
  return null;
36012
36265
  if (!Array.isArray(obj.findings))
36013
36266
  return null;
36014
- 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 };
36015
36282
  }
36016
36283
  function parseLLMResponse(raw) {
36017
36284
  try {
@@ -36067,7 +36334,7 @@ function llmFindingToFinding(f, opts = {}) {
36067
36334
  return {
36068
36335
  source: "semantic-review",
36069
36336
  severity: normalizeSeverity2(f.severity),
36070
- category: "",
36337
+ category: normalizeSemanticCategory(f.category),
36071
36338
  file: f.file,
36072
36339
  line: f.line,
36073
36340
  message: f.issue,
@@ -36082,6 +36349,7 @@ function toReviewFindings(findings, opts = {}) {
36082
36349
  var UNVERIFIED_FINDING_PATTERNS;
36083
36350
  var init_semantic_helpers = __esm(() => {
36084
36351
  init_category_fix_target();
36352
+ init_semantic_categories();
36085
36353
  init_severity();
36086
36354
  UNVERIFIED_FINDING_PATTERNS = [
36087
36355
  "cannot verify",
@@ -36448,6 +36716,7 @@ async function performSemanticReground(turn, firstParsed, drops, ctx) {
36448
36716
  output: JSON.stringify({
36449
36717
  passed: false,
36450
36718
  findings: secondParsed.findings,
36719
+ ...secondParsed.acks && { acks: secondParsed.acks },
36451
36720
  _repromptInfo: { dropCount, outcome: "recovered-blocking", costUsd }
36452
36721
  }),
36453
36722
  estimatedCostUsd: costUsd
@@ -36460,6 +36729,7 @@ async function performSemanticReground(turn, firstParsed, drops, ctx) {
36460
36729
  output: JSON.stringify({
36461
36730
  passed: true,
36462
36731
  findings: [...firstAdvisory, ...secondAdvisory],
36732
+ ...secondParsed.acks && { acks: secondParsed.acks },
36463
36733
  _repromptInfo: { dropCount, outcome: "recovered-advisory-only", costUsd }
36464
36734
  }),
36465
36735
  estimatedCostUsd: costUsd
@@ -36558,7 +36828,7 @@ var FAIL_OPEN, SEMANTIC_REQUOTE_RECOVERED_EVENT = "review.semantic.finding.requo
36558
36828
  const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
36559
36829
  return {
36560
36830
  ...turn,
36561
- output: JSON.stringify({ passed, findings: requoted.findings }),
36831
+ output: JSON.stringify({ passed, findings: requoted.findings, ...parsed.acks && { acks: parsed.acks } }),
36562
36832
  estimatedCostUsd: (turn.estimatedCostUsd ?? 0) + requoted.extraCostUsd
36563
36833
  };
36564
36834
  }
@@ -36635,7 +36905,8 @@ var init_semantic_review = __esm(() => {
36635
36905
  findings: parsed.findings,
36636
36906
  normalizedFindings: [],
36637
36907
  acDropped: [],
36638
- repromptEvent
36908
+ repromptEvent,
36909
+ ...parsed.acks && { acks: parsed.acks }
36639
36910
  };
36640
36911
  }
36641
36912
  const unparsedPreview = previewOutput(output, UNPARSED_PREVIEW_BYTES);
@@ -36686,7 +36957,7 @@ var init_semantic_review = __esm(() => {
36686
36957
  };
36687
36958
  });
36688
36959
 
36689
- // src/operations/adversarial-review.ts
36960
+ // src/operations/adversarial-reprompt-marker.ts
36690
36961
  function withRepromptMarker2(output, info) {
36691
36962
  const parsed = tryParseLLMJson(output);
36692
36963
  if (!parsed || typeof parsed !== "object")
@@ -36709,6 +36980,9 @@ function extractRepromptInfo2(raw) {
36709
36980
  outcome: i.outcome
36710
36981
  };
36711
36982
  }
36983
+ var init_adversarial_reprompt_marker = () => {};
36984
+
36985
+ // src/operations/adversarial-review.ts
36712
36986
  async function requoteBlockingAdversarialFindings(findings, ctx) {
36713
36987
  const threshold = ctx.input.blockingThreshold ?? "error";
36714
36988
  const maxRequotes = ctx.input.adversarialConfig.substantiation?.maxRequotes ?? DEFAULT_MAX_REQUOTES2;
@@ -36823,6 +37097,7 @@ async function performAdversarialReground(turn, firstParsed, drops, ctx) {
36823
37097
  output: JSON.stringify({
36824
37098
  passed: false,
36825
37099
  findings: secondParsed.findings,
37100
+ ...secondParsed.acks && { acks: secondParsed.acks },
36826
37101
  _repromptInfo: { dropCount, outcome: "recovered-blocking", costUsd }
36827
37102
  }),
36828
37103
  estimatedCostUsd: costUsd
@@ -36835,6 +37110,7 @@ async function performAdversarialReground(turn, firstParsed, drops, ctx) {
36835
37110
  output: JSON.stringify({
36836
37111
  passed: true,
36837
37112
  findings: [...firstAdvisory, ...secondAdvisory],
37113
+ ...secondParsed.acks && { acks: secondParsed.acks },
36838
37114
  _repromptInfo: { dropCount, outcome: "recovered-advisory-only", costUsd }
36839
37115
  }),
36840
37116
  estimatedCostUsd: costUsd
@@ -36884,6 +37160,7 @@ var init_adversarial_review = __esm(() => {
36884
37160
  init_recurrence_demotion();
36885
37161
  init_requote_response();
36886
37162
  init__review_fallback();
37163
+ init_adversarial_reprompt_marker();
36887
37164
  FAIL_OPEN2 = {
36888
37165
  passed: true,
36889
37166
  findings: [],
@@ -36928,7 +37205,7 @@ var init_adversarial_review = __esm(() => {
36928
37205
  const passed = !requoted.findings.some((finding) => isBlockingSeverity(finding.severity, ctx.input.blockingThreshold ?? "error"));
36929
37206
  return {
36930
37207
  ...turn,
36931
- output: JSON.stringify({ passed, findings: requoted.findings }),
37208
+ output: JSON.stringify({ passed, findings: requoted.findings, ...parsed.acks && { acks: parsed.acks } }),
36932
37209
  estimatedCostUsd: (turn.estimatedCostUsd ?? 0) + requoted.extraCostUsd
36933
37210
  };
36934
37211
  }
@@ -36963,7 +37240,8 @@ var init_adversarial_review = __esm(() => {
36963
37240
  findings: parsed.findings,
36964
37241
  normalizedFindings: [],
36965
37242
  acDropped: [],
36966
- repromptEvent
37243
+ repromptEvent,
37244
+ ...parsed.acks && { acks: parsed.acks }
36967
37245
  };
36968
37246
  }
36969
37247
  if (/"passed"\s*:\s*false/.test(output) && !/"findings"\s*:\s*\[\s*\{/.test(output)) {
@@ -42299,12 +42577,50 @@ function recordAdversarialAudit(opts) {
42299
42577
  blockingThreshold: opts.blockingThreshold,
42300
42578
  result: opts.result,
42301
42579
  advisoryFindings: opts.advisoryFindings,
42580
+ acks: opts.acks,
42302
42581
  diffAvailable: opts.diffAvailable,
42303
42582
  adversarialDropAnalysis: opts.adversarialDropAnalysis,
42304
42583
  adversarialAcceptAnalysis: opts.adversarialAcceptAnalysis
42305
42584
  });
42306
42585
  }
42307
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
+
42308
42624
  // src/review/diff-utils.ts
42309
42625
  var {spawn: spawn3 } = globalThis.Bun;
42310
42626
  async function resolveNaxIgnorePathspecExcludes(workdir, options) {
@@ -42703,7 +43019,7 @@ var package_default;
42703
43019
  var init_package = __esm(() => {
42704
43020
  package_default = {
42705
43021
  name: "@nathapp/nax",
42706
- version: "0.75.6",
43022
+ version: "0.76.0",
42707
43023
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
42708
43024
  type: "module",
42709
43025
  bin: {
@@ -42807,8 +43123,8 @@ var init_version = __esm(() => {
42807
43123
  NAX_VERSION = package_default.version;
42808
43124
  NAX_COMMIT = (() => {
42809
43125
  try {
42810
- if (/^[0-9a-f]{6,10}$/.test("064f9083"))
42811
- return "064f9083";
43126
+ if (/^[0-9a-f]{6,10}$/.test("e7721ea5"))
43127
+ return "e7721ea5";
42812
43128
  } catch {}
42813
43129
  try {
42814
43130
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -42855,6 +43171,7 @@ function toPersistedEntry(entry, epochMs) {
42855
43171
  blockingThreshold: entry.blockingThreshold ?? "error",
42856
43172
  result: entry.result,
42857
43173
  advisoryFindings: entry.advisoryFindings ?? null,
43174
+ acks: entry.acks ?? null,
42858
43175
  acDropped: entry.acDropped ?? null,
42859
43176
  ...entry.parsed ? {} : { unparsedPreview: entry.unparsedPreview ?? null },
42860
43177
  diffAvailable: entry.diffAvailable ?? null,
@@ -43225,6 +43542,7 @@ async function runAdversarialReview(opts) {
43225
43542
  ...tagCoverageGap(toAdversarialReviewFindings(demoted, { isTestFile: testFileMatch }))
43226
43543
  ];
43227
43544
  const acDropped = opResult.acDropped ?? [];
43545
+ const acks = opResult.acks;
43228
43546
  let diffFiles;
43229
43547
  let diffAvailable;
43230
43548
  if (diff && diff.length > 0) {
@@ -43242,30 +43560,12 @@ async function runAdversarialReview(opts) {
43242
43560
  diffAvailable = true;
43243
43561
  }
43244
43562
  }
43245
- const adversarialDropAnalysis = acDropped.map((d) => ({
43246
- finding: {
43247
- file: d.finding.file ?? "<unknown>",
43248
- line: d.finding.line ?? 0,
43249
- severity: d.finding.severity,
43250
- category: d.finding.category ?? "<unknown>",
43251
- issue: d.finding.issue
43252
- },
43253
- dropCode: d.code,
43254
- acIndex: d.finding.acIndex,
43255
- rawCategory: d.finding.category ?? "",
43256
- counterfactual: analyzeStructuralCounterfactual({ acIndex: d.finding.acIndex, category: d.finding.category, file: d.finding.file }, story.acceptanceCriteria, diffFiles)
43257
- }));
43258
- const adversarialAcceptAnalysis = blockingFindings.map((f) => ({
43259
- finding: {
43260
- file: f.file,
43261
- line: f.line,
43262
- severity: f.severity,
43263
- category: f.category
43264
- },
43265
- acIndex: f.acIndex,
43266
- rawCategory: f.category,
43267
- counterfactual: analyzeStructuralCounterfactual({ acIndex: f.acIndex, category: f.category, file: f.file }, story.acceptanceCriteria, diffFiles)
43268
- }));
43563
+ const { adversarialDropAnalysis, adversarialAcceptAnalysis } = buildCounterfactualTelemetry({
43564
+ acDropped,
43565
+ blockingFindings,
43566
+ acceptanceCriteria: story.acceptanceCriteria,
43567
+ diffFiles
43568
+ });
43269
43569
  if (advisoryFindings.length > 0) {
43270
43570
  logger?.debug("review", `Adversarial review: ${advisoryFindings.length} advisory findings (below threshold '${threshold}')`, {
43271
43571
  storyId: story.id,
@@ -43307,7 +43607,8 @@ async function runAdversarialReview(opts) {
43307
43607
  advisoryFindings: advisoryFindings.length > 0 ? advisoryReviewFindings : undefined,
43308
43608
  diffAvailable,
43309
43609
  adversarialDropAnalysis,
43310
- adversarialAcceptAnalysis
43610
+ adversarialAcceptAnalysis,
43611
+ acks
43311
43612
  });
43312
43613
  const output = blockingFindings.length > 0 ? `Adversarial review failed:
43313
43614
 
@@ -43341,6 +43642,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
43341
43642
  storyId: story.id,
43342
43643
  featureName,
43343
43644
  parsed: true,
43645
+ acks,
43344
43646
  failOpen: false,
43345
43647
  passed: true,
43346
43648
  passReason: "ac_quote_not_substring_demoted",
@@ -43378,6 +43680,7 @@ ${formatFindings(blockingFindings)}` : "Adversarial review failed (no findings)"
43378
43680
  storyId: story.id,
43379
43681
  featureName,
43380
43682
  parsed: true,
43683
+ acks,
43381
43684
  failOpen: false,
43382
43685
  passed: false,
43383
43686
  blockingThreshold: threshold,
@@ -43408,6 +43711,7 @@ ${dropSummary}`,
43408
43711
  storyId: story.id,
43409
43712
  featureName,
43410
43713
  parsed: true,
43714
+ acks,
43411
43715
  failOpen: false,
43412
43716
  passed: true,
43413
43717
  blockingThreshold: threshold,
@@ -43438,7 +43742,7 @@ var init_adversarial = __esm(() => {
43438
43742
  init_logger2();
43439
43743
  init_adversarial_review();
43440
43744
  init_call();
43441
- init_ac_structural_counterfactual();
43745
+ init_adversarial_counterfactual_telemetry();
43442
43746
  init_adversarial_helpers();
43443
43747
  init_diff_utils();
43444
43748
  init_finding_projection();
@@ -43898,7 +44202,8 @@ function recordSemanticDebateAudit(opts) {
43898
44202
  passed: opts.passed,
43899
44203
  blockingThreshold: opts.blockingThreshold,
43900
44204
  result: opts.result,
43901
- advisoryFindings: opts.advisoryFindings
44205
+ advisoryFindings: opts.advisoryFindings,
44206
+ acks: opts.acks
43902
44207
  });
43903
44208
  }
43904
44209
  async function runSemanticDebate(opts) {
@@ -43952,12 +44257,16 @@ async function runSemanticDebate(opts) {
43952
44257
  const debateCost = debateResult.totalCostUsd ?? 0;
43953
44258
  const resolverPassed = debateResult.outcome === "passed";
43954
44259
  const allFindings = [];
44260
+ const acks = [];
43955
44261
  for (const p of debateResult.proposals) {
43956
44262
  const parsed = parseLLMResponse(p.output);
43957
44263
  if (parsed) {
43958
44264
  allFindings.push(...parsed.findings);
44265
+ if (parsed.acks)
44266
+ acks.push(...parsed.acks.slice(0, MAX_ACKS - acks.length));
43959
44267
  }
43960
44268
  }
44269
+ const debateAcks = acks.length > 0 ? acks : undefined;
43961
44270
  const seen = new Set;
43962
44271
  const deduped = [];
43963
44272
  for (const f of allFindings) {
@@ -43991,6 +44300,7 @@ async function runSemanticDebate(opts) {
43991
44300
  storyId: story.id,
43992
44301
  featureName,
43993
44302
  parsed: true,
44303
+ acks: debateAcks,
43994
44304
  passed: false,
43995
44305
  blockingThreshold: debateThreshold,
43996
44306
  result: {
@@ -44023,6 +44333,7 @@ ${formatFindings2(debateBlocking)}`,
44023
44333
  storyId: story.id,
44024
44334
  featureName,
44025
44335
  parsed: true,
44336
+ acks: debateAcks,
44026
44337
  passed: true,
44027
44338
  blockingThreshold: debateThreshold,
44028
44339
  result: {
@@ -44049,6 +44360,7 @@ ${formatFindings2(debateBlocking)}`,
44049
44360
  storyId: story.id,
44050
44361
  featureName,
44051
44362
  parsed: true,
44363
+ acks: debateAcks,
44052
44364
  passed: true,
44053
44365
  blockingThreshold: debateThreshold,
44054
44366
  result: {
@@ -44092,7 +44404,8 @@ function recordSemanticAudit(opts) {
44092
44404
  passed: opts.passed,
44093
44405
  blockingThreshold: opts.blockingThreshold,
44094
44406
  result: opts.result,
44095
- advisoryFindings: opts.advisoryFindings
44407
+ advisoryFindings: opts.advisoryFindings,
44408
+ acks: opts.acks
44096
44409
  });
44097
44410
  }
44098
44411
  async function runSemanticReview(opts) {
@@ -44370,6 +44683,7 @@ async function runSemanticReview(opts) {
44370
44683
  }
44371
44684
  const threshold = blockingThreshold ?? "error";
44372
44685
  const allFindings = opResult.findings;
44686
+ const acks = opResult.acks;
44373
44687
  const blockingFindings = allFindings.filter((f) => isBlockingSeverity(f.severity, threshold));
44374
44688
  const advisoryFindings = allFindings.filter((f) => !isBlockingSeverity(f.severity, threshold));
44375
44689
  if (advisoryFindings.length > 0) {
@@ -44407,6 +44721,7 @@ ${formatFindings2(blockingFindings)}`;
44407
44721
  failOpen: false,
44408
44722
  passed: false,
44409
44723
  blockingThreshold: threshold,
44724
+ acks,
44410
44725
  result: {
44411
44726
  passed: false,
44412
44727
  findings: llmFindingsToReviewFindings(allFindings, { source: "semantic-review", isTestFile: testFileMatch })
@@ -44437,6 +44752,7 @@ ${formatFindings2(blockingFindings)}`;
44437
44752
  storyId: story.id,
44438
44753
  featureName,
44439
44754
  parsed: true,
44755
+ acks,
44440
44756
  failOpen: false,
44441
44757
  passed: false,
44442
44758
  blockingThreshold: threshold,
@@ -44462,6 +44778,7 @@ ${formatFindings2(blockingFindings)}`;
44462
44778
  storyId: story.id,
44463
44779
  featureName,
44464
44780
  parsed: true,
44781
+ acks,
44465
44782
  failOpen: false,
44466
44783
  passed: true,
44467
44784
  blockingThreshold: threshold,
@@ -44840,6 +45157,7 @@ var init_review = __esm(() => {
44840
45157
  init_runner2();
44841
45158
  init_requote_response();
44842
45159
  init_severity();
45160
+ init_semantic_categories();
44843
45161
  });
44844
45162
 
44845
45163
  // src/prompts/builders/rectifier-builder-helpers.ts
@@ -46857,6 +47175,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46857
47175
  workdir,
46858
47176
  pipelineStage: stage,
46859
47177
  modelDef,
47178
+ ...resolvedRunOptions.modelDef !== undefined ? {} : { modelTier: effectiveTier },
46860
47179
  timeoutSeconds: resolvedRunOptions.timeoutSeconds ?? config2.execution?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds,
46861
47180
  featureName,
46862
47181
  storyId: story.id,
@@ -46864,6 +47183,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46864
47183
  });
46865
47184
  }
46866
47185
  } else {
47186
+ const pinned = hopKind.kind === "primary" && resolvedRunOptions.modelDef !== undefined;
46867
47187
  const modelDef = hopKind.kind === "primary" ? resolvedRunOptions.modelDef ?? resolveModelForAgent(config2.models, agentName, effectiveTier, defaultAgent) : resolveModelForAgent(config2.models, agentName, effectiveTier, defaultAgent);
46868
47188
  handle = await sessionManager.openSession(sessionName, {
46869
47189
  agentName,
@@ -46871,6 +47191,7 @@ function buildHopCallback(ctx, sessionId, _initialOptions) {
46871
47191
  workdir,
46872
47192
  pipelineStage: stage,
46873
47193
  modelDef,
47194
+ ...pinned ? {} : { modelTier: effectiveTier },
46874
47195
  timeoutSeconds: resolvedRunOptions.timeoutSeconds ?? config2.execution?.sessionTimeoutSeconds ?? DEFAULT_CONFIG.execution.sessionTimeoutSeconds,
46875
47196
  featureName,
46876
47197
  storyId: story.id,
@@ -47033,6 +47354,7 @@ async function callOp(ctx, op, input) {
47033
47354
  const sessionName = sessionRole2 && ctx.packageDir ? computeAcpHandle(ctx.packageDir, ctx.featureName, ctx.storyId, sessionRole2) : undefined;
47034
47355
  const completeOptions = {
47035
47356
  modelDef: resolved.modelDef,
47357
+ ...resolved.modelTier !== undefined ? { modelTier: resolved.modelTier } : {},
47036
47358
  jsonMode: completeOp.jsonMode ?? false,
47037
47359
  pipelineStage: op.stage,
47038
47360
  storyId: ctx.storyId,
@@ -48088,7 +48410,7 @@ var init_agent_stream_logging = __esm(() => {
48088
48410
  });
48089
48411
 
48090
48412
  // src/runtime/middleware/cost.ts
48091
- function attachCostSubscriber(bus, aggregator, runId) {
48413
+ function attachCostSubscriber(bus, aggregator, runId, projectKey) {
48092
48414
  const offDispatch = bus.onDispatch((event) => {
48093
48415
  const tu = event.tokenUsage;
48094
48416
  const wireExactCostUsd = event.exactCostUsd;
@@ -48101,9 +48423,15 @@ function attachCostSubscriber(bus, aggregator, runId) {
48101
48423
  const costEvent = {
48102
48424
  ts: event.timestamp,
48103
48425
  runId,
48426
+ ...projectKey !== undefined ? { projectKey } : {},
48427
+ schemaVersion: COST_ROW_SCHEMA_VERSION,
48104
48428
  agentName: event.agentName,
48105
- model: "unknown",
48429
+ model: event.model ?? "unknown",
48430
+ ...event.modelTier !== undefined ? { modelTier: event.modelTier } : {},
48431
+ ...event.profile !== undefined ? { profile: event.profile } : {},
48106
48432
  stage: event.stage,
48433
+ sessionRole: event.sessionRole,
48434
+ ...event.featureName !== undefined ? { featureName: event.featureName } : {},
48107
48435
  storyId: event.storyId,
48108
48436
  callId: event.callId,
48109
48437
  scopeId: event.scopeId,
@@ -48117,14 +48445,18 @@ function attachCostSubscriber(bus, aggregator, runId) {
48117
48445
  exactCostUsd,
48118
48446
  costUsd: exactCostUsd,
48119
48447
  confidence,
48448
+ pricingSource: hasWireExactCost ? "wire" : resolvePricingSource(event.model),
48120
48449
  durationMs: event.durationMs
48121
48450
  };
48122
48451
  aggregator.record(costEvent);
48123
48452
  });
48124
48453
  const offError = bus.onDispatchError((event) => {
48125
48454
  const errorEvent = {
48455
+ kind: "error",
48126
48456
  ts: event.timestamp,
48127
48457
  runId,
48458
+ ...projectKey !== undefined ? { projectKey } : {},
48459
+ schemaVersion: COST_ROW_SCHEMA_VERSION,
48128
48460
  agentName: event.agentName,
48129
48461
  stage: event.stage,
48130
48462
  storyId: event.storyId,
@@ -48153,6 +48485,10 @@ function attachCostSubscriber(bus, aggregator, runId) {
48153
48485
  offCompleted();
48154
48486
  };
48155
48487
  }
48488
+ var COST_ROW_SCHEMA_VERSION = 2;
48489
+ var init_cost2 = __esm(() => {
48490
+ init_agents();
48491
+ });
48156
48492
 
48157
48493
  // src/runtime/middleware/audit.ts
48158
48494
  function attachAuditSubscriber(bus, auditor, runId) {
@@ -48251,6 +48587,7 @@ function attachReviewAuditSubscriber(bus, auditor, runId) {
48251
48587
  blockingThreshold: event.blockingThreshold,
48252
48588
  result: event.result,
48253
48589
  advisoryFindings: event.advisoryFindings,
48590
+ acks: event.acks ? [...event.acks] : undefined,
48254
48591
  acDropped: event.acDropped ? [...event.acDropped] : undefined,
48255
48592
  unparsedPreview: event.unparsedPreview,
48256
48593
  diffAvailable: event.diffAvailable,
@@ -48268,7 +48605,7 @@ function attachReviewAuditSubscriber(bus, auditor, runId) {
48268
48605
  function scheduleTickIfNeeded(tickRef, tick, intervalMs) {
48269
48606
  if (tickRef.handle !== null)
48270
48607
  return;
48271
- tickRef.handle = setTimeout(tick, intervalMs);
48608
+ tickRef.handle = _idleWatchdogDeps.setTimeout(tick, intervalMs);
48272
48609
  }
48273
48610
  function handleObserveTimeout(state, reason, idleDurationMs, nonToolCallIdleMs) {
48274
48611
  if (state.warnedForCurrentIdlePeriod)
@@ -48296,7 +48633,7 @@ async function handleCancelTimeout(state, reason, controllerRegistry, maxRetryAt
48296
48633
  return;
48297
48634
  }
48298
48635
  state.cancelAttempts++;
48299
- state.lastActivityAt = Date.now();
48636
+ state.lastActivityAt = _idleWatchdogDeps.now();
48300
48637
  getSafeLogger()?.warn("idle-watchdog", reason === "tool_call_only_idle_timeout_exceeded" ? "Canceling tool-call-only idle call" : "Canceling idle call", {
48301
48638
  storyId: state.storyId,
48302
48639
  key: reason,
@@ -48330,17 +48667,17 @@ function handleWarnThenCancelTimeout(state, reason, controllerRegistry, maxRetry
48330
48667
  });
48331
48668
  state.inGracePeriod = true;
48332
48669
  state.graceReason = reason;
48333
- state.graceTimer = setTimeout(async () => {
48670
+ state.graceTimer = _idleWatchdogDeps.setTimeout(async () => {
48334
48671
  if (!activeStates.has(state.callId))
48335
48672
  return;
48336
48673
  state.inGracePeriod = false;
48337
48674
  state.graceTimer = undefined;
48338
48675
  state.graceReason = undefined;
48339
- const currentReason = getTimeoutReason(state, Date.now(), idleTimeoutMs, toolCallOnlyTimeoutMs);
48676
+ const currentReason = getTimeoutReason(state, _idleWatchdogDeps.now(), idleTimeoutMs, toolCallOnlyTimeoutMs);
48340
48677
  if (currentReason !== reason)
48341
48678
  return;
48342
48679
  state.cancelAttempts++;
48343
- state.lastActivityAt = Date.now();
48680
+ state.lastActivityAt = _idleWatchdogDeps.now();
48344
48681
  const cancel = controllerRegistry.get(state.callId);
48345
48682
  if (cancel)
48346
48683
  await cancel().catch(() => {});
@@ -48348,7 +48685,7 @@ function handleWarnThenCancelTimeout(state, reason, controllerRegistry, maxRetry
48348
48685
  }
48349
48686
  function clearGrace(state) {
48350
48687
  if (state.inGracePeriod && state.graceTimer !== undefined) {
48351
- clearTimeout(state.graceTimer);
48688
+ _idleWatchdogDeps.clearTimeout(state.graceTimer);
48352
48689
  state.graceTimer = undefined;
48353
48690
  state.inGracePeriod = false;
48354
48691
  state.graceReason = undefined;
@@ -48384,7 +48721,7 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48384
48721
  const tickRef = { handle: null };
48385
48722
  function tick() {
48386
48723
  tickRef.handle = null;
48387
- const now = Date.now();
48724
+ const now = _idleWatchdogDeps.now();
48388
48725
  for (const [, state] of activeStates) {
48389
48726
  if (state.inGracePeriod)
48390
48727
  continue;
@@ -48407,7 +48744,7 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48407
48744
  const unsubscribe = agentStreamEvents.onAgentStream((event) => {
48408
48745
  switch (event.kind) {
48409
48746
  case "agent.call_started": {
48410
- const now = Date.now();
48747
+ const now = _idleWatchdogDeps.now();
48411
48748
  activeStates.set(event.callId, {
48412
48749
  callId: event.callId,
48413
48750
  agentName: event.agentName,
@@ -48479,11 +48816,11 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48479
48816
  const state = activeStates.get(event.callId);
48480
48817
  if (state) {
48481
48818
  if (state.graceTimer !== undefined)
48482
- clearTimeout(state.graceTimer);
48819
+ _idleWatchdogDeps.clearTimeout(state.graceTimer);
48483
48820
  activeStates.delete(event.callId);
48484
48821
  }
48485
48822
  if (activeStates.size === 0 && tickRef.handle !== null) {
48486
- clearTimeout(tickRef.handle);
48823
+ _idleWatchdogDeps.clearTimeout(tickRef.handle);
48487
48824
  tickRef.handle = null;
48488
48825
  }
48489
48826
  break;
@@ -48494,17 +48831,23 @@ function attachAgentIdleWatchdog(agentStreamEvents, controllerRegistry, config2)
48494
48831
  unsubscribe();
48495
48832
  for (const state of activeStates.values()) {
48496
48833
  if (state.graceTimer !== undefined)
48497
- clearTimeout(state.graceTimer);
48834
+ _idleWatchdogDeps.clearTimeout(state.graceTimer);
48498
48835
  }
48499
48836
  activeStates.clear();
48500
48837
  if (tickRef.handle !== null) {
48501
- clearTimeout(tickRef.handle);
48838
+ _idleWatchdogDeps.clearTimeout(tickRef.handle);
48502
48839
  tickRef.handle = null;
48503
48840
  }
48504
48841
  };
48505
48842
  }
48843
+ var _idleWatchdogDeps;
48506
48844
  var init_idle_watchdog = __esm(() => {
48507
48845
  init_logger2();
48846
+ _idleWatchdogDeps = {
48847
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
48848
+ clearTimeout: (id) => clearTimeout(id),
48849
+ now: () => Date.now()
48850
+ };
48508
48851
  });
48509
48852
 
48510
48853
  // src/runtime/middleware/index.ts
@@ -48512,6 +48855,7 @@ var init_middleware = __esm(() => {
48512
48855
  init_cancellation();
48513
48856
  init_logging();
48514
48857
  init_agent_stream_logging();
48858
+ init_cost2();
48515
48859
  init_idle_watchdog();
48516
48860
  });
48517
48861
 
@@ -49069,6 +49413,11 @@ var init_manager_sweep = __esm(() => {
49069
49413
  DEFAULT_ORPHAN_TTL_MS = 4 * 60 * 60 * 1000;
49070
49414
  });
49071
49415
 
49416
+ // src/session/model-selection.ts
49417
+ function selectModel(opts) {
49418
+ return { modelDef: opts.modelDef, ...opts.modelTier ? { modelTier: opts.modelTier } : {} };
49419
+ }
49420
+
49072
49421
  // src/session/naming.ts
49073
49422
  var exports_naming = {};
49074
49423
  __export(exports_naming, {
@@ -49374,7 +49723,7 @@ class SessionManager {
49374
49723
  agentName: opts.agentName,
49375
49724
  workdir: opts.workdir,
49376
49725
  resolvedPermissions,
49377
- modelDef: opts.modelDef,
49726
+ ...selectModel(opts),
49378
49727
  timeoutSeconds: opts.timeoutSeconds,
49379
49728
  onPidSpawned: this._pidRegistry ? (pid) => this._pidRegistry?.register(pid) : undefined,
49380
49729
  onPidExited: this._pidRegistry ? (pid) => this._pidRegistry?.unregister(pid) : undefined,
@@ -49649,8 +49998,8 @@ async function triageFlakyFindings(input) {
49649
49998
  result.push({ ...f });
49650
49999
  return { findings: result, quarantineReport: { keys, reasons } };
49651
50000
  }
49652
- const changedTestSet = new Set(diff.changedTestFiles.map(basename5));
49653
- 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));
49654
50003
  const candidates = findings.filter((f) => isProbeCandidate(f, changedTestSet, mappedTestSet));
49655
50004
  if (candidates.length > flakeDetection.maxProbesPerGate) {
49656
50005
  logger?.info("flake-triage", `Skipping flake triage \u2014 ${candidates.length} candidates exceed maxProbesPerGate=${flakeDetection.maxProbesPerGate}`);
@@ -49714,7 +50063,7 @@ async function triageFlakyFindings(input) {
49714
50063
  }
49715
50064
  return { findings: result, quarantineReport: { keys, reasons } };
49716
50065
  }
49717
- function basename5(path7) {
50066
+ function basename6(path7) {
49718
50067
  const i = path7.lastIndexOf("/");
49719
50068
  return i === -1 ? path7 : path7.slice(i + 1);
49720
50069
  }
@@ -49725,7 +50074,7 @@ function isProbeCandidate(finding, changedTestSet, mappedTestSet) {
49725
50074
  return false;
49726
50075
  if (!finding.rule)
49727
50076
  return false;
49728
- const base = basename5(finding.file);
50077
+ const base = basename6(finding.file);
49729
50078
  if (changedTestSet.has(base) || changedTestSet.has(finding.file))
49730
50079
  return false;
49731
50080
  if (mappedTestSet.has(base) || mappedTestSet.has(finding.file))
@@ -49859,6 +50208,7 @@ __export(exports_runtime, {
49859
50208
  attachAgentIdleWatchdog: () => attachAgentIdleWatchdog,
49860
50209
  _reviewAuditDeps: () => _reviewAuditDeps,
49861
50210
  _promptAuditorDeps: () => _promptAuditorDeps,
50211
+ _idleWatchdogDeps: () => _idleWatchdogDeps,
49862
50212
  _costAggDeps: () => _costAggDeps,
49863
50213
  ReviewAuditor: () => ReviewAuditor,
49864
50214
  PromptAuditor: () => PromptAuditor,
@@ -49868,7 +50218,7 @@ __export(exports_runtime, {
49868
50218
  CostAggregator: () => CostAggregator,
49869
50219
  AgentStreamEventBus: () => AgentStreamEventBus
49870
50220
  });
49871
- import { basename as basename6, join as join31 } from "path";
50221
+ import { basename as basename7, join as join31 } from "path";
49872
50222
  function createRuntime(config2, workdir, opts) {
49873
50223
  const runId = crypto.randomUUID();
49874
50224
  const controller = new AbortController;
@@ -49880,7 +50230,7 @@ function createRuntime(config2, workdir, opts) {
49880
50230
  const configLoader = createConfigLoader(config2);
49881
50231
  const dispatchEvents = new DispatchEventBus;
49882
50232
  const agentStreamEvents = opts?.agentStreamEvents ?? new AgentStreamEventBus;
49883
- const projectKey = config2.name?.trim() || basename6(workdir);
50233
+ const projectKey = config2.name?.trim() || basename7(workdir);
49884
50234
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
49885
50235
  const globalDir = globalOutputDir();
49886
50236
  const curatorRollupPathValue = curatorRollupPath(globalDir, config2.curator?.rollupPath);
@@ -49935,7 +50285,7 @@ function createRuntime(config2, workdir, opts) {
49935
50285
  agentManager.configureRuntime({ pidRegistry });
49936
50286
  }
49937
50287
  const offLogging = attachLoggingSubscriber(dispatchEvents, runId);
49938
- const offCost = attachCostSubscriber(dispatchEvents, costAggregator, runId);
50288
+ const offCost = attachCostSubscriber(dispatchEvents, costAggregator, runId, getProjectKey(config2, workdir));
49939
50289
  const offAudit = attachAuditSubscriber(dispatchEvents, promptAuditor, runId);
49940
50290
  const offReviewAudit = attachReviewAuditSubscriber(dispatchEvents, reviewAuditor, runId);
49941
50291
  const offAgentStreamLogging = attachAgentStreamLogging(agentStreamEvents, runId);
@@ -52864,7 +53214,8 @@ var init_telegram = __esm(() => {
52864
53214
  init_zod();
52865
53215
  init_logger2();
52866
53216
  _telegramPluginDeps = {
52867
- fetch: globalThis.fetch.bind(globalThis)
53217
+ fetch: globalThis.fetch.bind(globalThis),
53218
+ basePollBackoffMs: 1000
52868
53219
  };
52869
53220
  NUMERIC_CHAT_ID = /^-?\d+$/;
52870
53221
  TelegramConfigSchema = exports_external.object({
@@ -52878,7 +53229,7 @@ var init_telegram = __esm(() => {
52878
53229
  chatId = null;
52879
53230
  pendingMessages = new Map;
52880
53231
  lastUpdateId = 0;
52881
- backoffMs = 1000;
53232
+ backoffMs = _telegramPluginDeps.basePollBackoffMs;
52882
53233
  maxBackoffMs = 30000;
52883
53234
  static MAX_DRAIN_PAGES = 10;
52884
53235
  static INTERACTIVE_REQUEST_TYPES = new Set([
@@ -52988,7 +53339,7 @@ ${partLabel}${chunks[i]}`;
52988
53339
  this.clearInlineKeyboard(update.callback_query.message.message_id);
52989
53340
  }
52990
53341
  this.pendingMessages.delete(requestId);
52991
- this.backoffMs = 1000;
53342
+ this.backoffMs = _telegramPluginDeps.basePollBackoffMs;
52992
53343
  return response;
52993
53344
  }
52994
53345
  if (update.callback_query) {
@@ -53059,7 +53410,7 @@ ${partLabel}${chunks[i]}`;
53059
53410
  rejected: raw.length - updates.length
53060
53411
  });
53061
53412
  }
53062
- this.backoffMs = 1000;
53413
+ this.backoffMs = _telegramPluginDeps.basePollBackoffMs;
53063
53414
  return { ok: true, updates, rawCount: raw.length };
53064
53415
  } catch (err) {
53065
53416
  this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
@@ -55871,10 +56222,10 @@ __export(exports_status_cost, {
55871
56222
  displayCostMetrics: () => displayCostMetrics,
55872
56223
  _costReportEmitDeps: () => _costReportEmitDeps
55873
56224
  });
55874
- import { basename as basename7 } from "path";
56225
+ import { basename as basename8 } from "path";
55875
56226
  async function resolveProject(workdir) {
55876
56227
  const config2 = await loadConfig(workdir).catch(() => null);
55877
- const project = config2?.name?.trim() || basename7(workdir);
56228
+ const project = config2?.name?.trim() || basename8(workdir);
55878
56229
  const outputDir = projectOutputDir(project, config2?.outputDir);
55879
56230
  return { project, outputDir };
55880
56231
  }
@@ -56139,7 +56490,7 @@ __export(exports_status_features, {
56139
56490
  _statusFeaturesDeps: () => _statusFeaturesDeps
56140
56491
  });
56141
56492
  import { existsSync as existsSync17, readdirSync as readdirSync3 } from "fs";
56142
- 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";
56143
56494
  function isPidAlive(pid) {
56144
56495
  try {
56145
56496
  process.kill(pid, 0);
@@ -56162,7 +56513,7 @@ async function loadStatusFile(featureDir) {
56162
56513
  }
56163
56514
  async function loadProjectStatusFile(projectDir) {
56164
56515
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
56165
- const projectKey = config2?.name?.trim() || basename8(projectDir);
56516
+ const projectKey = config2?.name?.trim() || basename9(projectDir);
56166
56517
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
56167
56518
  const statusPath = join45(outputDir, "status.json");
56168
56519
  if (!existsSync17(statusPath)) {
@@ -56233,7 +56584,7 @@ async function getFeatureSummary(featureName, featureDir) {
56233
56584
  }
56234
56585
  async function displayAllFeatures(projectDir) {
56235
56586
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
56236
- const projectKey = config2?.name?.trim() || basename8(projectDir);
56587
+ const projectKey = config2?.name?.trim() || basename9(projectDir);
56237
56588
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
56238
56589
  const featuresDir = join45(outputDir, "features");
56239
56590
  if (!existsSync17(featuresDir)) {
@@ -56456,7 +56807,7 @@ async function displayFeatureStatus(options = {}) {
56456
56807
  if (options.dir) {
56457
56808
  const projectDir = resolve14(options.dir);
56458
56809
  const config2 = await _statusFeaturesDeps.loadConfig(projectDir).catch(() => null);
56459
- const projectKey = config2?.name?.trim() || basename8(projectDir);
56810
+ const projectKey = config2?.name?.trim() || basename9(projectDir);
56460
56811
  const outputDir = _statusFeaturesDeps.projectOutputDir(projectKey, config2?.outputDir);
56461
56812
  featureDir = join45(outputDir, "features", options.feature);
56462
56813
  } else {
@@ -57087,7 +57438,7 @@ var init_acceptance3 = __esm(() => {
57087
57438
  const allOutputParts = [];
57088
57439
  let anyError = false;
57089
57440
  let errorExitCode = 0;
57090
- let hardeningRetries = 0;
57441
+ let hardeningPromoted = 0;
57091
57442
  for (const { testPath, packageDir, testFramework, commandOverride } of testGroups) {
57092
57443
  const testFile = Bun.file(testPath);
57093
57444
  const exists = await testFile.exists();
@@ -57188,7 +57539,7 @@ ${stderr}`;
57188
57539
  runtime: ctx.runtime,
57189
57540
  abortSignal: ctx.abortSignal
57190
57541
  });
57191
- hardeningRetries = result.promoted.length;
57542
+ hardeningPromoted = result.promoted.length;
57192
57543
  } catch (err) {
57193
57544
  logger.debug("acceptance", "Hardening pass failed (non-blocking)", {
57194
57545
  storyId: ctx.story.id,
@@ -57201,7 +57552,8 @@ ${stderr}`;
57201
57552
  packageDir: ctx.workdir,
57202
57553
  passed: true,
57203
57554
  failedACs: [],
57204
- retries: hardeningRetries,
57555
+ retries: ctx.acceptanceRetries ?? 0,
57556
+ hardeningPromoted,
57205
57557
  durationMs
57206
57558
  });
57207
57559
  return { action: "continue" };
@@ -57217,7 +57569,8 @@ ${stderr}`;
57217
57569
  packageDir: ctx.workdir,
57218
57570
  passed: false,
57219
57571
  failedACs: allFailedACs,
57220
- retries: hardeningRetries,
57572
+ retries: ctx.acceptanceRetries ?? 0,
57573
+ hardeningPromoted,
57221
57574
  durationMs
57222
57575
  });
57223
57576
  if (anyError) {
@@ -62791,7 +63144,8 @@ var init_gitignore = __esm(() => {
62791
63144
  "**/_nax_acceptance_test.py",
62792
63145
  "**/_nax_suggested_test.py",
62793
63146
  "**/.nax/features/*/",
62794
- ".nax/prompt-audit/"
63147
+ ".nax/prompt-audit/",
63148
+ ".nax/finish-audit/"
62795
63149
  ];
62796
63150
  });
62797
63151
 
@@ -62804,7 +63158,7 @@ __export(exports_init_context, {
62804
63158
  generatePackageContextTemplate: () => generatePackageContextTemplate,
62805
63159
  generateContextTemplate: () => generateContextTemplate
62806
63160
  });
62807
- import { basename as basename10, join as join54 } from "path";
63161
+ import { basename as basename11, join as join54 } from "path";
62808
63162
  async function bunFileExists(path16) {
62809
63163
  return Bun.file(path16).exists();
62810
63164
  }
@@ -62899,7 +63253,7 @@ async function scanProject(projectRoot) {
62899
63253
  const readmeSnippet = await readReadmeSnippet(projectRoot);
62900
63254
  const entryPoints = await detectEntryPoints(projectRoot);
62901
63255
  const configFiles = await detectConfigFiles(projectRoot);
62902
- const projectName = packageManifest?.name || basename10(projectRoot);
63256
+ const projectName = packageManifest?.name || basename11(projectRoot);
62903
63257
  return {
62904
63258
  projectName,
62905
63259
  fileTree,
@@ -64322,7 +64676,8 @@ async function collectFromMetrics(context) {
64322
64676
  const success2 = boolValue(story.success, false);
64323
64677
  const storyId = stringValue(story.storyId ?? story.id, "unknown");
64324
64678
  const obs = {
64325
- schemaVersion: 1,
64679
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64680
+ projectKey: context.projectKey,
64326
64681
  runId: context.runId,
64327
64682
  featureId: stringValue(currentRun.feature, context.feature),
64328
64683
  storyId,
@@ -64342,6 +64697,24 @@ async function collectFromMetrics(context) {
64342
64697
  } catch {}
64343
64698
  return observations;
64344
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
+ }
64345
64718
  function findingRuleId(finding) {
64346
64719
  return stringValue(finding.ruleId ?? finding.rule ?? finding.checkId ?? finding.category, "unknown");
64347
64720
  }
@@ -64367,17 +64740,22 @@ async function collectFromReviewAudit(context) {
64367
64740
  const audit = asRecord3(await readJsonFile(fullPath));
64368
64741
  if (!audit)
64369
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;
64370
64748
  const result = asRecord3(audit.result);
64371
64749
  const findings = asArray(result?.findings);
64372
64750
  const storyId = stringValue(audit.storyId, "unknown");
64373
- const featureId = stringValue(audit.featureName ?? audit.featureId, context.feature);
64374
64751
  for (const rawFinding of findings) {
64375
64752
  const finding = asRecord3(rawFinding);
64376
64753
  if (!finding)
64377
64754
  continue;
64378
64755
  const ruleId = findingRuleId(finding);
64379
64756
  const obs = {
64380
- schemaVersion: 1,
64757
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64758
+ projectKey: context.projectKey,
64381
64759
  runId: context.runId,
64382
64760
  featureId,
64383
64761
  storyId,
@@ -64388,6 +64766,7 @@ async function collectFromReviewAudit(context) {
64388
64766
  ruleId,
64389
64767
  checkId: ruleId,
64390
64768
  severity: stringValue(finding.severity, "info"),
64769
+ category: optionalString(finding.category),
64391
64770
  file: stringValue(finding.file),
64392
64771
  line: numberValue(finding.line, 0),
64393
64772
  message: findingMessage(finding)
@@ -64403,6 +64782,7 @@ async function collectFromReviewAudit(context) {
64403
64782
  async function collectFromContextManifests(context) {
64404
64783
  const observations = [];
64405
64784
  const featuresDir = path18.join(context.workdir, ".nax", "features");
64785
+ let skippedManifests = 0;
64406
64786
  try {
64407
64787
  const glob = new Bun.Glob("*/stories/*/context-manifest-*.json");
64408
64788
  for await (const file3 of glob.scan({ cwd: featuresDir, absolute: false })) {
@@ -64411,15 +64791,21 @@ async function collectFromContextManifests(context) {
64411
64791
  const parts = file3.split("/");
64412
64792
  const featureId = parts[0] ?? context.feature;
64413
64793
  const storyId = parts[2] ?? "unknown";
64794
+ if (!await writtenThisRun(fullPath, context.runStartedAt)) {
64795
+ skippedManifests += 1;
64796
+ continue;
64797
+ }
64414
64798
  const manifest = asRecord3(await readJsonFile(fullPath));
64415
64799
  if (!manifest)
64416
64800
  continue;
64417
64801
  const ts = now();
64418
64802
  const chunkSummaries = asRecord3(manifest.chunkSummaries) ?? {};
64803
+ const chunkTokens = asRecord3(manifest.chunkTokens) ?? {};
64419
64804
  for (const chunkId of asArray(manifest.includedChunks)) {
64420
64805
  const id = String(chunkId);
64421
64806
  const obs = {
64422
- schemaVersion: 1,
64807
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64808
+ projectKey: context.projectKey,
64423
64809
  runId: context.runId,
64424
64810
  featureId,
64425
64811
  storyId,
@@ -64429,7 +64815,7 @@ async function collectFromContextManifests(context) {
64429
64815
  payload: {
64430
64816
  chunkId: id,
64431
64817
  label: stringValue(chunkSummaries[id], id),
64432
- tokens: 0
64818
+ tokens: numberValue(chunkTokens[id], 0)
64433
64819
  }
64434
64820
  };
64435
64821
  observations.push(obs);
@@ -64440,7 +64826,8 @@ async function collectFromContextManifests(context) {
64440
64826
  continue;
64441
64827
  const id = stringValue(excluded.id, "unknown");
64442
64828
  const obs = {
64443
- schemaVersion: 1,
64829
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64830
+ projectKey: context.projectKey,
64444
64831
  runId: context.runId,
64445
64832
  featureId,
64446
64833
  storyId,
@@ -64460,7 +64847,8 @@ async function collectFromContextManifests(context) {
64460
64847
  if (!provider || stringValue(provider.status) !== "empty")
64461
64848
  continue;
64462
64849
  const obs = {
64463
- schemaVersion: 1,
64850
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64851
+ projectKey: context.projectKey,
64464
64852
  runId: context.runId,
64465
64853
  featureId,
64466
64854
  storyId,
@@ -64491,7 +64879,8 @@ function entryFeatureId(context, entry, data) {
64491
64879
  function collectPullCall(context, entry, data) {
64492
64880
  const toolName = stringValue(data.tool ?? data.toolName, "unknown");
64493
64881
  return {
64494
- schemaVersion: 1,
64882
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64883
+ projectKey: context.projectKey,
64495
64884
  runId: context.runId,
64496
64885
  featureId: entryFeatureId(context, entry, data),
64497
64886
  storyId: entryStoryId(entry, data),
@@ -64510,7 +64899,8 @@ function collectPullCall(context, entry, data) {
64510
64899
  }
64511
64900
  function collectAcceptanceVerdict(context, entry, data) {
64512
64901
  return {
64513
- schemaVersion: 1,
64902
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64903
+ projectKey: context.projectKey,
64514
64904
  runId: context.runId,
64515
64905
  featureId: entryFeatureId(context, entry, data),
64516
64906
  storyId: entryStoryId(entry, data),
@@ -64528,7 +64918,8 @@ function collectAcceptanceVerdict(context, entry, data) {
64528
64918
  }
64529
64919
  function collectRectify(context, entry, data) {
64530
64920
  return {
64531
- schemaVersion: 1,
64921
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64922
+ projectKey: context.projectKey,
64532
64923
  runId: context.runId,
64533
64924
  featureId: entryFeatureId(context, entry, data),
64534
64925
  storyId: entryStoryId(entry, data),
@@ -64543,7 +64934,8 @@ function collectRectify(context, entry, data) {
64543
64934
  }
64544
64935
  function collectEscalation(context, entry, data) {
64545
64936
  return {
64546
- schemaVersion: 1,
64937
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64938
+ projectKey: context.projectKey,
64547
64939
  runId: context.runId,
64548
64940
  featureId: entryFeatureId(context, entry, data),
64549
64941
  storyId: entryStoryId(entry, data),
@@ -64559,7 +64951,8 @@ function collectEscalation(context, entry, data) {
64559
64951
  function collectFixCycleIteration(context, entry, data) {
64560
64952
  const outcome = optionalString(data.outcome);
64561
64953
  return {
64562
- schemaVersion: 1,
64954
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64955
+ projectKey: context.projectKey,
64563
64956
  runId: context.runId,
64564
64957
  featureId: entryFeatureId(context, entry, data),
64565
64958
  storyId: entryStoryId(entry, data),
@@ -64579,7 +64972,8 @@ function collectFixCycleIteration(context, entry, data) {
64579
64972
  }
64580
64973
  function collectFixCycleExit(context, entry, data) {
64581
64974
  return {
64582
- schemaVersion: 1,
64975
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64976
+ projectKey: context.projectKey,
64583
64977
  runId: context.runId,
64584
64978
  featureId: entryFeatureId(context, entry, data),
64585
64979
  storyId: entryStoryId(entry, data),
@@ -64594,7 +64988,8 @@ function collectFixCycleExit(context, entry, data) {
64594
64988
  }
64595
64989
  function collectFixCycleRetry(context, entry, data) {
64596
64990
  return {
64597
- schemaVersion: 1,
64991
+ schemaVersion: OBSERVATION_SCHEMA_VERSION,
64992
+ projectKey: context.projectKey,
64598
64993
  runId: context.runId,
64599
64994
  featureId: entryFeatureId(context, entry, data),
64600
64995
  storyId: entryStoryId(entry, data),
@@ -64648,6 +65043,7 @@ async function collectObservations(context) {
64648
65043
  ]);
64649
65044
  return [...metricsObs, ...auditObs, ...manifestObs, ...jsonlObs];
64650
65045
  }
65046
+ var OBSERVATION_SCHEMA_VERSION = 3;
64651
65047
  var init_collect = () => {};
64652
65048
 
64653
65049
  // src/plugins/builtin/curator/heuristics.ts
@@ -64664,6 +65060,12 @@ function mergeThresholds(thresholds) {
64664
65060
  function uniqueStoryIds(storyIds) {
64665
65061
  return [...new Set(storyIds)];
64666
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
+ }
64667
65069
  function firstLine2(message) {
64668
65070
  return message.split(`
64669
65071
  `)[0] ?? message;
@@ -64672,37 +65074,45 @@ function h1RepeatedReviewFinding(observations, threshold) {
64672
65074
  const findings = observations.filter((o) => o.kind === "review-finding");
64673
65075
  const groups = new Map;
64674
65076
  for (const obs of findings) {
64675
- const ruleId = obs.payload.ruleId;
64676
- const message = obs.payload.message;
64677
- const existing = groups.get(ruleId);
64678
- if (existing) {
64679
- existing.storyIds.push(obs.storyId);
64680
- const sampleKey = firstLine2(message);
64681
- if (existing.samples.length < 2 && sampleKey && !existing.samples.includes(sampleKey)) {
64682
- existing.samples.push(sampleKey);
64683
- }
64684
- } else {
64685
- const sampleKey = firstLine2(message);
64686
- 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;
64687
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);
64688
65094
  }
64689
65095
  const proposals = [];
64690
- for (const [ruleId, { storyIds, samples }] of groups.entries()) {
64691
- if (storyIds.length < threshold)
65096
+ for (const group of groups.values()) {
65097
+ const featureCount = group.featureIds.size;
65098
+ if (featureCount < threshold)
64692
65099
  continue;
64693
- const count = storyIds.length;
64694
- const severity2 = count >= 4 ? "HIGH" : "MED";
64695
- const unique = uniqueStoryIds(storyIds);
64696
- const sampleSection = samples.length > 0 ? `
64697
- 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(" | ")}` : "";
64698
65108
  proposals.push({
64699
65109
  id: "H1",
64700
- severity: severity2,
65110
+ severity: featureCount >= threshold * HIGH_SEVERITY_MULTIPLE ? "HIGH" : "MED",
64701
65111
  target: { canonicalFile: ".nax/rules/curator-suggestions.md", action: "add" },
64702
- description: `Repeated review finding: ${ruleId} appeared ${count}x across stories`,
64703
- 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}`,
64704
65114
  sourceKinds: ["review-finding"],
64705
- storyIds: unique
65115
+ storyIds: sites
64706
65116
  });
64707
65117
  }
64708
65118
  return proposals;
@@ -64873,8 +65283,9 @@ function runHeuristics(observations, thresholds) {
64873
65283
  ...h6FixCycleUnchanged(observations, t.unchangedOutcome)
64874
65284
  ];
64875
65285
  }
64876
- var DEFAULT_THRESHOLDS2;
65286
+ var DEFAULT_THRESHOLDS2, HIGH_SEVERITY_MULTIPLE = 2, DESCRIPTION_GIST_CHARS = 90, MAX_EVIDENCE_FILES = 4, CROSS_FEATURE_MESSAGE_PREFIX = 48;
64877
65287
  var init_heuristics = __esm(() => {
65288
+ init_review();
64878
65289
  DEFAULT_THRESHOLDS2 = {
64879
65290
  repeatedFinding: 2,
64880
65291
  emptyKeyword: 2,
@@ -64947,7 +65358,7 @@ function renderProposals(proposals, runId, observationCount) {
64947
65358
  const storyList = p.storyIds.join(", ");
64948
65359
  lines.push(`- [ ] [${p.severity}] ${p.id}: ${p.description} \u2014 stories: ${storyList}`);
64949
65360
  if (p.evidence) {
64950
- lines.push(` _Evidence: ${p.evidence}_`);
65361
+ lines.push(` _Evidence: ${p.evidence.replace(/\s*\n\s*/g, " \xB7 ")}_`);
64951
65362
  }
64952
65363
  }
64953
65364
  lines.push("");
@@ -64957,6 +65368,26 @@ function renderProposals(proposals, runId, observationCount) {
64957
65368
  `);
64958
65369
  }
64959
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
+
64960
65391
  // src/plugins/builtin/curator/rollup.ts
64961
65392
  import { appendFile as appendFile3, mkdir as mkdir9, writeFile } from "fs/promises";
64962
65393
  import * as path19 from "path";
@@ -64977,7 +65408,72 @@ async function appendToRollup(observations, rollupPath) {
64977
65408
  await appendFile3(rollupPath, newLines);
64978
65409
  } catch {}
64979
65410
  }
64980
- 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
+ });
64981
65477
 
64982
65478
  // src/plugins/builtin/curator/index.ts
64983
65479
  import { mkdir as mkdir10 } from "fs/promises";
@@ -65022,11 +65518,12 @@ function getCuratorThresholds(context) {
65022
65518
  unchangedOutcome: raw.unchangedOutcome ?? DEFAULT_THRESHOLDS3.unchangedOutcome
65023
65519
  };
65024
65520
  }
65025
- 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;
65026
65522
  var init_curator = __esm(() => {
65027
65523
  init_collect();
65028
65524
  init_heuristics();
65029
65525
  init_rollup();
65526
+ init_rollup();
65030
65527
  DEFAULT_THRESHOLDS3 = {
65031
65528
  repeatedFinding: 2,
65032
65529
  emptyKeyword: 2,
@@ -65059,12 +65556,22 @@ var init_curator = __esm(() => {
65059
65556
  await Bun.write(observationsPath, observations.map((o) => JSON.stringify(o)).join(`
65060
65557
  `) + (observations.length > 0 ? `
65061
65558
  ` : ""));
65559
+ await appendToRollup(observations, rollupPath);
65062
65560
  const thresholds = getCuratorThresholds(context);
65063
- 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);
65064
65572
  const markdown = renderProposals(proposals, context.runId, observations.length);
65065
65573
  const proposalsMdPath = path20.join(runDir, "curator-proposals.md");
65066
65574
  await Bun.write(proposalsMdPath, markdown);
65067
- await appendToRollup(observations, rollupPath);
65068
65575
  }
65069
65576
  return {
65070
65577
  success: true,
@@ -65097,15 +65604,21 @@ function selectFinish(config2) {
65097
65604
  return;
65098
65605
  return finishConfigSelector.select(config2)?.finish;
65099
65606
  }
65607
+ function resolveFlowAgent(config2, explicit) {
65608
+ if (typeof explicit === "string" && explicit.length > 0)
65609
+ return explicit;
65610
+ return resolveDefaultAgent(config2 ?? {});
65611
+ }
65100
65612
  function getFinishAutoFlowConfig(ctx) {
65101
65613
  const autoFlow = selectFinish(ctx.config)?.autoFlow;
65102
65614
  if (!autoFlow)
65103
- return DEFAULT_FINISH_AUTO_FLOW_CONFIG;
65615
+ return { ...DEFAULT_FINISH_AUTO_FLOW_CONFIG, defaultAgent: resolveFlowAgent(ctx.config, null) };
65104
65616
  const defaults = DEFAULT_FINISH_AUTO_FLOW_CONFIG;
65105
65617
  return {
65106
65618
  enabled: autoFlow.enabled === true,
65107
65619
  flowPath: autoFlow.flowPath ?? defaults.flowPath,
65108
- defaultAgent: autoFlow.defaultAgent ?? null,
65620
+ defaultAgent: resolveFlowAgent(ctx.config, autoFlow.defaultAgent),
65621
+ model: autoFlow.model ?? null,
65109
65622
  reviewers: {
65110
65623
  spec: autoFlow.reviewers?.spec ?? null,
65111
65624
  quality: autoFlow.reviewers?.quality ?? null
@@ -65129,11 +65642,12 @@ function telegramCreds(config2) {
65129
65642
  }
65130
65643
  var DEFAULT_FINISH_AUTO_FLOW_CONFIG;
65131
65644
  var init_config2 = __esm(() => {
65645
+ init_agents();
65132
65646
  init_config();
65133
65647
  DEFAULT_FINISH_AUTO_FLOW_CONFIG = {
65134
65648
  enabled: false,
65135
65649
  flowPath: "flows/nax-finish/nax-finish.flow.ts",
65136
- defaultAgent: null,
65650
+ model: null,
65137
65651
  reviewers: { spec: null, quality: null },
65138
65652
  escalate: { telegram: true },
65139
65653
  notify: { mode: "escalation" },
@@ -65228,14 +65742,21 @@ async function defaultRun2(cmd, opts) {
65228
65742
  clearTimeout(timer);
65229
65743
  }
65230
65744
  }
65231
- async function defaultReadResult(workdir) {
65232
- 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);
65233
65754
  if (!await f.exists())
65234
65755
  return null;
65235
65756
  return JSON.parse(await f.text());
65236
65757
  }
65237
- async function defaultClearResult(workdir) {
65238
- const file3 = Bun.file(path21.join(workdir, ".nax", "nax-finish-result.json"));
65758
+ async function defaultClearResult(resultPath) {
65759
+ const file3 = Bun.file(resultPath);
65239
65760
  if (await file3.exists())
65240
65761
  await file3.delete();
65241
65762
  }
@@ -65260,18 +65781,19 @@ async function resolveFlowPath(workdir, flowPath, deps = _naxFinishDeps) {
65260
65781
  }
65261
65782
  return null;
65262
65783
  }
65263
- function buildFlowArgv(flowPath, inputJson, defaultAgent, stepMs) {
65264
- 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))] : [];
65265
65786
  return [
65266
65787
  "acpx",
65267
65788
  "--approve-all",
65268
65789
  ...stepTimeout,
65790
+ ...opts.model ? ["--model", opts.model] : [],
65269
65791
  "flow",
65270
65792
  "run",
65271
65793
  flowPath,
65272
65794
  "--input-json",
65273
65795
  inputJson,
65274
- ...defaultAgent ? ["--default-agent", defaultAgent] : []
65796
+ ...opts.defaultAgent ? ["--default-agent", opts.defaultAgent] : []
65275
65797
  ];
65276
65798
  }
65277
65799
  function buildFlowEnv(cfg) {
@@ -65309,22 +65831,29 @@ async function executeFinishFlow(options) {
65309
65831
  escalateTelegram
65310
65832
  };
65311
65833
  }
65312
- await _naxFinishDeps.clearResult(ctx.workdir);
65834
+ const resultPath = finishResultPath(ctx, ctx.runId);
65835
+ await _naxFinishDeps.clearResult(resultPath);
65313
65836
  const input = {
65314
65837
  feature: ctx.feature,
65315
65838
  workdir: ctx.workdir,
65316
65839
  branch: ctx.branch,
65317
65840
  prdPath: ctx.prdPath,
65841
+ auditDir: finishAuditDir(ctx),
65842
+ runId: ctx.runId,
65318
65843
  escalateTelegram,
65319
65844
  timeouts: { acceptanceMs: cfg.timeouts.acceptanceMs, gateMs: cfg.timeouts.gateMs }
65320
65845
  };
65321
- 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
+ });
65322
65851
  const res = await _naxFinishDeps.run(cmd, {
65323
65852
  cwd: ctx.workdir,
65324
65853
  env: buildFlowEnv(cfg),
65325
65854
  timeoutMs: cfg.timeouts.flowMs
65326
65855
  });
65327
- const result = await _naxFinishDeps.readResult(ctx.workdir);
65856
+ const result = await _naxFinishDeps.readResult(resultPath);
65328
65857
  if (!result)
65329
65858
  return missingResultOutcome(ctx, res, escalateTelegram);
65330
65859
  return {
@@ -65702,7 +66231,7 @@ function startHeartbeat(opts) {
65702
66231
  let stopped = false;
65703
66232
  let timer;
65704
66233
  const armTimer = () => {
65705
- timer = setTimeout(() => {
66234
+ timer = _heartbeatDeps.setTimeout(() => {
65706
66235
  try {
65707
66236
  Promise.resolve(onTick(getSnapshot())).catch((err) => getSafeLogger()?.warn(STAGE2, "Heartbeat tick failed", {
65708
66237
  error: err instanceof Error ? err.message : String(err)
@@ -65721,7 +66250,7 @@ function startHeartbeat(opts) {
65721
66250
  stop() {
65722
66251
  stopped = true;
65723
66252
  if (timer !== undefined)
65724
- clearTimeout(timer);
66253
+ _heartbeatDeps.clearTimeout(timer);
65725
66254
  }
65726
66255
  };
65727
66256
  }
@@ -65765,10 +66294,14 @@ function buildHeartbeatMetricsPayload(p) {
65765
66294
  ]
65766
66295
  };
65767
66296
  }
65768
- var STAGE2 = "otel-reporter-heartbeat";
66297
+ var STAGE2 = "otel-reporter-heartbeat", _heartbeatDeps;
65769
66298
  var init_heartbeat = __esm(() => {
65770
66299
  init_logger2();
65771
66300
  init_otlp();
66301
+ _heartbeatDeps = {
66302
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
66303
+ clearTimeout: (id) => clearTimeout(id)
66304
+ };
65772
66305
  });
65773
66306
 
65774
66307
  // src/plugins/builtin/otel-reporter/ids.ts
@@ -65798,7 +66331,7 @@ function toLogRecord(entry) {
65798
66331
  const nonScalars = {};
65799
66332
  for (const [key, value] of Object.entries(data)) {
65800
66333
  if (typeof value === "string") {
65801
- attributes.push(attr(`nax.data.${key}`, truncate3(value)));
66334
+ attributes.push(attr(`nax.data.${key}`, truncate4(value)));
65802
66335
  } else if (typeof value === "number") {
65803
66336
  if (Number.isFinite(value)) {
65804
66337
  attributes.push(attr(`nax.data.${key}`, value));
@@ -65812,7 +66345,7 @@ function toLogRecord(entry) {
65812
66345
  }
65813
66346
  }
65814
66347
  if (Object.keys(nonScalars).length > 0) {
65815
- attributes.push(attr("nax.data_json", truncate3(JSON.stringify(nonScalars))));
66348
+ attributes.push(attr("nax.data_json", truncate4(JSON.stringify(nonScalars))));
65816
66349
  }
65817
66350
  return {
65818
66351
  body: { stringValue: entry.message },
@@ -65835,7 +66368,7 @@ function buildLogsPayload(entries, resource) {
65835
66368
  ]
65836
66369
  };
65837
66370
  }
65838
- function truncate3(value) {
66371
+ function truncate4(value) {
65839
66372
  if (value.length <= DATA_JSON_MAX)
65840
66373
  return value;
65841
66374
  const marker = TRUNCATION_MARKER;
@@ -66803,11 +67336,11 @@ function getSafeLogger6() {
66803
67336
  return getSafeLogger();
66804
67337
  }
66805
67338
  function extractPluginName(pluginPath) {
66806
- const basename12 = path22.basename(pluginPath);
66807
- 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") {
66808
67341
  return path22.basename(path22.dirname(pluginPath));
66809
67342
  }
66810
- return basename12.replace(/\.(ts|js|mjs)$/, "");
67343
+ return basename13.replace(/\.(ts|js|mjs)$/, "");
66811
67344
  }
66812
67345
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins, isTestFileFn, reporters) {
66813
67346
  const loadedPlugins = [];
@@ -67480,9 +68013,9 @@ var init_hooks = __esm(() => {
67480
68013
  // src/execution/crash-heartbeat.ts
67481
68014
  import { appendFileSync as appendFileSync2 } from "fs";
67482
68015
  async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath) {
67483
- const logger = _heartbeatDeps.getSafeLogger();
68016
+ const logger = _heartbeatDeps2.getSafeLogger();
67484
68017
  while (gen === _heartbeatGen && _heartbeatActive) {
67485
- await _heartbeatDeps.sleep(60000);
68018
+ await _heartbeatDeps2.sleep(60000);
67486
68019
  if (gen !== _heartbeatGen || !_heartbeatActive)
67487
68020
  break;
67488
68021
  try {
@@ -67511,11 +68044,11 @@ async function heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jso
67511
68044
  }
67512
68045
  }
67513
68046
  function startHeartbeat2(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
67514
- const logger = _heartbeatDeps.getSafeLogger();
68047
+ const logger = _heartbeatDeps2.getSafeLogger();
67515
68048
  _heartbeatActive = true;
67516
68049
  const gen = ++_heartbeatGen;
67517
68050
  heartbeatLoop(gen, statusWriter, getTotalCost, getIterations, jsonlFilePath).catch((err) => {
67518
- _heartbeatDeps.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
68051
+ _heartbeatDeps2.getSafeLogger()?.warn("crash-recovery", "Heartbeat loop crashed; status updates stopped", {
67519
68052
  error: err instanceof Error ? err.message : String(err)
67520
68053
  });
67521
68054
  });
@@ -67528,10 +68061,10 @@ function stopHeartbeat() {
67528
68061
  getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
67529
68062
  }
67530
68063
  }
67531
- var _heartbeatDeps, _heartbeatGen = 0, _heartbeatActive = false;
68064
+ var _heartbeatDeps2, _heartbeatGen = 0, _heartbeatActive = false;
67532
68065
  var init_crash_heartbeat = __esm(() => {
67533
68066
  init_logger2();
67534
- _heartbeatDeps = {
68067
+ _heartbeatDeps2 = {
67535
68068
  sleep: async (ms) => Bun.sleep(ms),
67536
68069
  getSafeLogger
67537
68070
  };
@@ -68306,6 +68839,7 @@ function buildAcceptanceContext(ctx, prd) {
68306
68839
  agentManager: ctx.agentManager,
68307
68840
  sessionManager: ctx.sessionManager,
68308
68841
  acceptanceTestPaths: ctx.acceptanceTestPaths,
68842
+ acceptanceRetries: ctx.acceptanceRetries ?? 0,
68309
68843
  runtime: ctx.runtime,
68310
68844
  abortSignal: ctx.abortSignal
68311
68845
  };
@@ -68402,8 +68936,9 @@ async function runAcceptanceLoop(ctx) {
68402
68936
  logger?.info("acceptance", "All stories complete, running acceptance validation");
68403
68937
  const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
68404
68938
  while (acceptanceRetries < maxRetries) {
68939
+ const attemptCtx = { ...ctx, acceptanceRetries };
68405
68940
  const firstStory = prd.userStories[0];
68406
- const acceptanceContext = buildAcceptanceContext(ctx, prd);
68941
+ const acceptanceContext = buildAcceptanceContext(attemptCtx, prd);
68407
68942
  const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
68408
68943
  if (acceptanceResult.action === "continue") {
68409
68944
  logger?.info("acceptance", "Acceptance validation passed!");
@@ -68494,17 +69029,14 @@ async function runAcceptanceLoop(ctx) {
68494
69029
  confidence: diagnosis.confidence,
68495
69030
  attempt: acceptanceRetries
68496
69031
  });
68497
- const cycleResult = await runAcceptanceFixCycle(ctx, prd, pkgFailures, diagnosis, effectivePath, testCommand, {
68498
- packageDir: pkg.packageDir,
68499
- testPath: effectivePath
68500
- });
69032
+ const cycleResult = await runAcceptanceFixCycle(attemptCtx, prd, pkgFailures, diagnosis, effectivePath, testCommand, { packageDir: pkg.packageDir, testPath: effectivePath });
68501
69033
  totalCost2 += cycleResult.costUsd ?? 0;
68502
69034
  totalInternalIterations += cycleResult.iterations.length;
68503
69035
  const pkgResolved = cycleResult.exitReason === "resolved" || cycleResult.finalFindings.length === 0;
68504
69036
  if (!pkgResolved)
68505
69037
  remainingFindings.push(...cycleResult.finalFindings);
68506
69038
  }
68507
- const finalCheck = await runAcceptanceTestsOnce(ctx, prd);
69039
+ const finalCheck = await runAcceptanceTestsOnce(attemptCtx, prd);
68508
69040
  const success2 = finalCheck.passed && remainingFindings.length === 0;
68509
69041
  const failureMessages = !success2 ? finalCheck.failedACs.length > 0 ? finalCheck.failedACs : remainingFindings.length > 0 ? remainingFindings.map((f) => f.message) : ["acceptance validation failed (unknown cause)"] : undefined;
68510
69042
  return buildResult(success2, prd, totalCost2, iterations, storiesCompleted, prdDirty, failureMessages, acceptanceRetries + totalInternalIterations);
@@ -69812,10 +70344,10 @@ var init_ensure_package_dirs = __esm(() => {
69812
70344
 
69813
70345
  // src/pipeline/subscribers/events-writer.ts
69814
70346
  import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
69815
- import { basename as basename15, join as join86 } from "path";
70347
+ import { basename as basename16, join as join86 } from "path";
69816
70348
  function wireEventsWriter(bus, feature, runId, workdir) {
69817
70349
  const logger = getSafeLogger();
69818
- const project = basename15(workdir);
70350
+ const project = basename16(workdir);
69819
70351
  const eventsDir = join86(getEventsRootDir(), project);
69820
70352
  const eventsFile = join86(eventsDir, "events.jsonl");
69821
70353
  let dirReady = false;
@@ -69998,10 +70530,10 @@ var init_interaction2 = __esm(() => {
69998
70530
 
69999
70531
  // src/pipeline/subscribers/registry.ts
70000
70532
  import { mkdir as mkdir14, writeFile as writeFile2 } from "fs/promises";
70001
- import { basename as basename16, join as join87 } from "path";
70533
+ import { basename as basename17, join as join87 } from "path";
70002
70534
  function wireRegistry(bus, feature, runId, workdir, outputDir) {
70003
70535
  const logger = getSafeLogger();
70004
- const project = basename16(workdir);
70536
+ const project = basename17(workdir);
70005
70537
  const runDir = join87(getRunsDir(), `${project}-${feature}-${runId}`);
70006
70538
  const metaFile = join87(runDir, "meta.json");
70007
70539
  const unsub = bus.on("run:started", (_ev) => {
@@ -73921,7 +74453,8 @@ function buildPostRunContext(opts, durationMs, logger) {
73921
74453
  projectKey,
73922
74454
  curatorRollupPath: curatorRollupPath2,
73923
74455
  logFilePath,
73924
- config: config2
74456
+ config: config2,
74457
+ startTime
73925
74458
  } = opts;
73926
74459
  const counts = countStories(prd);
73927
74460
  return {
@@ -73932,6 +74465,7 @@ function buildPostRunContext(opts, durationMs, logger) {
73932
74465
  branch,
73933
74466
  version: version2,
73934
74467
  totalDurationMs: durationMs,
74468
+ runStartedAt: startTime,
73935
74469
  totalCost: totalCost2,
73936
74470
  storySummary: {
73937
74471
  completed: storiesCompleted,
@@ -103897,6 +104431,88 @@ var init_bakeoff = __esm(() => {
103897
104431
  init_report2();
103898
104432
  });
103899
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
+
103900
104516
  // src/commands/curator.ts
103901
104517
  var exports_curator = {};
103902
104518
  __export(exports_curator, {
@@ -103904,14 +104520,12 @@ __export(exports_curator, {
103904
104520
  curatorGc: () => curatorGc,
103905
104521
  curatorDryrun: () => curatorDryrun,
103906
104522
  curatorCommit: () => curatorCommit,
104523
+ _testing: () => _testing,
103907
104524
  _curatorCmdDeps: () => _curatorCmdDeps
103908
104525
  });
103909
104526
  import { readdirSync as readdirSync9 } from "fs";
103910
- import { unlink as unlink4 } from "fs/promises";
103911
- import { basename as basename18, join as join100 } from "path";
103912
- function getProjectKey(config2, projectDir) {
103913
- return config2.name?.trim() || basename18(projectDir);
103914
- }
104527
+ import { unlink as unlink5 } from "fs/promises";
104528
+ import { join as join100 } from "path";
103915
104529
  function listRunIds(runsDir) {
103916
104530
  try {
103917
104531
  return readdirSync9(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
@@ -104153,34 +104767,25 @@ async function curatorGc(options) {
104153
104767
  const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
104154
104768
  const gDir = _curatorCmdDeps.globalOutputDir();
104155
104769
  const rollupPath = _curatorCmdDeps.curatorRollupPath(gDir, config2.curator?.rollupPath);
104156
- const rollupText = await _curatorCmdDeps.readFile(rollupPath).catch(() => null);
104157
- if (rollupText === null) {
104770
+ if (!await _curatorCmdDeps.fileExists(rollupPath)) {
104158
104771
  console.log(`[gc] No rollup file found at ${rollupPath}. Nothing to prune.`);
104159
104772
  return;
104160
104773
  }
104161
- const lines = rollupText.trim().split(`
104162
- `).filter(Boolean);
104163
- const observations = lines.map((l) => JSON.parse(l));
104164
- const maxTsByRunId = new Map;
104165
- for (const obs of observations) {
104166
- const existing = maxTsByRunId.get(obs.runId);
104167
- if (!existing || obs.ts > existing) {
104168
- maxTsByRunId.set(obs.runId, obs.ts);
104169
- }
104170
- }
104774
+ const projectKey = getProjectKey(config2, resolved.projectDir);
104775
+ const uniqueRunIds = await _curatorCmdDeps.scanProjectRunIds(rollupPath, projectKey);
104171
104776
  const keep = options.keep ?? DEFAULT_KEEP;
104172
- const uniqueRunIds = [...maxTsByRunId.entries()].sort((a, b) => a[1] > b[1] ? -1 : a[1] < b[1] ? 1 : 0).map(([runId]) => runId);
104173
- if (uniqueRunIds.length <= keep) {
104174
- 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.`);
104175
104780
  return;
104176
104781
  }
104177
104782
  const keepSet = new Set(uniqueRunIds.slice(0, keep));
104178
- const filtered = observations.filter((obs) => keepSet.has(obs.runId));
104179
- const newContent = `${filtered.map((obs) => JSON.stringify(obs)).join(`
104180
- `)}
104181
- `;
104182
- await _curatorCmdDeps.writeFile(rollupPath, newContent);
104183
- const projectKey = getProjectKey(config2, resolved.projectDir);
104783
+ const result2 = await _curatorCmdDeps.pruneRollup({
104784
+ rollupPath,
104785
+ projectKey,
104786
+ keepRunIds: keepSet,
104787
+ dropUnattributed: sweep
104788
+ });
104184
104789
  const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
104185
104790
  const perRunsDir = join100(outputDir, "runs");
104186
104791
  for (const runId of uniqueRunIds) {
@@ -104190,12 +104795,20 @@ async function curatorGc(options) {
104190
104795
  await _curatorCmdDeps.removeFile(join100(runDir, "curator-proposals.md"));
104191
104796
  }
104192
104797
  }
104193
- 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.");
104194
104806
  }
104195
- var _curatorCmdDeps, DEFAULT_KEEP = 50;
104807
+ var _curatorCmdDeps, _testing, DEFAULT_KEEP = 50;
104196
104808
  var init_curator2 = __esm(() => {
104197
104809
  init_config();
104198
104810
  init_heuristics();
104811
+ init_rollup_prune();
104199
104812
  init_paths2();
104200
104813
  init_common();
104201
104814
  _curatorCmdDeps = {
@@ -104205,6 +104818,9 @@ var init_curator2 = __esm(() => {
104205
104818
  globalOutputDir: () => globalOutputDir(),
104206
104819
  curatorRollupPath: (gDir, override) => curatorRollupPath(gDir, override),
104207
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),
104208
104824
  writeFile: async (p, content) => {
104209
104825
  await Bun.write(p, content);
104210
104826
  },
@@ -104215,7 +104831,7 @@ var init_curator2 = __esm(() => {
104215
104831
  },
104216
104832
  removeFile: async (p) => {
104217
104833
  try {
104218
- await unlink4(p);
104834
+ await unlink5(p);
104219
104835
  } catch {}
104220
104836
  },
104221
104837
  openInEditor: async (filePath) => {
@@ -104226,13 +104842,14 @@ var init_curator2 = __esm(() => {
104226
104842
  }
104227
104843
  }
104228
104844
  };
104845
+ _testing = { parseCheckedProposals };
104229
104846
  });
104230
104847
 
104231
104848
  // bin/nax.ts
104232
104849
  init_source();
104233
104850
  import { existsSync as existsSync39, mkdirSync as mkdirSync8 } from "fs";
104234
104851
  import { homedir as homedir3 } from "os";
104235
- import { basename as basename19, join as join101 } from "path";
104852
+ import { basename as basename20, join as join101 } from "path";
104236
104853
 
104237
104854
  // node_modules/commander/esm.mjs
104238
104855
  var import__ = __toESM(require_commander(), 1);
@@ -104889,12 +105506,12 @@ init_errors();
104889
105506
  init_logger2();
104890
105507
  init_runtime();
104891
105508
  import { existsSync as existsSync18, readdirSync as readdirSync4 } from "fs";
104892
- import { basename as basename9, join as join46 } from "path";
105509
+ import { basename as basename10, join as join46 } from "path";
104893
105510
  async function resolveOutputDir(workdir, override) {
104894
105511
  if (override)
104895
105512
  return override;
104896
105513
  const config2 = await loadConfig(workdir).catch(() => null);
104897
- const projectKey = config2?.name?.trim() || basename9(workdir);
105514
+ const projectKey = config2?.name?.trim() || basename10(workdir);
104898
105515
  return projectOutputDir(projectKey, config2?.outputDir);
104899
105516
  }
104900
105517
  async function parseRunLog(logPath) {
@@ -105929,7 +106546,7 @@ async function contextInspectCommand(options) {
105929
106546
  init_canonical_loader();
105930
106547
  init_errors();
105931
106548
  import { mkdir as mkdir11 } from "fs/promises";
105932
- import { basename as basename13, join as join72 } from "path";
106549
+ import { basename as basename14, join as join72 } from "path";
105933
106550
  var _rulesCLIDeps = {
105934
106551
  readFile: async (path24) => Bun.file(path24).text(),
105935
106552
  writeFile: async (path24, content) => {
@@ -106014,6 +106631,32 @@ function neutralizeContent(content) {
106014
106631
  apply(/\p{Extended_Pictographic}/gu, "");
106015
106632
  return { content: result.trim(), replacements };
106016
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
+ }
106017
106660
  async function collectMigrationSources(workdir) {
106018
106661
  const sources = [];
106019
106662
  const claudeMdPath = join72(workdir, "CLAUDE.md");
@@ -106029,7 +106672,7 @@ async function collectMigrationSources(workdir) {
106029
106672
  try {
106030
106673
  const content = await _rulesCLIDeps.readFile(filePath);
106031
106674
  if (content.trim()) {
106032
- sources.push({ sourcePath: filePath, targetFileName: basename13(filePath), content });
106675
+ sources.push({ sourcePath: filePath, targetFileName: basename14(filePath), content });
106033
106676
  }
106034
106677
  } catch {}
106035
106678
  }
@@ -106060,11 +106703,9 @@ async function rulesMigrateCommand(options) {
106060
106703
  skipped++;
106061
106704
  continue;
106062
106705
  }
106063
- const { content: neutralized, replacements } = neutralizeContent(content);
106064
- const notice = replacements > 0 ? `<!-- NOTE: ${replacements} neutralization(s) applied \u2014 review before committing -->
106065
-
106066
- ` : "";
106067
- const output = notice + neutralized;
106706
+ const { content: scoped2 } = translateLegacyFrontmatter(content);
106707
+ const { content: neutralized, replacements } = neutralizeContent(scoped2);
106708
+ const output = withReviewNotice(neutralized, replacements);
106068
106709
  if (options.dryRun) {
106069
106710
  console.log(`[dry-run] Would write ${targetFileName} from ${sourcePath} (${replacements} replacements)`);
106070
106711
  } else {
@@ -106140,6 +106781,7 @@ async function resolveRunProfileOverride(opts) {
106140
106781
  }
106141
106782
  // src/cli/features-resolve.ts
106142
106783
  init_config();
106784
+ init_test_runners();
106143
106785
  import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
106144
106786
  import { join as join74, relative as relative17 } from "path";
106145
106787
 
@@ -106198,6 +106840,15 @@ async function resolveGroupCommand(repoRoot, packageDir, rootCommand) {
106198
106840
  }
106199
106841
 
106200
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
+ }
106201
106852
  async function isNonEmptyFile(absolutePath) {
106202
106853
  if (!existsSync28(absolutePath))
106203
106854
  return false;
@@ -106302,6 +106953,7 @@ async function resolveFeatureSpec(name, workdir) {
106302
106953
  featureName: name,
106303
106954
  specSource: source2,
106304
106955
  acceptance: await resolveFeatureAcceptance(name, workdir),
106956
+ testPatterns: await resolveTestPatterns(workdir),
106305
106957
  message: `resolved spec: ${source2.path}`
106306
106958
  };
106307
106959
  }
@@ -106348,6 +107000,7 @@ async function resolveFeatureSpec(name, workdir) {
106348
107000
  featureName: onlyName,
106349
107001
  specSource: source,
106350
107002
  acceptance: await resolveFeatureAcceptance(onlyName, workdir),
107003
+ testPatterns: await resolveTestPatterns(workdir),
106351
107004
  message: `resolved spec: ${source.path}`
106352
107005
  };
106353
107006
  }
@@ -106367,7 +107020,7 @@ init_runtime();
106367
107020
  init_json_file();
106368
107021
  init_routing();
106369
107022
  import { mkdirSync as mkdirSync6 } from "fs";
106370
- import { basename as basename14, join as join75 } from "path";
107023
+ import { basename as basename15, join as join75 } from "path";
106371
107024
  var _routingCalibrateDeps = {
106372
107025
  loadRunMetrics: (outputDir) => loadRunMetrics(outputDir),
106373
107026
  readConfig: (workdir) => loadConfig(workdir),
@@ -106425,7 +107078,7 @@ async function runRoutingCalibrateCli(options, deps = _routingCalibrateDeps) {
106425
107078
  function resolveOutputDir2(workdir, override, prior) {
106426
107079
  if (override)
106427
107080
  return override;
106428
- const key = prior?.name?.trim() || basename14(workdir);
107081
+ const key = prior?.name?.trim() || basename15(workdir);
106429
107082
  return projectOutputDir(key, prior?.outputDir);
106430
107083
  }
106431
107084
  function mergeComplexityRouting(prior, adjustments) {
@@ -107411,7 +108064,7 @@ init_errors();
107411
108064
  init_checkpoint();
107412
108065
  init_runtime();
107413
108066
  import { existsSync as existsSync37 } from "fs";
107414
- import { basename as basename17, join as join95 } from "path";
108067
+ import { basename as basename18, join as join95 } from "path";
107415
108068
  async function defaultCheckpointExists(featureDir) {
107416
108069
  if (!featureDir || !existsSync37(featureDir))
107417
108070
  return false;
@@ -107490,7 +108143,7 @@ function registerResumeCommand(program2) {
107490
108143
  const globalNaxDir = globalConfigDir();
107491
108144
  const hooks = await loadHooksConfig2(naxDir, globalNaxDir);
107492
108145
  applyResumeModeDeps2(opts.featureDir ?? "", "auto");
107493
- const projectKey = config2.name?.trim() || basename17(cmdOpts.dir);
108146
+ const projectKey = config2.name?.trim() || basename18(cmdOpts.dir);
107494
108147
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
107495
108148
  const statusFilePath = join95(outputDir, "status.json");
107496
108149
  const result = await run2({
@@ -115941,7 +116594,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
115941
116594
  process.exit(1);
115942
116595
  }
115943
116596
  resetLogger();
115944
- const projectKey = config2.name?.trim() || basename19(workdir);
116597
+ const projectKey = config2.name?.trim() || basename20(workdir);
115945
116598
  const outputDir = projectOutputDir(projectKey, config2.outputDir);
115946
116599
  const runsDir = join101(outputDir, "features", options.feature, "runs");
115947
116600
  mkdirSync8(runsDir, { recursive: true });
@@ -116562,12 +117215,13 @@ curator.command("dryrun").description("Re-run heuristics on an existing observat
116562
117215
  process.exit(1);
116563
117216
  }
116564
117217
  });
116565
- 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) => {
116566
117219
  const { curatorGc: curatorGc2 } = await Promise.resolve().then(() => (init_curator2(), exports_curator));
116567
117220
  try {
116568
117221
  await curatorGc2({
116569
117222
  project: options.project,
116570
- 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
116571
117225
  });
116572
117226
  } catch (err) {
116573
117227
  console.error(source_default.red(`Error: ${err.message}`));