@sema-agent/core 5.32.0 → 5.33.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/agents/cascade.d.ts +49 -1
  3. package/dist/agents/cascade.js +2 -2
  4. package/dist/agents/verify.d.ts +70 -4
  5. package/dist/agents/verify.js +62 -16
  6. package/dist/core/checkpoint-store.d.ts +95 -0
  7. package/dist/core/checkpoint-store.js +40 -0
  8. package/dist/core/hooks.d.ts +14 -6
  9. package/dist/core/hooks.js +14 -3
  10. package/dist/core/memory-engine/file-backend.d.ts +172 -22
  11. package/dist/core/memory-engine/file-backend.js +877 -79
  12. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  13. package/dist/core/runner/assemble-result.d.ts +7 -0
  14. package/dist/core/runner/assemble-result.js +1 -1
  15. package/dist/core/runner/prepare-acquire-reconcile.d.ts +72 -0
  16. package/dist/core/runner/prepare-acquire-reconcile.js +126 -0
  17. package/dist/core/runner/prepare-config-doors.d.ts +140 -0
  18. package/dist/core/runner/prepare-config-doors.js +250 -0
  19. package/dist/core/runner/prepare-safety-scan.d.ts +53 -0
  20. package/dist/core/runner/prepare-safety-scan.js +80 -0
  21. package/dist/core/runner/prepare-task.d.ts +27 -81
  22. package/dist/core/runner/prepare-task.js +83 -586
  23. package/dist/core/runner/prepare-workspace-restore.d.ts +102 -0
  24. package/dist/core/runner/prepare-workspace-restore.js +144 -0
  25. package/dist/core/runner/runtask.js +8 -2
  26. package/dist/core/tool-policy.d.ts +25 -0
  27. package/dist/core/types.d.ts +140 -13
  28. package/dist/index.d.ts +4 -3
  29. package/dist/index.js +1 -1
  30. package/dist/orchestration/workflow-governance.d.ts +6 -4
  31. package/dist/tools/fs/bash-readonly-classifier.d.ts +9 -3
  32. package/dist/tools/fs/bash-readonly-classifier.js +4 -1
  33. package/dist/tools/fs/fs-bash.d.ts +19 -3
  34. package/dist/tools/fs/fs-bash.js +26 -1
  35. package/dist/tools/fs/index.d.ts +20 -4
  36. package/dist/tools/fs/index.js +4 -1
  37. package/dist/tools/fs/read-deny.d.ts +66 -8
  38. package/dist/tools/fs/read-deny.js +75 -39
  39. package/dist/tools/fs/read-face.d.ts +3 -2
  40. package/dist/tools/fs/search.js +2 -0
  41. package/package.json +1 -1
@@ -243,6 +243,39 @@ export async function memoryBackendContract(hooks) {
243
243
  assert.strictEqual(after.frontmatter.trust, "untrusted");
244
244
  assert.strictEqual(after.rev, next.rev, "provenance participates in the rev (computeEntryRev closure)");
245
245
  });
