agent-qa 0.1.21 → 0.2.1

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/index.d.ts CHANGED
@@ -1,19 +1,144 @@
1
- import { AgentQaConfig } from '@vostride/agent-qa-core';
1
+ import { AgentQaConfig, resolveKnowledgeRoot } from '@vostride/agent-qa-core';
2
2
  export * from '@vostride/agent-qa-core';
3
+ import { MaskFn, CompletedRunHandoff, Benefit, CrossCheck, CanaryOutcome, RecordId, Anchor, RunAnalysisArtifact, MisdirectionRecordResult, TrustPriorTable, CurationSnapshot, ResolvePromotionPosture, ScopeKey } from '@vostride/agent-qa-schema';
4
+ import { CuratorAuthor, LocalCurationCapabilities, CurationCandidateProjection, EnqueueCurationCandidateProjectionOutcome, OpenServingKnowledgeReaderConfig } from '@vostride/agent-qa-agent-local';
5
+ import { ModelProfile, ResolvedModelAuth, createModel, resolveCapabilities, createTokenAccounting } from '@vostride/agent-qa-model';
6
+ import { KnowledgeHintReader } from '@vostride/agent-qa-runner';
7
+ import { loadCompletedRunHandoff } from '@vostride/agent-qa-runner-local';
8
+ import { runSafetyScope } from '@vostride/agent-qa-safety-local';
9
+ import { z } from 'zod';
3
10
 
11
+ /**
12
+ * A committed config the parser cannot read, named by WHERE and never by WHAT (395-14 · F-01).
13
+ *
14
+ * WHY A NAMED CLASS AND NOT A BARE `Error`. This throw leaves the loader through every config op,
15
+ * through `resolveConfig`, and — via `refusalMessage` — into a chat turn's answer and its session
16
+ * jsonl. A caller that wants to recognise the case (F-03's typed-refusal arm, later) needs
17
+ * something better than a substring match on a sentence; `name` and `configPath` are that. The
18
+ * message itself carries the path because the surfaces this reaches print a message and nothing
19
+ * else.
20
+ *
21
+ * NO MEMBER INTERPOLATES THE CAUGHT ERROR'S OWN TEXT. `yaml` pretty-prints its parse errors with a
22
+ * verbatim excerpt of the person's source, which was measured carrying a planted credential out of
23
+ * three ops.
24
+ *
25
+ * AND THE CAUSE IS SANITIZED TOO, BECAUSE THE RUNTIME READS THE CAUSE CHAIN (395-16 · CR-01).
26
+ * An earlier revision of this class attached the raw `YAMLParseError` as `cause` on the reasoning
27
+ * that no composed member reads it. That reasoning was false of the runtime, and shipped as a
28
+ * defect: a thrown value is read by far more than the code that composed it. Node's DEFAULT
29
+ * UNCAUGHT PRINTER renders an error with `util.inspect`, which walks `cause` and prints it under a
30
+ * `[cause]:` header — `agent-qa config show` awaited this loader bare and crash-printed the
31
+ * person's own file back at them. `console.error(err)` does the same thing, structured loggers
32
+ * (pino, winston) serialize the chain, and crash reporters upload it. So the cause carried here is
33
+ * a fresh `Error` holding THE SAME POSITION-ONLY SENTENCE as the message: a debugger still gets a
34
+ * chain to walk, and every one of those readers gets a position and nothing else. The caught value
35
+ * is not reachable from this error by any property.
36
+ */
37
+ declare class ConfigFileNotYamlError extends Error {
38
+ readonly configPath: string;
39
+ constructor(configPath: string, cause: unknown);
40
+ }
41
+ /**
42
+ * THE READ AND THE PARSE GET SEPARATE CATCHES, and that separation is load-bearing.
43
+ *
44
+ * `Config file not found: <path>` is matched verbatim by the live lane's third-arm detection
45
+ * (`scripts/395-live-from-zero.sh:514`) and folded by `coveringGrantInForce`; a non-ENOENT read
46
+ * fault (EISDIR, EACCES) is a fact about the filesystem and not about the file's content, and is
47
+ * rethrown untouched. Only the PARSE throw is reworded — one guard, aimed at one channel.
48
+ */
4
49
  declare function loadConfigFile(configPath: string): Promise<unknown>;
