import type { OracleIsolationVerdict } from "./oracle-isolation.js"; import type { SkillManifest } from "./types.js"; /** * The terminal of {@link decideAutoPromote}. * - `"auto_promote"` — the ONLY exit that skips human review. Reachable only when all four prior guards * (tripwire / irreversible / statistics / concurrence) clear AND all four gates are present+passing. * - `"candidate_only"` — surface the artifact as a candidate; the SOLE shortfall is the oracle isolation class * being downgraded to `in_process_probe_only` (oracle-isolation.ts:34) with everything else passing. * - `"needs_human"` — fail-closed default: escalate to a human (any guard tripped, any other gate failing). */ export type AutoPromoteTerminal = "auto_promote" | "candidate_only" | "needs_human"; /** * Gate-2 + Gate-3 lineage-bound handle (裁决净增③ / P0#3 / P1 lineage-bind). `gate2Verdict` and `manifest` * MUST come from the SAME atomic `appendPromotable` write (memory.ts:295) — this struct bundles them into one * handle so the decision can never pair a verdict with a divergent manifest ("assume two booleans are * co-sourced" is the bug this prevents). `lineageId` keys the active-skill-scope frame (active-skill-scope.ts). */ export interface PromotedRecord { /** Stable lineage id of the promoted artifact; the Gate-3 scope key. */ lineageId: string; /** * Gate-2 chokepoint result: the promotable write landed WITH a positive utility verdict (memory.ts:240), * or it was absent/ungated. Only `"present_positive"` is gate-passing; `"absent_or_ungated"` ⇒ needs_human. */ gate2Verdict: "present_positive" | "absent_or_ungated"; /** Gate-3 capability manifest — co-located with `gate2Verdict` in the same store record, never diverging. */ manifest: SkillManifest; /** * Gate-3 scope is armed AND has propagated across the subagent boundary (design/79 §4, design/77 §3/§7). * `true` ONLY when either (a) the manifest lists NO subagent-spawn tool, or (b) its scope has propagated to * spawned children. The skill→subagent manifest-scope propagation just landed in this branch (commit * `04f5104`); for an artifact that lists a subagent-spawn tool whose scope has NOT propagated, this is * `false` → the decision falls to `needs_human` (裁决净增⑤ / P1). A bare in-process `gate4Armed`-style * boolean is intentionally NOT enough here: the propagation hole is encoded structurally into this flag. */ gate3Scoped: boolean; } /** * Inputs to {@link decideAutoPromote}. Every field is a PRESENCE/boolean/set the profile has already computed — * the core fold inspects no numeric threshold, no stats shape, no taxonomy. The caller (a profile-promotion job) * has already RE-asserted Gate-1 (`assertOracleIsolation`), run the exec-gate, read the appended Gate-2 verdict, * and armed the Gate-4 seam BEFORE building this struct; this function only READS those outputs. */ export interface AutoPromoteInputs { /** Gate-1 verdict (oracle-isolation.ts:37); the caller re-asserted it before calling this function. */ oracle: OracleIsolationVerdict; /** Gate-2 + Gate-3 lineage-bound single handle (净增③). */ record: PromotedRecord; /** Gate-4: the irreversibility tighten is in place (or the artifact touches no irreversible tool). */ gate4Armed: boolean; /** * §6 permanent human door (净增①): the artifact touches at least one irreversible TYPED tool. When true the * decision is ALWAYS `needs_human`, even if everything else would pass — because G4 is pure in-process * gate-PRESENCE (2-of-3 asymmetry) and `gate4Armed` alone ("the gate is wired") is not trustworthy for an * irreversible action. */ touchesIrreversibleTool: boolean; /** * The profile's statistics RAN for this run (it produced a verdict). Core requires PRESENCE only — the * statistical THRESHOLDS (train/val/test split, canary, min-samples, cost-per-truly-correct) are profile. */ statisticsPresent: boolean; /** * The KIND labels of the independent mechanical signals that concur. Core enforces `size >= 2` DISTINCT * kinds (净增②) — feeding the SAME kind twice still yields size 1 (it's a Set keyed by kind). WHICH kinds * count, and whether they are truly de-correlated, are profile concerns (§7 honest residual). */ concurringSignals: ReadonlySet; /** Tripwire result — ESCALATE-ONLY (§5 ③); when `escalate` it always wins and is never cleared. */ tripwire: TripwireResult; } /** * The fail-closed PRESENCE AND-chain (design/79 §1 §2). Any missing/failing gate ⇒ NEVER `auto_promote`; it * degrades to `needs_human` (or, for the single oracle-isolation-class shortfall, `candidate_only`). This * function computes NO numeric threshold — only PRESENCE/boolean/set-size checks. * * **Guard order (load-bearing):** * 1. `tripwire.escalate` → needs_human (tripwire always wins, never clears — §5 ③) * 2. `touchesIrreversibleTool` → needs_human (§6 permanent human door — 净增①; even if all else passes) * 3. `!statisticsPresent` → needs_human (statistics must have RAN — core-required; thresholds are profile) * 4. `concurringSignals.size < 2` → needs_human (≥2 DISTINCT KIND mechanical signals concur — 净增②) * 5. the four-gate present-passing check; if not all pass, the SOLE-shortfall arm: * `in_process_probe_only` oracle (and every other gate passing) → `candidate_only`; otherwise needs_human. * 6. all four gates present+passing (with guards clear) → `auto_promote` (the only auto exit). */ export declare function decideAutoPromote(i: AutoPromoteInputs): AutoPromoteTerminal; /** * The result of {@link deriveTripwire}: an ESCALATE-ONLY advisory. `escalate` can only ever be set to `true` * by a scan; a clean scan contributes nothing and NEVER clears another scan's escalation. `reasons` carries * the matched tokens / drifted symbols for observability. */ export interface TripwireResult { escalate: boolean; reasons: string[]; } /** * The CORE-FROZEN denylist floor (design/79 §3 净增④ / P0#4): a safety invariant the profile MAY NOT remove. * `deriveTripwire`'s `profileTokens` may only UNION (add) to this — never subtract — resolving the tension * between "the token set can rot (new exfil primitives appear)" and "constitution-minimal". These are RAW-BYTE * substrings scanned against the artifact's raw bytes INCLUDING its markdown bytes (extractSymbols skips * markdown, so the byte scan is what covers prose/fenced-code that names an egress/exec primitive). */ export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet; /** * Build a tripwire verdict over a promoted artifact's text. **ESCALATE-ONLY** (design/79 §3,守 design/76 * §5.1 r3): it can ONLY ever raise `escalate:true`; a clean artifact yields `{escalate:false, reasons:[]}` and * NEVER clears anything — the REAL containment is Gate-3 runtime deny-narrowing (exhaustive at call time), not * this advisory. Two mechanical scans, both escalate-only: * * 1. **symbol-mention drift** via {@link extractSymbols} (repo-map.ts). extractSymbols is a v1 heuristic: * column-0 / source-only, it skips indented lines and is structurally INCAPABLE of being a containment * proof (it can't enumerate everything the artifact can call). So it is used ONLY to ESCALATE — if a * surfaced top-level symbol names a tool/capability that is NOT in `declared.allowTools`, escalate. * 2. **raw-byte denylist** over the artifact's RAW bytes INCLUDING markdown bytes — {@link FROZEN_DENYLIST_FLOOR} * UNIONed with `profileTokens` (add-only). A substring hit → escalate, with the matched token in `reasons`. * * **Fail-closed:** if EITHER scan THROWS on real content, return `{escalate:true, ...}` — a scan failure must * NOT silently become "no escalation". (This is disjoint from the composition-layer "tripwire DOWN ⇒ zero * contribution, never block" case, which is the caller's concern, not a throw inside the scan.) * * @param artifactText the artifact's raw text (markdown body included). * @param declared the artifact's declared manifest — its `allowTools` is the symbol-drift baseline. * @param profileTokens optional ADD-ONLY extra denylist tokens (UNIONed onto the frozen floor; cannot remove). */ export declare function deriveTripwire(artifactText: string, declared: SkillManifest, profileTokens?: ReadonlySet): TripwireResult; //# sourceMappingURL=auto-promote.d.ts.map