246
+ defer("projection authority: getByIds carries the OWNING scope; other scopes never list the entry", async () => {
247
+ const b = await hooks.make();
248
+ const e = entry("id-auth-0001", "s1", "authored", "authored body", { name: "Authored" });
249
+ await b.applyPatches([{ op: "add", id: e.id, entry: e }]);
250
+ const got = await b.getByIds([e.id]);
251
+ assert.strictEqual(got.length, 1);
252
+ assert.strictEqual(got[0]?.scope, "s1", "getByIds must carry the owning scope");
253
+ assert.deepStrictEqual((await b.listHeaders(["s2"])).map((h) => h.id), [], "a foreign scope never lists it");
254
+ });
255
+ defer("move immediacy: a cross-scope update moves the WHOLE projection in one transaction (no account lag)", async () => {
256
+ const b = await hooks.make();
257
+ const v1 = entry("id-mvnow-001", "s1", "prompt", "in s1");
258
+ await b.applyPatches([{ op: "add", id: v1.id, entry: v1 }]);
259
+ const v2 = entry("id-mvnow-001", "s2", "prompt", "in s1");
260
+ const rep = await b.applyPatches([{ op: "update", id: v1.id, entry: v2, baseRev: v1.rev }]);
261
+ assert.strictEqual(rep.conflicts.length, 0);
262
+ assert.strictEqual((await b.listHeaders(["s1"])).length, 0, "the old scope loses it in the same transaction");
263
+ assert.deepStrictEqual((await b.listHeaders(["s2"])).map((h) => h.id), [v1.id]);
264
+ const got = await b.getByIds([v1.id]);
265
+ assert.strictEqual(got.length, 1, "one id ⇒ one entry, mid-move states never observable");
266
+ assert.strictEqual(got[0]?.scope, "s2");
267
+ });
268
+ defer("landing honesty: a suffixed slug collision reports the slug that ACTUALLY landed, and reads agree with it", async () => {
269
+ const b = await hooks.make();
270
+ await b.applyPatches([{ op: "add", id: "id-land-0001", entry: entry("id-land-0001", "s1", "spot", "first") }]);
271
+ const rep = await b.applyPatches([{ op: "add", id: "id-land-0002", entry: entry("id-land-0002", "s1", "spot", "second") }]);
272
+ const applied = rep.applied.find((a) => a.id === "id-land-0002");
273
+ assert.ok(applied !== undefined, "the collided add applies (suffixed)");
274
+ const header = (await b.listHeaders(["s1"])).find((h) => h.id === "id-land-0002");
275
+ assert.ok(header !== undefined);
276
+ assert.strictEqual(applied.slug, header.slug, "applied[].slug must be the landing slug the reads serve — never the requested one");
277
+ assert.notStrictEqual(header.slug, "spot", "the collision really landed on a suffix");
278
+ });
246
279
  defer("search: tied scores order deterministically by id (no arrival-order accident)", async () => {
247
280
  const b = await hooks.make();
248
281
  await b.applyPatches([
@@ -121,6 +121,13 @@ export interface ResultFlags {
121
121
  * a refused `suspendVM` still ends the task exactly the way it did before — this only stops the eleven
122
122
  * distinct codes from arriving as one anonymous `limits.max_turns_exceeded`. Empty/absent ⇒ the field is omitted. */
123
123
  remoteEnvFailures?: TaskResult["remoteEnvFailures"];
124
+ /** #240 (design/199 v1.1) — the run's effective read posture (resolved face + normalized deny
125
+ * additions), echoed on `TaskResult.effectiveReadFace` / `.effectiveReadDenyPatterns`. Pure
126
+ * pass-through (assembly neither adds nor filters) and INDEPENDENT of the status/errorCode chain:
127
+ * the posture is a fact about the leg that ran, whatever terminal it reached — a post-completion
128
+ * spawner folds it stricter-wins into follow-on legs; every other consumer may ignore it. */
129
+ effectiveReadFace?: TaskResult["effectiveReadFace"];
130
+ effectiveReadDenyPatterns?: TaskResult["effectiveReadDenyPatterns"];
124
131
  /** ruled 2026-08-04 — the usage-governance wait hint carried by the platform terminal the run adopted
125
132
  * (`undefined` for every other cause: an expiring environment has no return time to give, and a store
126
133
  * failure is not a window). Passed as data rather than read back off `threw` so the seat has a typed
@@ -159,5 +159,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
159
159
  void _internalCompaction;
160
160
  if (flags.unpricedSpend)
161
161
  delete publicStats.costMicroUsd;
162
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), stats: publicStats };
162
+ return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), stats: publicStats };
163
163
  }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * design/238 B-3 (P2 相位抽取) — prepareTask's session acquire+reconcile phase. Acquire the session
3
+ * and, when resuming, reconcile any interrupted (orphan) tool call before the turn. Both are retried
4
+ * together on a F2 optimistic-lock conflict: a concurrent writer on another instance can move the
5
+ * leaf during reconcile's append, and the fix can't be applied to a now-stale in-memory view — so we
6
+ * drop it and **re-wake**. Bounded; exhaustion → the conflict propagates and the task fails with
7
+ * `errorCode="conflict"` for the caller to retry. The retry loop's WHOLENESS is the T4 invariant
8
+ * (resume-at validation + rebranch + reconcile re-run as one unit against the fresh view) — pinned
9
+ * in `prepare-task-phase-pins`.
10
+ *
11
+ * Ownership (design/238 相 API 规则件): on SUCCESS the acquired session's ownership transfers whole
12
+ * to the caller (the driver's forgetOnThrow / the Runner's finish own the cleanup from then on);
13
+ * every failure path INSIDE this phase drops its own cached view first (`forget`, never `release` —
14
+ * audit B-17: release deletes the history on the default store), so a throw here never strands a
15
+ * cached view and never hands out a half-acquired resource.
16
+ *
17
+ * Throws pass through unchanged: `resume.session_not_found` (requireExisting fail-loud mapping),
18
+ * the `resume_at.*` target-validation refusals, and — after the bounded re-wake budget — the
19
+ * session conflict itself.
20
+ */
21
+ import { StoredSession } from "../session.js";
22
+ import type { AcquiredSession, SessionStore } from "../session.js";
23
+ import { type RecoveredOrphan } from "../session-reconcile.js";
24
+ import type { TaskSpec, ToolEffect } from "../types.js";
25
+ import type { PrepareResume } from "./prepare-task.js";
26
+ export interface PrepareAcquireReconcileInput {
27
+ /** borrowed-mutable (service port) — the deployment's session store. This phase's verbs on it are
28
+ * the acquire/forget half of the session resource protocol: `acquire` per attempt, `forget` to
29
+ * drop a stale cached view on a lost CAS and on the throw path (never `release` — B-17). */
30
+ sessions: SessionStore;
31
+ /** borrowed-readonly — the REBOUND spec's four session-addressing fields (窄输入: exactly what the
32
+ * slice reads; a new read here must widen this Pick). `sessionId`/`requireExistingSession` drive
33
+ * the acquire, `resumeAt`/`resumeAtMode` the §E18 rebranch validation. */
34
+ spec: Pick<TaskSpec, "sessionId" | "requireExistingSession" | "resumeAt" | "resumeAtMode">;
35
+ /** borrowed-readonly — the durable-resume seats this phase reads: `leafId` (rewind to the
36
+ * suspension point BEFORE reconcile) and `suspendedBatch` (wake-reconcile SKIPS those calls —
37
+ * they are resumed, not crash-interrupted). Absent for a fresh/non-durable run. */
38
+ resume: Pick<PrepareResume, "leafId" | "suspendedBatch"> | undefined;
39
+ /** borrowed-readonly here — the safety scan's effect registry (its mutation-owner table lives on
40
+ * {@link import("./prepare-safety-scan.js").PrepareSafetyScanResult}); reconcile classifies an
41
+ * orphaned call's retry-safety by it. */
42
+ toolEffects: Map<string, ToolEffect>;
43
+ }
44
+ /** The phase's outputs (design/238 相 API 规则件 four-class form). All five are fresh bindings —
45
+ * the driver destructures them into consts, so a consumer moved ahead of this call is a lexical
46
+ * error, which is the static guarantee the extraction keeps. */
47
+ export interface PrepareAcquireReconcileResult {
48
+ /** owned (resource handover) — the store's acquired session. Handover happens ONLY on return:
49
+ * every throw inside the phase already forgot its own cached view. */
50
+ acquired: AcquiredSession;
51
+ /** owned — the conflict-proxy-wrapped view over `acquired`'s storage (engine-internal: prepare
52
+ * always wraps storage in the concrete built-in — the epoch pin needs its
53
+ * append/getPromptEpoch). Aliases `acquired.session`'s storage through the proxy. */
54
+ session: StoredSession;
55
+ /** owned cell — flipped to `hit: true` by the conflict-detecting proxy installed inside `session`
56
+ * whenever a RUN-time write loses the optimistic lock (identity is contract: the proxy closes
57
+ * over this exact object; the run loop reads it into `errorCode="conflict"`). */
58
+ conflictRef: {
59
+ hit: boolean;
60
+ };
61
+ /** owned — scan-1/A5: what the wake/crash reconcile closed, handed to the run loop so it can mint
62
+ * the matching stream frames (see `Prepared.wakeRecovered`). Written by the acquire attempt that
63
+ * SUCCEEDS (a conflict-losing attempt re-wakes and re-derives the whole set from the fresh
64
+ * branch), and stays empty for a fresh session / a clean one with nothing to reconcile. */
65
+ wakeRecovered: RecoveredOrphan[];
66
+ /** owned — §E18 `"before"` mode: the validated target's parentId — the new conversation leaf, and
67
+ * the E19 file-anchor walk's starting point (consumed by the rewind-restore block after env
68
+ * setup). `null` outside `"before"` mode. */
69
+ resumeAtBeforeParentId: string | null;
70
+ }
71
+ /** The B-3 phase body — the acquire+reconcile slice, verbatim (see the module header for the contract). */
72
+ export declare function prepareAcquireReconcile(input: PrepareAcquireReconcileInput): Promise<PrepareAcquireReconcileResult>;
@@ -0,0 +1,126 @@
1
+ import { StoredSession, isSessionConflict } from "../session.js";
2
+ import { reconcileInterruptedSession } from "../session-reconcile.js";
3
+ const RECONCILE_MAX_RETRIES = 3;
4
+ function detectConflicts(storage, ref) {
5
+ const guard = (fn) => async (...args) => {
6
+ try {
7
+ return await fn(...args);
8
+ }
9
+ catch (err) {
10
+ if (isSessionConflict(err)) {
11
+ ref.hit = true;
12
+ }
13
+ throw err;
14
+ }
15
+ };
16
+ return new Proxy(storage, {
17
+ get(target, prop, receiver) {
18
+ const value = Reflect.get(target, prop, receiver);
19
+ if (typeof value !== "function") {
20
+ return value;
21
+ }
22
+ const bound = value.bind(target);
23
+ return prop === "appendEntry" || prop === "setLeafId"
24
+ ? guard(bound)
25
+ : bound;
26
+ },
27
+ });
28
+ }
29
+ export async function prepareAcquireReconcile(input) {
30
+ const { sessions, spec, resume, toolEffects } = input;
31
+ let acquired;
32
+ let session;
33
+ let conflictRef;
34
+ let wakeRecovered = [];
35
+ let resumeAtBeforeParentId = null;
36
+ for (let attempt = 0;; attempt++) {
37
+ try {
38
+ acquired = await sessions.acquire(spec.sessionId, spec.requireExistingSession ? { requireExisting: true } : undefined);
39
+ }
40
+ catch (err) {
41
+ if (spec.requireExistingSession && err?.code === "not_found") {
42
+ const e = new Error(`requireExistingSession: session "${spec.sessionId}" does not exist — refusing a silent fresh run (design/114 Phase3)`);
43
+ e.code = "resume.session_not_found";
44
+ throw e;
45
+ }
46
+ throw err;
47
+ }
48
+ conflictRef = { hit: false };
49
+ session = new StoredSession(detectConflicts(acquired.session.getStorage(), conflictRef));
50
+ if (!spec.sessionId) {
51
+ break;
52
+ }
53
+ try {
54
+ if (resume) {
55
+ await session.getStorage().setLeafId(resume.leafId);
56
+ }
57
+ else if (spec.resumeAt !== undefined) {
58
+ const entry = await session.getEntry(spec.resumeAt);
59
+ if (!entry) {
60
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" not found in session "${spec.sessionId}"`);
61
+ e.code = "resume_at.not_found";
62
+ throw e;
63
+ }
64
+ if (entry.type !== "message" && entry.type !== "custom_message") {
65
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" is a "${entry.type}" entry; resume-at requires a message boundary`);
66
+ e.code = "resume_at.not_a_message";
67
+ throw e;
68
+ }
69
+ if (entry.type === "message") {
70
+ const msg = entry.message;
71
+ if (msg?.role === "toolResult") {
72
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" is a tool-result (mid-turn); resume-at requires a settled turn boundary (a user message or a finished assistant turn)`);
73
+ e.code = "resume_at.not_a_message";
74
+ throw e;
75
+ }
76
+ if (msg?.role === "assistant" && Array.isArray(msg.content) && msg.content.some((p) => p?.type === "toolCall")) {
77
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" ends a turn mid-tool-call; resume-at requires a settled turn boundary`);
78
+ e.code = "resume_at.not_a_message";
79
+ throw e;
80
+ }
81
+ }
82
+ if (spec.resumeAtMode === "before") {
83
+ const role = entry.type === "message" ? entry.message.role : undefined;
84
+ if (role !== "user") {
85
+ const kind = entry.type === "message" ? `role "${String(role)}" message` : `"${entry.type}" entry`;
86
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" is a ${kind}; resumeAtMode "before" supports only USER-message targets (the exclusive-rewind shape: the removed message is a user message)`);
87
+ e.code = "resume_at.before_target_not_user";
88
+ throw e;
89
+ }
90
+ let ancestorId = entry.parentId;
91
+ let sawConversationAbove = false;
92
+ while (ancestorId !== null) {
93
+ const ancestor = await session.getEntry(ancestorId);
94
+ if (!ancestor)
95
+ break;
96
+ if (ancestor.type === "message" || ancestor.type === "custom_message") {
97
+ sawConversationAbove = true;
98
+ break;
99
+ }
100
+ ancestorId = ancestor.parentId;
101
+ }
102
+ if (!sawConversationAbove) {
103
+ const e = new Error(`resumeAt entry "${spec.resumeAt}" is the session's first message; resumeAtMode "before" cannot rewind past the session root — start a NEW session instead`);
104
+ e.code = "resume_at.before_root_unsupported";
105
+ throw e;
106
+ }
107
+ resumeAtBeforeParentId = entry.parentId;
108
+ }
109
+ await session.getStorage().setLeafId(spec.resumeAtMode === "before" ? entry.parentId : spec.resumeAt);
110
+ }
111
+ wakeRecovered = (await reconcileInterruptedSession(session, toolEffects, resume?.suspendedBatch)).recovered;
112
+ break;
113
+ }
114
+ catch (err) {
115
+ if (isSessionConflict(err) && attempt < RECONCILE_MAX_RETRIES) {
116
+ if (sessions.forget) {
117
+ await Promise.resolve(sessions.forget(spec.sessionId)).catch(() => undefined);
118
+ }
119
+ continue;
120
+ }
121
+ await Promise.resolve(sessions.forget ? sessions.forget(acquired.sessionId) : undefined).catch(() => undefined);
122
+ throw err;
123
+ }
124
+ }
125
+ return { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId };
126
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * design/238 B-1 (P0 相位抽取) — prepareTask's config-doors phase: the synchronous stretch from the
3
+ * tool-face snapshot freeze through model/thinking/compaction-model resolution. The inputs are
4
+ * exactly the fields of {@link PrepareConfigDoorsInput} (the original extraction slice — spec, deps,
5
+ * sessions, the resume/internals seats); the outputs are the fields of
6
+ * {@link PrepareConfigDoorsResult}, each one a value the slice defined and later phases consume —
7
+ * re-verified as the complete "defined inside, consumed outside" closure at extraction time.
8
+ *
9
+ * ⚠️ SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL — a hard constraint, not a style choice. This phase and
10
+ * the safety scan after it run BEFORE prepareTask's first await (`sessions.acquire`), so a caller
11
+ * has no microtask window in which to mutate `spec.tools`/the face arrays between the snapshots,
12
+ * the doors and the scan. An `async` wrapper — even with a zero-await body — would hand the caller
13
+ * exactly that window (the driver's `await` yields one microtask). Pinned by the
14
+ * `prepare-task-phase-pins` microtask-latch tests.
15
+ *
16
+ * Throws pass through unchanged: every `config.*` / `resume_at.*` refusal here is a DELIBERATE
17
+ * at-the-door rejection (design/99 §E18 — a throw before acquisition needs no cleanup), and the
18
+ * interaction-posture door's `deps.onError` disclosure (call order and payload) is part of the
19
+ * contract. The per-task agents rebind returns a REBOUND spec (shallow copy, only `tools`
20
+ * replaced) — the caller's object is never mutated; the safety scan must receive the rebound one.
21
+ */
22
+ import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
23
+ import type { Model } from "../../internal/llm.js";
24
+ import type { ThinkingLevel } from "../../internal/harness.js";
25
+ import { type LockedPreflight } from "../locked-config.js";
26
+ import { type ResolvedRole } from "../roles.js";
27
+ import type { SessionStore } from "../session.js";
28
+ import type { RunnerDeps, TaskLimits, TaskSpec } from "../types.js";
29
+ import { type UsageWindow } from "../usage-window-store.js";
30
+ import type { PrepareResume, RunInternals } from "./prepare-task.js";
31
+ export declare function limitConfigError(code: string, message: string): Error & {
32
+ code?: string;
33
+ };
34
+ /**
35
+ * design/164 — validate `TaskSpec.limits` at the door and return it unchanged.
36
+ *
37
+ * Two refusals, both fail-loud (the {@link resolveStallTimeoutMs} posture: a bound nobody can evaluate is
38
+ * not a bound, and folding it to a default would silently run the task under limits nobody chose):
39
+ * - **unknown key** ⇒ `config.limit_unknown_key`. TypeScript already rejects a stale key, but a wire /
40
+ * plain-JS caller does not go through TypeScript, and the retired names (`timeoutSec` above all) used
41
+ * to be honored — accepting them silently is how a caller keeps believing a limit is armed when the
42
+ * engine has stopped reading it.
43
+ * - **unevaluable numeric value** (non-number / non-finite / negative) ⇒ `config.limit_invalid`, naming
44
+ * the key. `0` is legal on every numeric axis: on `maxTurns` it is the documented "unbounded"
45
+ * sentinel, and on the budget axes it is an exhausted window (absurd but honest).
46
+ */
47
+ export declare function resolveTaskLimits(limits: TaskLimits | undefined): TaskLimits | undefined;
48
+ /**
49
+ * R2 双形轴(clay 追加令 2026-07-18): CC 2.1.212's fable-variant prompt gate (b9e —
50
+ * `fable_5_mitigations` capability / claude-mythos-5), ORTHOGONAL to the simple/classic profile.
51
+ * sema is BYOM, so the id may carry provider prefixes ("anthropic/claude-fable-5",
52
+ * "openrouter/anthropic/claude-fable-5"): BOUNDARY-AWARE family match on the last path segment
53
+ * (codex 统一复审 F3 — raw substring classified "vendor/not-claude-fable-5" and "claude-mythos-50"
54
+ * as fable), case-normalized. Recognition set = CC's _Nr (startsWith "claude-fable-") + b9e
55
+ * (mythos-5). R3 system sections fork on the resulting fact.
56
+ */
57
+ export declare function isFableFamilyModelId(id: string): boolean;
58
+ /**
59
+ * RB-50 (CC 2.1.220 启示①, clay 2026-07-25): the SINGLE decision point for the two prompt-shape axes.
60
+ * Both were resolved in separate places with different mechanisms — `promptProfile` off a TaskSpec field,
61
+ * `fableMitigations` off a raw model-id prefix test — so "which shape does this task speak" had no one
62
+ * place to read. CC 2.1.220's counterpart is a model-registry `capabilities` array (one table drives
63
+ * `lean_prompt` + `fable_5_mitigations` alike; anchors/2.1.220/CC-218-220-DIFF.md §2).
64
+ *
65
+ * sema stays BYOM: we cannot key off a capability table for arbitrary model ids, so the RESOLUTION RULES
66
+ * are unchanged — profile: spec > inherited internals > "simple"; mitigations: model family. This is a
67
+ * consolidation, not a behavior change (the axes stay ORTHOGONAL: neither rewrites the other).
68
+ */
69
+ export declare function resolveModelPromptTraits(model: {
70
+ id: string;
71
+ }, spec: {
72
+ promptProfile?: "simple" | "classic";
73
+ }, internals?: {
74
+ promptProfile?: "simple" | "classic";
75
+ }): {
76
+ promptProfile: "simple" | "classic";
77
+ fableMitigations: boolean;
78
+ };
79
+ export interface PrepareConfigDoorsInput {
80
+ /** borrowed-readonly — the caller's spec. NEVER mutated here: the per-task agents rebind returns a
81
+ * SHALLOW COPY on {@link PrepareConfigDoorsResult.spec} (only `tools` replaced). */
82
+ spec: TaskSpec;
83
+ /** borrowed-readonly — deployment seats. Read for: readFace value screen, locked-config preflight,
84
+ * retention door, interaction-posture door (incl. `deps.onError` disclosure), usage windows,
85
+ * brain-call guardrail default, model/role maps. */
86
+ deps: RunnerDeps;
87
+ /** borrowed-readonly — the retention door validates THIS store's declaration (it is one of the
88
+ * effective stores the leg would run over). No acquire happens in this phase. */
89
+ sessions: SessionStore;
90
+ /** borrowed-readonly — presence gates the resumeAt-conflict door and exempts the empty-objective
91
+ * door (a checkpoint resume passes `objective: ""` by design). */
92
+ resume?: PrepareResume | undefined;
93
+ /** borrowed-readonly — trusted engine channel: prompt-profile inheritance, the spawning run's
94
+ * interaction posture, the engine question-strip flag. */
95
+ internals?: RunInternals | undefined;
96
+ }
97
+ export interface PrepareConfigDoorsResult {
98
+ /** owned (by the caller, from here on) — the REBOUND spec: identical to the input object unless
99
+ * `spec.agents` was set, in which case it is a shallow copy whose `tools` is the
100
+ * withAgents-rebuilt roster. Every later phase (the safety scan above all — T21) must read THIS,
101
+ * never the original input. */
102
+ spec: TaskSpec;
103
+ /** owned — the frozen task-start snapshot of the caller's tool-face control arrays; the ONLY thing
104
+ * later face reads consult (own-task filter, defer classify, delegation ctx). Frozen-immutable ⇒
105
+ * shared by reference into tool execution ctxs. */
106
+ toolFaceSnapshot: {
107
+ exclude: readonly string[] | undefined;
108
+ defer: readonly string[] | undefined;
109
+ alwaysLoad: readonly string[] | undefined;
110
+ };
111
+ /** owned — the profile half of the RB-50 single decision point (model-independent by contract). */
112
+ promptProfile: "simple" | "classic";
113
+ /** owned — the administrator lock snapshot; the rest of prepare reads guarded slots through it. */
114
+ lockedPreflight: LockedPreflight;
115
+ /** owned — the resolved interaction posture (spec > spawning run > deployment); the
116
+ * AskUserQuestion mount and the child-ctx injection read this same value. */
117
+ resolvedInteractionPosture: "interactive" | "headless" | undefined;
118
+ /** owned — the resolved role; its `systemPrompt` seat is still read at prompt-input time. */
119
+ resolvedRole: ResolvedRole;
120
+ /** owned — the resolved main model (`resolvedRole.model`, re-exposed as the name every later
121
+ * phase reads). */
122
+ model: Model;
123
+ /** owned — caller-explicit > role default > model default; undefined = off. */
124
+ thinking: ThinkingLevel | undefined;
125
+ /** owned — the compaction model: explicit wins, else the `summarize` role, else undefined (main
126
+ * model fallback inside maybeCompact). NOT derivable from {@link resolvedRole}. */
127
+ compModel: Model | undefined;
128
+ /** owned — the mitigations half of the RB-50 decision point (model-family fact). */
129
+ fableMitigations: boolean;
130
+ /** owned — validated deployment governance windows (undefined = ungoverned). */
131
+ usageWindows: readonly UsageWindow[] | undefined;
132
+ /** owned, out-param cell — created EMPTY here; the brain-call wiring later installs into
133
+ * `.current`. Returned so driver and harness share one cell identity. */
134
+ brainCallGuardrailRef: BrainCallGuardrailRef;
135
+ /** owned — the resolved outer brain-call guardrail (task > deployment > default; undefined per
136
+ * the resolver's off states). */
137
+ brainCallGuardrailMs: number | undefined;
138
+ }
139
+ /** The B-1 phase body — the config-doors slice, verbatim (see the module header for the contract). */
140
+ export declare function prepareConfigDoors(input: PrepareConfigDoorsInput): PrepareConfigDoorsResult;