50
+ /**
51
+ * The absent-file case, RECOGNISABLE WITHOUT A SUBSTRING MATCH ON ITS OWN SENTENCE (395-27 · WR-02).
52
+ *
53
+ * The message is BYTE-IDENTICAL to the bare `Error` this replaces, because that exact string is
54
+ * matched by the live lane's third-arm detection (`scripts/395-live-from-zero.sh:514`), folded by
55
+ * `coveringGrantInForce`, and pinned by `(E1)`. Nothing about what any surface PRINTS changes.
56
+ *
57
+ * What changes is that `showAction` can now ask WHICH fault this is by `instanceof` instead of by
58
+ * `message.startsWith(...)` — the reasoning `ConfigFileNotYamlError` above already wrote down: a
59
+ * caller that wants to recognise the case needs something better than a substring match on a
60
+ * sentence, and a sentence-matching caller is one rewording away from silently taking the wrong arm.
61
+ */
62
+ declare class ConfigFileNotFoundError extends Error {
63
+ readonly configPath: string;
64
+ constructor(configPath: string);
65
+ }
5
66
  declare function loadRequiredConfigFile(configPath: string): Promise<unknown>;
6
67
  declare function loadEnvOverrides(): Record<string, unknown>;
7
68
  declare function mergeConfigs(file: unknown, env: Record<string, unknown>, flags: Record<string, unknown>): unknown;
8
69
  declare function deepMerge(base: unknown, override: unknown): unknown;
9
70
  declare function mergeWithTestConfig(globalConfig: unknown, testConfig: unknown, flagOverrides: Record<string, unknown>): unknown;
71
+ /**
72
+ * The one shaping `resolveConfig` applies before validating (395-01: exported, not moved).
73
+ *
74
+ * EXPORTED so the config-mutation validate port can run the SAME three steps a real run runs —
75
+ * merge, normalize, parse. A port that skipped this would refuse a candidate whose only problem is
76
+ * a key `resolveConfig` deletes, which is a proposal blocked by a rule nothing enforces.
77
+ */
78
+ declare function normalizeConfigForValidation(config: unknown): unknown;
10
79
  declare function mergeUseBlocks(globalUse: Record<string, unknown> | undefined, suiteUse: Record<string, unknown> | undefined, testUse: Record<string, unknown> | undefined, cliFlags: Record<string, unknown>): Record<string, unknown>;
11
- declare function formatConfigDebug(globalConfig: unknown, testConfig: unknown, flagOverrides: Record<string, unknown>): string;
80
+ declare function formatConfigDebug(globalConfig: unknown, testConfig: unknown, flagOverrides: Record<string, unknown>, mask: MaskFn): string;
81
+ /**
82
+ * The privacy declaration alone, read out of the RAW document without validating it
83
+ * (Phase 398 gap round — finding F-1 / review CR-01).
84
+ *
85
+ * WHY THIS EXISTS. The composition roots that emit telemetry read their config through a tolerant
86
+ * `try { resolveConfig() } catch { undefined }`, because refusing a surface over its telemetry
87
+ * configuration would be refusing the capability over the telemetry (T-398-32). That posture is
88
+ * right and is unchanged. What was wrong is that the `catch` collapsed TWO different states into
89
+ * one `undefined`:
90
+ *
91
+ * 1. there is no config file — nothing was ever declared, and
92
+ * 2. there IS one, it declares `analytics.privacy: true`, and it failed schema validation for
93
+ * some unrelated reason (a missing `use.mobile.appState`, a legacy key, a typo three levels
94
+ * away).
95
+ *
96
+ * In state 2 a user set a shipped privacy control and was transmitted anyway, silently, with the
97
+ * surface working normally so nothing ever looked wrong. A product cannot ship a config field and
98
+ * then honour it only when the REST of the document happens to validate.
99
+ *
100
+ * READING IS NOT VALIDATING. Extracting one boolean from a parsed document does not require the
101
+ * document to be well-formed anywhere else, so honouring the declaration costs the availability
102
+ * posture nothing: the surface still opens, still scaffolds, still runs.
103
+ *
104
+ * IT READS THE SAME FILE THE VALIDATOR READ. The path expression and the loader are literally the
105
+ * ones `resolveConfig` uses, immediately below — not a second spelling of "where the config lives".
106
+ * A privacy read that consults a different file than the validator did would be a new defect
107
+ * wearing this fix's clothes.
108
+ *
109
+ * THE UNPARSEABLE ARM FAILS CLOSED. If the file exists but is not YAML at all, no reader can tell
110
+ * whether the declaration is in there. The cost of opting out is lost telemetry; the cost of the
111
+ * other choice is transmitting against a declaration that may be sitting in the file. For a
112
+ * privacy control the doubt resolves toward the user. A MISSING file is not doubt — nothing was
113
+ * declared — so it stays `undefined`.
114
+ */
115
+ declare function readAnalyticsPrivacyDeclaration(configPath?: string): Promise<{
116
+ analytics: {
117
+ privacy: boolean;
118
+ };
119
+ } | undefined>;
12
120
  declare function resolveConfig(options: {
13
121
  configPath?: string;
14
122
  flags?: Record<string, unknown>;
15
123
  loadAuthPlugins?: boolean;
16
124
  }): Promise<AgentQaConfig>;
125
+ declare function loadConfigPlugins(config: AgentQaConfig, configPath: string): Promise<void>;
126
+ /** Resolve the writer-gated knowledge root without making each CLI surface import core directly. */
127
+ declare function resolveConfiguredKnowledgeRoot(config: AgentQaConfig, configDir: string): string;
128
+ /**
129
+ * The same root, from a RAW config (407 D-18/D-19).
130
+ *
131
+ * `doctor` deliberately loads the config unvalidated so it can REPORT on a broken one, so it
132
+ * never has an `AgentQaConfig` to hand the resolver above. The schema's own `dir` default is what
133
+ * makes the root knowable from a config that never mentions `knowledge` at all — reproducing that
134
+ * default at a call site would be a second answer to "where does this project keep its knowledge".
135
+ *
136
+ * `null` means the question has no answer for this host: the config could not be read, or its
137
+ * `knowledge` block is malformed enough that the directory cannot be named. It does NOT mean the
138
+ * directory is missing — that is the caller's own existence check, because "configured but absent"
139
+ * and "not configured" read the same to a resolver and differently to a person.
140
+ */
141
+ declare function resolveRawConfigKnowledgeRoot(config: Record<string, unknown> | null, configPath: string): string | null;
17
142
 
18
143
  interface ResolvedTarget {
19
144
  name: string;
@@ -24,6 +149,8 @@ interface ResolvedTarget {
24
149
  appPackage?: string;
25
150
  appActivity?: string;
26
151
  device?: string;
152
+ /** D-06 (phase 401), 432 D-18: the target's declared environment; `undefined` means undeclared. */
153
+ environment?: 'local' | 'shared' | 'production';
27
154
  }
28
155
  interface TargetSummary {
29
156
  name: string;
@@ -32,9 +159,495 @@ interface TargetSummary {
32
159
  declare function resolveTarget(config: AgentQaConfig, targetName: string): ResolvedTarget;
33
160
  declare function listTargets(config: AgentQaConfig): TargetSummary[];
34
161
 
35
- declare function purgeTest(testPath: string, cacheDir: string, configContent?: string): Promise<number>;
36
- declare function purgeAll(cacheDir: string, force: boolean): Promise<number>;
162
+ /**
163
+ * Per-test targeting is not possible under the redesigned cache: keys are opaque,
164
+ * content-addressed over masked step text + a runtime screenIdentityHash, and carry no raw
165
+ * step text on disk (D-06). `--test` therefore clears the whole branch-namespaced cache so
166
+ * the requested test's stale plans are guaranteed gone.
167
+ */
168
+ declare function purgeTest(_testPath: string, cacheDir: string): Promise<number>;
169
+ declare function purgeAll(cacheDir: string): Promise<number>;
170
+
171
+ /** Provisional M2 malformed-output retry cap; intentionally not a user-facing config key. */
172
+ declare const CURATOR_RETRY_CAP = 2;
173
+ /** Provisional M2 neighbor presentation cap; intentionally not a user-facing config key. */
174
+ declare const CURATOR_MAX_NEIGHBORS = 12;
175
+ interface BuildCuratorAuthorDependencies {
176
+ /** The run's already-resolved registry profile. It is reused verbatim (D-03). */
177
+ profile: ModelProfile;
178
+ auth: ResolvedModelAuth;
179
+ mask: MaskFn;
180
+ createModelImpl?: typeof createModel;
181
+ resolveCapabilitiesImpl?: typeof resolveCapabilities;
182
+ createTokenAccountingImpl?: typeof createTokenAccounting;
183
+ signal?: AbortSignal;
184
+ }
185
+ /**
186
+ * Build the CLI-owned Curator author over the run's already-resolved model. The returned function
187
+ * reads no config and every provider call therefore traverses createModel's registry middleware.
188
+ */
189
+ declare function buildCuratorAuthor(deps: BuildCuratorAuthorDependencies): CuratorAuthor;
190
+
191
+ declare const nudgeResponseSchema: z.ZodObject<{
192
+ sessionId: z.ZodString;
193
+ }, z.core.$strict>;
194
+ type NudgeResponse = z.infer<typeof nudgeResponseSchema>;
195
+
196
+ interface NudgeDaemonOptions {
197
+ /** Resolved configured root; defaults only for legacy project-local callers. */
198
+ readonly knowledgeRoot?: string;
199
+ /** Absolute wall-clock deadline shared with the caller's complete operation. */
200
+ readonly deadlineMs?: number;
201
+ /** Request an empty terminal receipt when the caller gates completion on it. */
202
+ readonly requireReceipt?: boolean;
203
+ readonly signal?: AbortSignal;
204
+ }
205
+ /** Best-effort curation wake-up returning the durable session identity when acknowledged. */
206
+ declare function nudgeDaemon(agentQaRoot: string, options?: NudgeDaemonOptions): Promise<NudgeResponse | undefined>;
207
+
208
+ declare const completionSchema: z.ZodObject<{
209
+ schemaVersion: z.ZodLiteral<2>;
210
+ runId: z.ZodString;
211
+ candidateId: z.ZodString;
212
+ attributedObservationId: z.ZodString;
213
+ observedStateDigest: z.ZodString;
214
+ result: z.ZodUnion<readonly [z.ZodObject<{
215
+ kind: z.ZodLiteral<"ablated">;
216
+ crossCheck: z.ZodEnum<{
217
+ flipped: "flipped";
218
+ held: "held";
219
+ inconclusive: "inconclusive";
220
+ "not-run": "not-run";
221
+ }>;
222
+ corpusDigest: z.ZodString;
223
+ k: z.ZodNumber;
224
+ baseSeed: z.ZodNumber;
225
+ seeds: z.ZodArray<z.ZodNumber>;
226
+ seedHonored: z.ZodBoolean;
227
+ spend: z.ZodObject<{
228
+ modelCalls: z.ZodNumber;
229
+ screens: z.ZodNumber;
230
+ promptTokens: z.ZodNumber;
231
+ completionTokens: z.ZodNumber;
232
+ }, z.core.$strict>;
233
+ gate2Dir: z.ZodString;
234
+ }, z.core.$strict>, z.ZodObject<{
235
+ kind: z.ZodLiteral<"error">;
236
+ message: z.ZodLiteral<"ablation-error">;
237
+ }, z.core.$strict>, z.ZodObject<{
238
+ kind: z.ZodLiteral<"unavailable">;
239
+ reason: z.ZodLiteral<"ablation-unavailable">;
240
+ }, z.core.$strict>, z.ZodObject<{
241
+ kind: z.ZodLiteral<"no-eval-root">;
242
+ }, z.core.$strict>]>;
243
+ }, z.core.$strict>;
244
+ type CompletedCanaryAblation = z.infer<typeof completionSchema>;
245
+
246
+ interface CanaryAblationInput {
247
+ handoff: CompletedRunHandoff;
248
+ runDir: string;
249
+ candidateId: string;
250
+ runId: string;
251
+ testId: string;
252
+ testIndex: number;
253
+ stepIndex: number;
254
+ attributedObservationId: string;
255
+ }
256
+ type CanaryAblationResult = {
257
+ kind: 'ablated';
258
+ crossCheck: CrossCheck;
259
+ corpusDigest: string;
260
+ k: number;
261
+ baseSeed: number;
262
+ seeds: number[];
263
+ seedHonored: boolean;
264
+ spend: {
265
+ modelCalls: number;
266
+ screens: number;
267
+ promptTokens: number;
268
+ completionTokens: number;
269
+ };
270
+ gate2Dir: string;
271
+ } | {
272
+ kind: 'no-eval-root';
273
+ } | {
274
+ kind: 'unavailable';
275
+ reason: string;
276
+ } | {
277
+ kind: 'error';
278
+ message: string;
279
+ };
280
+ type CanaryCorpusDigestResult = {
281
+ kind: 'digest';
282
+ corpusDigest: string;
283
+ } | {
284
+ kind: 'no-eval-root';
285
+ } | {
286
+ kind: 'unavailable';
287
+ reason: string;
288
+ };
289
+ interface CanaryAblation {
290
+ isNovel(input: {
291
+ product: string;
292
+ stateDigest: string;
293
+ }): Promise<boolean>;
294
+ stage(input: CanaryAblationInput & {
295
+ product: string;
296
+ stateDigest: string;
297
+ benefit: Benefit;
298
+ }): Promise<{
299
+ kind: 'staged' | 'already-staged' | 'not-novel' | 'no-eval-root' | 'target-unavailable' | 'write-conflict';
300
+ } | {
301
+ kind: 'unavailable';
302
+ reason: string;
303
+ } | {
304
+ kind: 'error';
305
+ message: string;
306
+ }>;
307
+ ablate(input: CanaryAblationInput): Promise<CanaryAblationResult>;
308
+ corpusDigest(): Promise<CanaryCorpusDigestResult>;
309
+ resolveCaps?(): Promise<{
310
+ maxAblations: number;
311
+ source: string;
312
+ }>;
313
+ }
314
+ interface CanaryCreditCounters {
315
+ credited: number;
316
+ rideAlongUncredited: number;
317
+ ablationCapExceeded: number;
318
+ ablationHeld: number;
319
+ ablationInconclusive: number;
320
+ ablationError: number;
321
+ rerunExcluded: number;
322
+ nonSignal: Record<NonSignalClass, number>;
323
+ staged: number;
324
+ stagingRefused: number;
325
+ }
326
+ interface CanaryCreditInput {
327
+ completedAblations: readonly CompletedCanaryAblation[];
328
+ saveCompletedAblation: (entry: CompletedCanaryAblation) => void;
329
+ maxAblations: number;
330
+ knowledgeRoot: string;
331
+ readBundleStateDigests: LocalCurationCapabilities['readCanonicalBundleStateDigests'];
332
+ attributions: readonly ProjectedCanaryAttribution[];
333
+ ablation?: CanaryAblation;
334
+ handoff: CompletedRunHandoff;
335
+ runDir: string;
336
+ observationIdOf: (outcome: CanaryOutcome) => string;
337
+ }
338
+
339
+ type NonSignalClass = 'environment' | 'timeout' | 'flake';
340
+ interface ProjectCanaryOutcomesInput {
341
+ handoff: CompletedRunHandoff;
342
+ /**
343
+ * The run's persisted analysis artifact when one exists (389-06, D-10). Data, not a port:
344
+ * `emitCanaryOutcomes` loads it from the fold's own `runDir`; the pure projection takes it
345
+ * as an argument so identical (handoff, artifact) bytes keep yielding identical outcomes.
346
+ */
347
+ artifact?: RunAnalysisArtifact;
348
+ /**
349
+ * The circularity arbiter's per-record rows (422-16, D-20), when the handoff's own published
350
+ * sidecar is not the copy the caller holds — `emitCanaryOutcomes` supplies this only for the
351
+ * durable-recovery case below. ABSENT here means "read `handoff.misdirectionEvidence`", which
352
+ * is itself absent on every run written before 422. Data, never a port.
353
+ */
354
+ misdirectionEvidence?: readonly MisdirectionRecordResult[];
355
+ resolveCandidateAnchors: (id: RecordId) => CandidateAnchorResolution;
356
+ }
357
+ type CandidateAnchorResolution = {
358
+ kind: 'resolved';
359
+ anchors: Anchor[];
360
+ } | {
361
+ kind: 'unresolved';
362
+ reason: 'not-found' | 'invalid' | 'lookup-error';
363
+ detail?: string;
364
+ };
365
+ type EmitCanaryOutcomeResult = {
366
+ kind: 'non-signal';
367
+ class: NonSignalClass;
368
+ } | {
369
+ kind: 'emitted';
370
+ } | {
371
+ kind: 'refused';
372
+ reason: 'emit-error';
373
+ } | {
374
+ kind: 'unresolved';
375
+ reason: Extract<CandidateAnchorResolution, {
376
+ kind: 'unresolved';
377
+ }>['reason'];
378
+ detail?: string;
379
+ };
380
+ type EmitCanaryOutcomeDetail = {
381
+ staged?: never;
382
+ benefit?: never;
383
+ crossCheck?: never;
384
+ corpusDigestMoved?: never;
385
+ misdirectionFlipped?: never;
386
+ candidateId?: never;
387
+ stepIndex?: never;
388
+ testId: string;
389
+ testIndex: number;
390
+ outcome: {
391
+ kind: 'rerun-excluded';
392
+ };
393
+ } | {
394
+ staged?: true;
395
+ benefit: CanaryOutcome['benefit'];
396
+ crossCheck?: CrossCheck;
397
+ corpusDigestMoved?: true;
398
+ /**
399
+ * 422-16 (D-20): this row reached the harmed-outcome writer on the arbiter's CAUSAL evidence,
400
+ * not on anchor contact. Distinct from `crossCheck` beside it, which is the Gate-2 ABLATION's
401
+ * answer about a positive — two different cross-checks, and conflating them would read a
402
+ * removal that proved harm as a removal that proved benefit.
403
+ */
404
+ misdirectionFlipped?: true;
405
+ candidateId: RecordId;
406
+ testId: string;
407
+ testIndex: number;
408
+ stepIndex: number;
409
+ outcome: EmitCanaryOutcomeResult;
410
+ };
411
+ interface EmitCanaryOutcomeReport {
412
+ canary: CanaryCreditCounters;
413
+ emitted: number;
414
+ refused: number;
415
+ unresolved: number;
416
+ /** D-12: per-source split of the planSource attribution stream; totals cover BOTH sources. */
417
+ procedure: {
418
+ emitted: number;
419
+ refused: number;
420
+ unresolved: number;
421
+ };
422
+ /**
423
+ * 422-16 (D-20): rows the arbiter's FLIPPED evidence sent to the harmed-outcome writer carry
424
+ * `misdirectionFlipped` on their DETAIL, and are counted from there rather than from a
425
+ * fourth total here. Two reasons, both deliberate. The consequence is the ordinary harmed
426
+ * outcome, so a parallel total would invite a reader to add it to `emitted`. And a required
427
+ * new member on this report would break every existing `emitCanaryOutcomes` double in the
428
+ * tree — the tail reads the rows it already renders, exactly as `candidateSummaryLine` does.
429
+ */
430
+ details: EmitCanaryOutcomeDetail[];
431
+ }
432
+ interface EmitCanaryOutcomesOptions {
433
+ credit?: Partial<Pick<CanaryCreditInput, 'ablation' | 'maxAblations' | 'readBundleStateDigests'>>;
434
+ handoff: CompletedRunHandoff;
435
+ /** Absolute agent-qa-knowledge root used for store lookup and durable queue emission. */
436
+ knowledgeRoot: string;
437
+ /** Absolute completed-run directory retained to keep the two post-verdict emit seams aligned. */
438
+ runDir: string;
439
+ emit: (outcome: CanaryOutcome) => void;
440
+ }
441
+ type AttributionSource = 'served-brief' | 'procedure-replay';
442
+ type ProjectedCanaryAttribution = {
443
+ kind: 'rerun-excluded';
444
+ testId: string;
445
+ testIndex: number;
446
+ } | {
447
+ kind: 'non-signal';
448
+ source: AttributionSource;
449
+ class: NonSignalClass;
450
+ candidateId: RecordId;
451
+ testId: string;
452
+ testIndex: number;
453
+ stepIndex: number;
454
+ } | {
455
+ kind: 'outcome';
456
+ source: AttributionSource;
457
+ outcome: CanaryOutcome;
458
+ /** 422-16 (D-20): selected by the arbiter's flipped row rather than by anchor contact. */
459
+ misdirectionFlipped?: true;
460
+ } | {
461
+ kind: 'unresolved';
462
+ source: AttributionSource;
463
+ candidateId: RecordId;
464
+ testId: string;
465
+ testIndex: number;
466
+ stepIndex: number;
467
+ reason: Extract<CandidateAnchorResolution, {
468
+ kind: 'unresolved';
469
+ }>['reason'];
470
+ detail?: string;
471
+ };
472
+ /** Pure Pin-1 projection: identical handoff + canonical snapshot yields identical outcome bytes. */
473
+ declare function projectCanaryOutcomes(input: ProjectCanaryOutcomesInput): CanaryOutcome[];
474
+ /** Resolves anchors read-only, credits evidence, then emits every attribution in isolation. */
475
+ declare function emitCanaryOutcomes(opts: EmitCanaryOutcomesOptions): Promise<EmitCanaryOutcomeReport>;
476
+
477
+ interface EmitCurationCandidatesOptions {
478
+ handoff: CompletedRunHandoff;
479
+ /** Absolute completed-run directory containing runner-local's blobs/ subdirectory. */
480
+ runDir: string;
481
+ /** Absolute agent-qa-knowledge root that owns the durable queue. */
482
+ knowledgeRoot: string;
483
+ mask?: MaskFn;
484
+ }
485
+ type EmitCandidateOutcome = EnqueueCurationCandidateProjectionOutcome | {
486
+ kind: 'refused';
487
+ reason: 'artifact-io-error';
488
+ };
489
+ interface EmitCandidateDetail {
490
+ candidateId?: string;
491
+ /** The projecting candidate's kind: per-kind outcome attribution from the report alone (381-50). */
492
+ kind: CurationCandidateProjection['kind'];
493
+ outcome: EmitCandidateOutcome;
494
+ }
495
+ interface EmitReport {
496
+ enqueued: number;
497
+ coalesced: number;
498
+ capExceeded: number;
499
+ refused: number;
500
+ details: EmitCandidateDetail[];
501
+ }
502
+ /**
503
+ * Resolves runner-local artifact blobs and synchronously publishes each candidate in isolation.
504
+ * Queue publication owns temp-write, fsync, cap enforcement, and atomic rename semantics.
505
+ */
506
+ declare function emitCurationCandidates(opts: EmitCurationCandidatesOptions): EmitReport;
507
+
508
+ type ResolveTrustPriors = () => TrustPriorTable | undefined;
509
+
510
+ type RunCurationContext = {
511
+ readonly mask: MaskFn;
512
+ readonly knowledgeRoot: string;
513
+ } | {
514
+ readonly handBack: string;
515
+ };
516
+ declare function resolveRunCurationContext(input: {
517
+ readonly agentQaRoot: string;
518
+ readonly runId: string;
519
+ readonly drainerKnowledgeRoot: string;
520
+ }): Promise<RunCurationContext>;
521
+
522
+ declare const CURATION_TAIL_RECEIPT_FILE = "curation-tail-receipt.json";
523
+ /** The receipt's key set and stamp must match exactly; anything else refolds the run once. */
524
+ declare const CURATION_TAIL_RECEIPT_SCHEMA_VERSION = 2;
525
+ interface CurationTailReceipt {
526
+ /** Fixed successful membership; absent on handed-off receipts and when no validated snapshot was recorded. */
527
+ snapshot?: CurationSnapshot;
528
+ /** Counts recorded beside the snapshot. */
529
+ curated?: number;
530
+ deprecated?: number;
531
+ canary: CanaryCreditCounters;
532
+ schemaVersion: typeof CURATION_TAIL_RECEIPT_SCHEMA_VERSION;
533
+ runId: string;
534
+ handoffDigest: string;
535
+ /**
536
+ * Run-outcome projection component: a version bump refolds already-receipted runs.
537
+ *
538
+ * This field alone only re-RUNS them (393 CR-02). The per-run fold behind it short-circuits on
539
+ * the durable applied-runs marker, so what actually re-derives the rows at a new version is the
540
+ * once-per-sweep `reconcileRunOutcomesProjection` below; the receipts then rewrite here at the
541
+ * new version through this same sweep.
542
+ */
543
+ projectionVersion: number;
544
+ drainKind: 'drained' | 'handed-off';
545
+ /** The standing writer that accepted the handoff; null when this run drained inline. */
546
+ handoffHolderPid: number | null;
547
+ /** Stable run-side provenance, ordered before the receipt bytes are committed. */
548
+ candidateIds: string[];
549
+ }
550
+ type CurationTailModelDeps = Pick<BuildCuratorAuthorDependencies, 'profile' | 'auth'>;
551
+ interface CurationTailLogger {
552
+ warn(message: string): void;
553
+ /** 381-50: carries the per-run candidate summary line — diagnosis, never control flow. */
554
+ info?(message: string): void;
555
+ }
556
+ interface CurationTailOperations {
557
+ /** 2026-09-07, 418 D-01: eval-composing hosts bind this in Plan 05; BYOM omits it. */
558
+ canaryAblation?: CanaryAblation;
559
+ loadCompletedRunHandoff: typeof loadCompletedRunHandoff;
560
+ resolveKnowledgeRoot: typeof resolveKnowledgeRoot;
561
+ runSafetyScope: typeof runSafetyScope;
562
+ emitCurationCandidates: typeof emitCurationCandidates;
563
+ openKnowledgeReader(config: OpenServingKnowledgeReaderConfig): KnowledgeHintReader;
564
+ emitCanaryOutcomes: typeof emitCanaryOutcomes;
565
+ foldRunOutcomes: LocalCurationCapabilities['foldRunOutcomes'];
566
+ reconcileRunOutcomesProjection: LocalCurationCapabilities['reconcileRunOutcomesProjection'];
567
+ acquireWriterLease: LocalCurationCapabilities['maintenance']['acquireWriterLease'];
568
+ releaseLease: LocalCurationCapabilities['maintenance']['releaseLease'];
569
+ buildCuratorAuthor: typeof buildCuratorAuthor;
570
+ drainQueue: LocalCurationCapabilities['drainQueue'];
571
+ listCompletedRunDirs(agentQaRoot: string): string[];
572
+ readCurationTailReceipt(runDir: string): CurationTailReceipt | undefined;
573
+ writeCurationTailReceipt(runDir: string, receipt: CurationTailReceipt): void;
574
+ nudgeDaemon: typeof nudgeDaemon;
575
+ resolveRunCurationContext: typeof resolveRunCurationContext;
576
+ }
577
+ /**
578
+ * Writer authority the CALLER already holds (388-02, Pitfall 7).
579
+ *
580
+ * The fold acquires no lease of its own when this is present. That is not an optimisation: a
581
+ * process holding the writer lease that calls `acquireWriterLease` again is told `held` — by
582
+ * itself — and would hand its own work off to nobody (the 387-08 D-18 shape).
583
+ */
584
+ interface CurationWriterAuthority {
585
+ leaseId: string;
586
+ epoch: number;
587
+ }
588
+ interface RunCurationTailOptions {
589
+ resolveTrustPriors?: ResolveTrustPriors;
590
+ maxAblations?: number;
591
+ /** Built once by the caller; synchronous per candidate inside writer authority. */
592
+ resolvePromotionPosture?: ResolvePromotionPosture;
593
+ /** 418 D-13: reuse prepared identity but bind each swept handoff's own ordinal. */
594
+ resolvePromotionPostureForRun?: (buildOrdinal: number, scope?: ScopeKey) => Promise<ResolvePromotionPosture | undefined>;
595
+ runId: string;
596
+ /** Absolute `.agent-qa` runtime root the run handoff lives under. */
597
+ agentQaRoot: string;
598
+ knowledgeConfig: {
599
+ dir: string;
600
+ };
601
+ configDir: string;
602
+ mask?: MaskFn;
603
+ /** The exact already-resolved planner profile and auth used by the run. */
604
+ modelDeps?: CurationTailModelDeps;
605
+ signal?: AbortSignal;
606
+ log: CurationTailLogger;
607
+ /** 388-02: the sweep runs under the post-run tail's held lease when there is one. */
608
+ authority?: CurationWriterAuthority;
609
+ /** 388-02 (D-03): absolute wall-clock stop point shared with the one post-run budget. */
610
+ deadlineMs?: number;
611
+ /** Focused test seam. Production callers omit it. */
612
+ operations?: Partial<CurationTailOperations>;
613
+ }
614
+ declare function curationTailHandoffDigest(handoff: unknown): string;
615
+ /**
616
+ * Run the post-verdict curation fold. The caller fixes its exit code before awaiting this function;
617
+ * every failure is logged here and intentionally resolves so learning can never change the verdict.
618
+ */
619
+ declare function runCurationTail(opts: RunCurationTailOptions): Promise<void>;
620
+ interface RunVerdictFirstCurationTailOptions {
621
+ /** The complete verdict-derived process code, fixed before the tail begins. */
622
+ exitCode: number;
623
+ detach: boolean;
624
+ /**
625
+ * The durable hand-off, run BEFORE the detach short-circuit (388-02, D-03 / Pitfall 3).
626
+ *
627
+ * `--detach` means "do not spend this process on the work", not "do not record that the
628
+ * work exists". The enqueue is the record; the drain is the spending. Putting it inside
629
+ * `runTail` would have made a detached run the one posture that silently loses its
630
+ * analysis, which is the opposite of what detaching asks for.
631
+ */
632
+ enqueue?(): Promise<void>;
633
+ runTail(): Promise<void>;
634
+ /**
635
+ * The user-facing close-out, run on BOTH paths (388-04, D-08).
636
+ *
637
+ * The detach short-circuit below returns before `runTail`, and the one posture that most
638
+ * needs a line about work left queued is exactly that one — so this cannot live inside the
639
+ * tail. It is told which gate was applied rather than inferring it, because the gate is
640
+ * this function's own knowledge and nothing downstream can see it.
641
+ */
642
+ finish?(input: {
643
+ detached: boolean;
644
+ }): Promise<void>;
645
+ /** Defensive call-site boundary; runCurationTail itself already never rejects. */
646
+ onUnexpectedError?(error: unknown): void;
647
+ }
648
+ /** Await the optional tail without permitting it to replace the already-fixed verdict code. */
649
+ declare function runVerdictFirstCurationTail(opts: RunVerdictFirstCurationTailOptions): Promise<number>;
37
650
 
38
651
  declare const VERSION: string;
39
652
 
40
- export { type ResolvedTarget, type TargetSummary, VERSION, deepMerge, formatConfigDebug, listTargets, loadConfigFile, loadEnvOverrides, loadRequiredConfigFile, mergeConfigs, mergeUseBlocks, mergeWithTestConfig, purgeAll, purgeTest, resolveConfig, resolveTarget };
653
+ export { type BuildCuratorAuthorDependencies, CURATION_TAIL_RECEIPT_FILE, CURATION_TAIL_RECEIPT_SCHEMA_VERSION, CURATOR_MAX_NEIGHBORS, CURATOR_RETRY_CAP, ConfigFileNotFoundError, ConfigFileNotYamlError, type CurationTailLogger, type CurationTailModelDeps, type CurationTailOperations, type CurationTailReceipt, type EmitCanaryOutcomeDetail, type EmitCanaryOutcomeReport, type EmitCanaryOutcomeResult, type EmitCanaryOutcomesOptions, type ProjectCanaryOutcomesInput, type ResolvedTarget, type RunCurationTailOptions, type RunVerdictFirstCurationTailOptions, type TargetSummary, VERSION, buildCuratorAuthor, curationTailHandoffDigest, deepMerge, emitCanaryOutcomes, formatConfigDebug, listTargets, loadConfigFile, loadConfigPlugins, loadEnvOverrides, loadRequiredConfigFile, mergeConfigs, mergeUseBlocks, mergeWithTestConfig, normalizeConfigForValidation, projectCanaryOutcomes, purgeAll, purgeTest, readAnalyticsPrivacyDeclaration, resolveConfig, resolveConfiguredKnowledgeRoot, resolveRawConfigKnowledgeRoot, resolveTarget, runCurationTail, runVerdictFirstCurationTail };