@sema-agent/core 7.5.1 → 7.6.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 (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import type { CheckpointPause } from "./pause-registry.js";
1
2
  import type { SystemInjectionPriority } from "./task-notification.js";
2
3
  import { type QuestionAnswer } from "./ask-question.js";
3
4
  import type { ReadEntry } from "../tools/fs/safety.js";
@@ -439,10 +440,21 @@ export declare function buildRiskDescriptor(input: {
439
440
  * refuses to redeem such a row (the governed boundary must not vanish with a deployment's wiring).
440
441
  */
441
442
  export interface RealApprovalGateBit {
442
- /** The three members of the closed ask-origin set that can mint the bit derived FROM that set, so
443
- * the two vocabularies cannot drift into two spellings of one word. */
444
- origin: Extract<import("./ask-origin.js").AskOrigin, "org_rule" | "org_unavailable" | "policy">;
443
+ /** The parked ask's ORIGIN word a projection of the same closed set the card, the observer and the
444
+ * `tool_end` frame carry ({@link import("./ask-origin.js").AskOrigin}), never a second vocabulary. It
445
+ * is the ask's ROUTING word: a marked call reads `unresolvable` even when the org governs it, because
446
+ * the ladder routes a marked call to the park before anything else. Rows minted before the projection
447
+ * carried a three-word subset of this set (`org_rule` / `org_unavailable` / `policy`), so every stored
448
+ * value is still a member. */
449
+ origin: import("./ask-origin.js").AskOrigin;
450
+ /** The ORG FACT the ask was minted under (`"rule"` = an org rule asked, `"unavailable"` = org adjudication
451
+ * unreachable), kept beside the routing word because routing can outrank the word (a marked call reads
452
+ * `unresolvable`) while the governance stays. The #120 governed mark and the resume belts read THIS
453
+ * member through {@link realApprovalOrgFact} (absent on rows minted before it: the org words imply it). */
454
+ org?: "rule" | "unavailable";
445
455
  }
456
+ /** The ONE read of a bit's governed fact (the member; the two org origin words for rows minted before it). */
457
+ export declare function realApprovalOrgFact(bit: RealApprovalGateBit | undefined): "rule" | "unavailable" | undefined;
446
458
  export type CheckpointGate =
447
459
  /** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
448
460
  * carries an OPTIONAL display-only {@link RiskDescriptor} (severity/axes/summary) for the supervisor
@@ -594,66 +606,38 @@ export type ResumeOutcome = {
594
606
  */
595
607
  answer?: QuestionAnswer;
596
608
  /**
597
- * WHAT ENDED THIS APPROVAL`"human"` (a person gave the final verdict), `"timeout"` (the
598
- * configured window elapsed with no answer), or `"aborted"` (every other non-human end: the run
599
- * was cancelled, the connection dropped, the store gave way, retries ran out). See
600
- * {@link import("./tool-policy.js").ApprovalSettledBy} for the vocabulary itself.
609
+ * The host's DECISION FACTSwho ended this wait: a person (`"person"`, the decide an operator made
610
+ * through the deployment's approval channel, with the attribution that channel reports), or the
611
+ * host's own SLA sweep at the row's deadline (`"sla_timeout"` FACET A of the deadline contract; only
612
+ * a sweep may say it, and a sweep cannot `allow`). REQUIRED: a decide that names nobody is not a
613
+ * record of anything that happened, and core does not guess.
601
614
  *
602
- * Attribution travels WITH the decision because it is a property OF the decision, not something
603
- * a later reader can reconstruct: by the time an approval reaches a consumer, "denied" and "the
604
- * window closed" have already collapsed into the same `decision:"deny"`, and only the caller that
605
- * settled it still knows which happened. Core does not derive it, does not default it, and never
606
- * infers it from some other outcome — it carries what the settling caller wrote onto the
607
- * `tool_end` frame of the call this resume resolves.
615
+ * The host supplies FACTS, never the settlement WORD: core mints the record
616
+ * ({@link import("./gate-outcome.js").GateOutcome} `human_allowed` / `human_refused` / `park_sla_expired`,
617
+ * joined with the row's persisted `origin`) at the resume entry, writes it onto the resolved row
618
+ * (`resolvedOutcome.gateOutcome`) and projects it onto the resolved call's `tool_end` frame. A caller
619
+ * that supplies `gateOutcome` itself is refused pre-CAS.
608
620
  *
609
- * **Fill it at every settlement site.** A deployment resolving an approval from several places
610
- * (an operator decide, an SLA sweep at the deadline, a cancel path) names the cause at each of
611
- * them. Deriving it once, centrally, from the last outcome at hand is precisely how a window that
612
- * elapsed gets reported to a person as another person's refusal.
621
+ * `approver` the attribution the approval channel reports for the party that ended the wait. Core
622
+ * AUTHENTICATES NOTHING: the identity work is the deployment's (its card, its signature check, its
623
+ * console). What core owns is that the claim is well-formed: a plain string, bounded, free of control
624
+ * characters, refused pre-CAS (`checkpoint.invalid_outcome`) otherwise refused rather than dropped,
625
+ * because a settlement that silently lost its attributed party reads as unattributed.
613
626
  *
614
- * OPTIONAL and additive: absent means the caller named no source (a caller written before this
615
- * field existed), and a consumer must NOT read a semantic out of the absence — core neither
616
- * substitutes a default nor guesses. A value outside the three words is refused pre-CAS
617
- * (`checkpoint.invalid_outcome`), like every other out-of-domain field on this arm: a settlement
618
- * source nobody can read is worse than none at all. So is one that contradicts its own decision:
619
- * `decision:"allow"` accepts only `"human"` or omission, because "nobody answered, therefore it
620
- * ran" is not a record of anything that happened (a deployment whose CONFIGURATION auto-allows an
621
- * elapsed window omits the field — no one ended that wait).
622
- *
623
- * SCOPE — this rides the settlement, NOT the persisted winner: `winnerFromOutcome` records the
624
- * decision (and its rewrite/answer/reason), which is what an `env_failed` replay must reproduce
625
- * exactly, and the source is not part of that authorization. A replay therefore states its own
626
- * source and is not held to the first one. Binding it into the persisted winner would also make a
627
- * re-supply that merely OMITS the field fail closed on the recovery path, which is the wrong place
628
- * to add a new refusal; carrying it into the durable row is a store-shape change owned jointly
629
- * with the backends and belongs to its own change, not to this one.
627
+ * SCOPE the facts ride the settlement, not the persisted winner: `winnerFromOutcome` records what an
628
+ * `env_failed` replay must reproduce exactly, and who reported a settlement is not part of that
629
+ * authorization; a replay states its own facts.
630
630
  */
631
- settledBy?: import("./tool-policy.js").ApprovalSettledBy;
631
+ hostDecision: {
632
+ decidedBy: "person" | "sla_timeout";
633
+ approver?: string;
634
+ };
632
635
  /**
633
- * design/252 G-7 WHOSE settlement: the identifier the approval channel reports for the party
634
- * that ended this wait, transcribed onto the resolved call's `tool_end` frame beside
635
- * {@link settledBy}.
636
- *
637
- * Core AUTHENTICATES NOTHING here. On the durable lane the identity work is entirely the
638
- * deployment's — its approval card, its signature check, its operator console decide who this
639
- * was; a library holding no identity surface has nothing to check the claim against and does not
640
- * pretend otherwise. What core owns is that the claim is well-formed: a plain string, bounded,
641
- * free of control characters, refused pre-CAS (`checkpoint.invalid_outcome`) otherwise, like
642
- * every other out-of-domain field on this arm. Refused rather than dropped, because a settlement
643
- * that silently lost its attributed party reads as unattributed — the misreport this seat exists
644
- * to end.
645
- *
646
- * SCOPE — like {@link settledBy}, this rides the SETTLEMENT and not the persisted winner:
647
- * `winnerFromOutcome` records what an `env_failed` replay must reproduce exactly, and who
648
- * reported a settlement is not part of that authorization. A replay states its own attribution
649
- * and is not held to the first one; binding it into the durable row would also make a re-supply
650
- * that merely omits the field fail closed on the recovery path. Carrying it into the row is a
651
- * store-shape change owned jointly with the backends and belongs to its own change.
652
- *
653
- * OPTIONAL and additive: absent means the caller named nobody, which is not the same as "nobody
654
- * approved this" and must not be read as one.
636
+ * CORE-MINTED, on the persisted twin only: the settlement record this decide produced. Present on
637
+ * the row's `resolvedOutcome` after the resolve, and on the `tool_end` frame of the resolved call.
638
+ * Never accepted from a caller (refused pre-CAS) — the host does not mint words.
655
639
  */
656
- approver?: string;
640
+ gateOutcome?: import("./gate-outcome.js").GateOutcome;
657
641
  }
658
642
  /** design/74: continue a resource-suspended run with the next slice's allowance. NO decision payload and
659
643
  * NO budget figure — the allowance is computed from {@link Checkpoint.resourceLedger}, so money never
@@ -874,6 +858,13 @@ export type PendingAction = {
874
858
  kind: "resource_limit";
875
859
  reason: ResourceLimitReason;
876
860
  }
861
+ /** design/76 §2.5: paused at a POST-PREDICTION REVIEW (`CheckpointGate.kind === "needs_review"`). There is NO
862
+ * pending tool to resolve — a human/judge reviews the buffered predicted state-diff (a PROFILE-held REF, never
863
+ * stored here) — so this arm has no fields. Its OWN member: before the pause registry a `needs_review` row
864
+ * borrowed another arm's seat, and the gate × pendingAction pairing lived in a comment. */
865
+ | {
866
+ kind: "review";
867
+ }
877
868
  /** design/80 D-B (r3 — an EXPLICIT arm, NOT a reused `resource_limit` placeholder): paused at a PRE-ACTION
878
869
  * PLAN REVIEW (`CheckpointGate.kind === "plan_review"`). There is NO pending tool to resolve — the human
879
870
  * reviews the PLAN, not a tool call — so this arm has no tool fields. Every consumer that reads the
@@ -1496,6 +1487,11 @@ export interface ResolvedOutcome {
1496
1487
  * the same decision" for no delivery benefit, since a replay is produced by re-presenting the same
1497
1488
  * outcome, not by reconstructing an equivalent one. */
1498
1489
  reason?: string;
1490
+ /** The settlement record core minted for this decide ({@link import("./gate-outcome.js").GateOutcome}):
1491
+ * the row's own copy of what the resolved call's `tool_end` frame carries, written with the winner so
1492
+ * an audit reading the row reads the same record the stream carried. NOT part of the replay equality
1493
+ * (`when` differs per replay; the record is a consequence of the decision, not its substance). */
1494
+ gateOutcome?: import("./gate-outcome.js").GateOutcome;
1499
1495
  }
1500
1496
  /**
1501
1497
  * design/80 D-1: why a consumed checkpoint was {@link CheckpointStore.reopen | reopened} (resolved →
@@ -1579,7 +1575,9 @@ export type TerminalClaimOutcome = {
1579
1575
  };
1580
1576
  /** A persisted suspension point: enough to resume a task on any replica. `status` drives the 3-state
1581
1577
  * machine (pending → resolved | expired) that makes resume idempotent (§5). */
1582
- export interface Checkpoint {
1578
+ /** A durable checkpoint: the row's fields ({@link CheckpointRow}) plus its registry-bound pause pair. */
1579
+ export type Checkpoint = CheckpointRow & CheckpointPause;
1580
+ export interface CheckpointRow {
1583
1581
  token: CheckpointToken;
1584
1582
  /** Non-secret stable identity ({@link mintCheckpointId}) — the display/correlation twin of
1585
1583
  * the secret `token` (identification vs capability, separate axes). Present on every row minted
@@ -1600,8 +1598,6 @@ export interface Checkpoint {
1600
1598
  sessionId: string;
1601
1599
  /** The session leaf the suspend happened at — the resume point (CAS write base, §5). */
1602
1600
  leafId: string;
1603
- gate: CheckpointGate;
1604
- pendingAction: PendingAction;
1605
1601
  state: CheckpointState;
1606
1602
  /** design/74 R3-B: cross-slice resource accounting. Present for a `resource_limit` gate (debited at the
1607
1603
  * suspend's `put`; read on resume to size the next slice + aggregate the final stats). **design/80 D-E-core
@@ -2075,15 +2071,15 @@ export declare class CheckpointError extends Error {
2075
2071
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
2076
2072
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
2077
2073
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
2078
- * `"settledBy"` (the settlement source is outside the three-word vocabulary: re-decide naming one
2079
- * of them, or omit the field), or — design/252 G-7 — `"approver"` (the attribution is not a
2074
+ * `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`, pair
2075
+ * an SLA timeout with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
2080
2076
  * well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
2081
2077
  * it). The throw
2082
2078
  * sites always knew which arm fired; this carries that fact instead of dropping it (ruled
2083
2079
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
2084
2080
  * discriminant. */
2085
2081
  readonly detail?: {
2086
- field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
2082
+ field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver";
2087
2083
  /** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
2088
2084
  * (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
2089
2085
  * retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
@@ -2257,15 +2253,15 @@ export declare class CheckpointError extends Error {
2257
2253
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
2258
2254
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
2259
2255
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
2260
- * `"settledBy"` (the settlement source is outside the three-word vocabulary: re-decide naming one
2261
- * of them, or omit the field), or — design/252 G-7 — `"approver"` (the attribution is not a
2256
+ * `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`, pair
2257
+ * an SLA timeout with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
2262
2258
  * well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
2263
2259
  * it). The throw
2264
2260
  * sites always knew which arm fired; this carries that fact instead of dropping it (ruled
2265
2261
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
2266
2262
  * discriminant. */
2267
2263
  detail?: {
2268
- field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
2264
+ field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver";
2269
2265
  /** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
2270
2266
  * (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
2271
2267
  * retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
@@ -2674,22 +2670,28 @@ export interface CheckpointStore {
2674
2670
  * The SINGLE resolution of "which CheckpointStore does this run use" (ruled 2026-08-04; re-homed
2675
2671
  * from prepare-task with the design/173 wiring manifest so the static self-check half shares it
2676
2672
  * without a runtime cycle). The spec seat is three-valued — a store wins, `undefined` falls through
2677
- * to the deployment's `RunnerDeps.checkpointStore`, and an explicit `null` means this run has NO
2678
- * store at all (see `TaskSpec.checkpointStore`: the per-run off switch for the durable machine, so
2679
- * a machine-started run cannot mint a checkpoint nobody will ever resolve).
2673
+ * to the deployment's `RunnerDeps.checkpointStore`, and the explicit word `"disabled"` means this run
2674
+ * has NO store at all (see `TaskSpec.checkpointStore`: the per-run off switch for the durable
2675
+ * machine, so a machine-started run cannot mint a checkpoint nobody will ever resolve).
2680
2676
  *
2681
- * Written as an explicit `=== null` test rather than the obvious `spec.checkpointStore ??
2682
- * deps.checkpointStore` BECAUSE `??` treats `null` exactly like `undefined`: under a bare nullish
2683
- * coalesce the null falls through and the run is handed the very deployment store the caller asked
2684
- * it not to have, re-arming every suspend leg silently, since the resulting run looks perfectly
2685
- * normal right up until it parks forever. Every read of the pair goes through here so that trap has
2686
- * ONE place to be wrong instead of nine.
2677
+ * The off switch is a WORD, not `null`, on purpose: `??` treats `null` exactly like `undefined`, so
2678
+ * the old null sentinel silently fell through a bare coalesce to the very deployment store the
2679
+ * caller asked not to have re-arming every suspend leg while the run looked perfectly normal right
2680
+ * up until it parked forever and every read of the pair had to remember a special `=== null` test.
2681
+ * A string is never nullish, so the ordinary coalesce is the whole rule. The retired spelling is
2682
+ * REFUSED loudly here (the one read every site goes through) rather than read as either value: a
2683
+ * caller still writing `null` is told the word to write, at the first prepare, instead of being
2684
+ * handed a store it asked not to have or a silent off switch it did not spell.
2687
2685
  */
2688
2686
  export declare function resolveCheckpointStore(spec: {
2689
- checkpointStore?: CheckpointStore | null;
2687
+ checkpointStore?: CheckpointStore | "disabled";
2690
2688
  }, deps: {
2691
2689
  checkpointStore?: CheckpointStore;
2692
2690
  }): CheckpointStore | undefined;
2691
+ /** The ONE refusal of the retired `checkpointStore: null` spelling (the resolver and the workflow baseline overlay both throw it). */
2692
+ export declare function retiredCheckpointStoreNull(): Error & {
2693
+ code: "config.invalid_checkpoint_store";
2694
+ };
2693
2695
  /**
2694
2696
  * design/80 D-1 (persist-winner): derive the durable {@link ResolvedOutcome} winner from a resume
2695
2697
  * `outcome`, or `undefined` when the outcome binds to no pending action. A `policy_ask` resume names a
@@ -178,6 +178,13 @@ export function buildRiskDescriptor(input) {
178
178
  return { severity: 2, axes: {}, toolName: typeof toolName === "string" ? toolName : "" };
179
179
  }
180
180
  }
181
+ export function realApprovalOrgFact(bit) {
182
+ if (bit === undefined)
183
+ return undefined;
184
+ if (bit.org !== undefined)
185
+ return bit.org;
186
+ return bit.origin === "org_rule" ? "rule" : bit.origin === "org_unavailable" ? "unavailable" : undefined;
187
+ }
181
188
  export const CURRENT_CHECKPOINT_VERSION = 1;
182
189
  export const RESOURCE_CHECKPOINT_VERSION = 2;
183
190
  export const BINDING_CHECKPOINT_VERSION = 3;
@@ -356,10 +363,18 @@ function sameValueAt(a, b, depth, aToB, bToA) {
356
363
  }
357
364
  }
358
365
  export function resolveCheckpointStore(spec, deps) {
359
- if (spec.checkpointStore === null)
366
+ if (Object.is(spec.checkpointStore, null))
367
+ throw retiredCheckpointStoreNull();
368
+ if (spec.checkpointStore === "disabled")
360
369
  return undefined;
361
370
  return spec.checkpointStore ?? deps.checkpointStore;
362
371
  }
372
+ export function retiredCheckpointStoreNull() {
373
+ const e = new Error('TaskSpec.checkpointStore: null is not a value any more — write checkpointStore: "disabled" to run without a ' +
374
+ "checkpoint store (the per-run durable off switch), or leave the field unset to use the deployment's store.");
375
+ e.code = "config.invalid_checkpoint_store";
376
+ return e;
377
+ }
363
378
  export function winnerFromOutcome(outcome) {
364
379
  if (outcome.gate === "plan_review" || outcome.gate === "dry_run_review") {
365
380
  const editedPlan = outcome.gate === "plan_review" ? outcome.editedPlan : undefined;
@@ -377,6 +392,7 @@ export function winnerFromOutcome(outcome) {
377
392
  decision: outcome.decision,
378
393
  ...(outcome.updatedInput === undefined ? {} : { updatedInput: outcome.updatedInput }),
379
394
  ...(outcome.answer === undefined ? {} : { answer: outcome.answer }),
395
+ ...(outcome.gateOutcome === undefined ? {} : { gateOutcome: outcome.gateOutcome }),
380
396
  ...(outcome.reason === undefined ? {} : { reason: outcome.reason }),
381
397
  };
382
398
  }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The settlement record and the gate outcome — the L0 vocabulary of what ONE tool-gate pass decided,
3
+ * what ask it settled and who asked. One structure, minted once (the gate's exit; the decide lane for a
4
+ * durable park) and projected three times without re-derivation: the `permissionDenied` observer's
5
+ * payload, the call's `tool_end` frame, and the durable row's resolved outcome. Lives beside
6
+ * `tool-policy.ts` rather than in it because it is a different concern from the permission verdict a
7
+ * policy composes: a policy says allow / ask / deny; this file says how the pass that consumed that
8
+ * verdict ended.
9
+ */
10
+ import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
11
+ /**
12
+ * How the wait an ask was in ENDED — the closed set. One word per way a wait can end; the `who` on the
13
+ * record says which party ended it and is fixed BY the word (see {@link Settlement}).
14
+ * - `human_allowed` / `human_refused` — a person (or the approver acting for one) decided; the refusal's
15
+ * own note, when the decider attached one, rides `note`.
16
+ * - `approval_window_expired` — an approval window elapsed with no answer: the engine's own approval
17
+ * factory window (`createApprovalPolicy.approvalTimeoutMs`), or a synchronous host approver that
18
+ * reported its own window elapsed.
19
+ * - `denial_limit_window_expired` — the classifier denial-limit fallback's auto-deny window (core's own
20
+ * timer over the synchronous leg) elapsed with no answer.
21
+ * - `park_sla_expired` — a DURABLE park's SLA deadline passed and the host's sweep resolved it as a deny
22
+ * (FACET A). A store `expire`/`reap` produces no `tool_end` and therefore no settlement.
23
+ * - `no_approver` — headless: no approver (or no question face) is wired, or the deny posture string.
24
+ * - `approver_unavailable` — the approver (or an ancestor's frozen approver) answered the ROUTING question
25
+ * "nobody reachable", and no park took the ask.
26
+ * - `approver_error` — the approver threw.
27
+ * - `approver_contract` — the approver answered outside its contract (a non-boolean verdict, an
28
+ * unreadable member, a timeout-settled allow, a refused attribution…).
29
+ * - `presentation_failed` — the args or the edit could not be safely presented/adopted (unclonable).
30
+ * - `blanket_allow_refused` — a blanket `onAsk:"allow"` met a `requiresRealApproval` ask.
31
+ * - `task_aborted` — the wait's abort signal ended it (the run's own end, or a turn-level interrupt).
32
+ */
33
+ export declare const SETTLEMENT_KINDS: readonly ["human_allowed", "human_refused", "approval_window_expired", "denial_limit_window_expired", "park_sla_expired", "no_approver", "approver_unavailable", "approver_error", "approver_contract", "presentation_failed", "blanket_allow_refused", "task_aborted"];
34
+ export type SettlementKind = (typeof SETTLEMENT_KINDS)[number];
35
+ /** Membership test for {@link SettlementKind} — the word crosses process boundaries on `tool_end`, so a
36
+ * consumer validating one it received must not hand-roll the set. */
37
+ export declare function isSettlementKind(v: unknown): v is SettlementKind;
38
+ /**
39
+ * The settlement record: WHAT ended the wait (`kind`), WHO ended it (`who` — its shape is decided by the
40
+ * word: the two human words carry a person, the window words say whose window, the park word the host,
41
+ * the fail-closed family nobody), WHEN (epoch ms, written at the settlement site), and for a person's
42
+ * refusal the decider's own NOTE when one was attached (a refusal WITHOUT a note is a bare "no", the arm
43
+ * the runner treats as a control-flow boundary for the issuing batch).
44
+ *
45
+ * It is a discriminated union and not a free product of `kind` × `who` on purpose: a free product can
46
+ * express "human_allowed, ended by the engine's window" — a structurally complete record that contradicts
47
+ * itself — and a consumer would then need the pairing rules this shape makes unwritable.
48
+ *
49
+ * @contract settlement.single_mint — every `Settlement` kind is composed by CORE at the site whose wait
50
+ * ended (the ask resolver's arms, the approval factory's arms, the decide lane over a host's decision
51
+ * FACTS). A host never mints a kind: a synchronous approver reports `"human"`/`"timeout"` on its outcome
52
+ * and a durable decide reports `hostDecision.decidedBy` (`"person"`/`"sla_timeout"`) — facts core turns
53
+ * into the word. Nothing on the caller-composable policy face (`PermissionResult`) carries a settlement,
54
+ * so a policy cannot self-declare one; the engine's own settlement sites that hand a settled decision
55
+ * back through a policy fold attest it through {@link engineSettlementOf}.
56
+ */
57
+ export type Settlement = {
58
+ kind: "human_allowed" | "human_refused";
59
+ who: {
60
+ party: "person";
61
+ approver?: string;
62
+ };
63
+ when: number;
64
+ note?: string;
65
+ } | {
66
+ kind: "approval_window_expired";
67
+ who: {
68
+ party: "engine";
69
+ window: "approval_factory";
70
+ } | {
71
+ party: "host";
72
+ };
73
+ when: number;
74
+ } | {
75
+ kind: "denial_limit_window_expired";
76
+ who: {
77
+ party: "engine";
78
+ window: "denial_limit";
79
+ };
80
+ when: number;
81
+ } | {
82
+ kind: "park_sla_expired";
83
+ who: {
84
+ party: "host";
85
+ approver?: string;
86
+ };
87
+ when: number;
88
+ } | {
89
+ kind: "no_approver" | "approver_unavailable" | "approver_error" | "approver_contract" | "presentation_failed" | "blanket_allow_refused" | "task_aborted";
90
+ who: {
91
+ party: "none";
92
+ };
93
+ when: number;
94
+ };
95
+ /** Which settlement kinds are REFUSALS (the ask ended as a deny). Total over the closed set — the fence
96
+ * below refuses a member without a row — so invariant I2 is a table lookup, not a list in a condition. */
97
+ export declare const SETTLEMENT_IS_REFUSAL: {
98
+ readonly human_allowed: false;
99
+ readonly human_refused: true;
100
+ readonly approval_window_expired: true;
101
+ readonly denial_limit_window_expired: true;
102
+ readonly park_sla_expired: true;
103
+ readonly no_approver: true;
104
+ readonly approver_unavailable: true;
105
+ readonly approver_error: true;
106
+ readonly approver_contract: true;
107
+ readonly presentation_failed: true;
108
+ readonly blanket_allow_refused: true;
109
+ readonly task_aborted: true;
110
+ };
111
+ /** Compile-time fence: `never` while every settlement kind has a refusal row. */
112
+ export type SettlementRefusalTableCoversEveryKind = AssertAllKeysHandled<Exclude<SettlementKind, keyof typeof SETTLEMENT_IS_REFUSAL>>;
113
+ /**
114
+ * WHO REFUSED a call — the layer whose verdict is the deny. The other question a deny raises, "who ASKED",
115
+ * is answered by {@link GateOutcome.origin} (an {@link import("./ask-origin.js").AskOrigin}); the two used
116
+ * to share one seven-word list (`PermissionDeniedSource`) in which `classifier` meant both "the classifier
117
+ * asked" and "the classifier denied".
118
+ * - `policy` — the deployment `ToolPolicy` denied (directly, or re-checking an approved edit), or the
119
+ * approval-edit chain hit its round cap.
120
+ * - `hook` — a PreToolUse hook denied, threw, or never answered (the engine's fail-closed block on a
121
+ * hook that made no decision counts as the hook layer's refusal for attribution).
122
+ * - `org` — an organization policy rule denied.
123
+ * - `classifier` — the auto-mode classifier denied directly (its ASK-side role is `origin:
124
+ * "denial_limit_fallback"`).
125
+ * - `plan_mode` — plan mode's read-only block on a write tool.
126
+ * - `compliance` — the compliance call-time lock.
127
+ * - `write_protection` — an approved edit was rewritten by the restriction chain onto a write-protected
128
+ * path that no approval covers.
129
+ * - `ask_resolution` — the ask's own settlement is the refusal (a person said no, a window elapsed,
130
+ * nobody could be asked…); the detail is on {@link GateOutcome.settlement}.
131
+ */
132
+ export declare const DENIED_BY_VALUES: readonly ["policy", "hook", "org", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
133
+ export type DeniedBy = (typeof DENIED_BY_VALUES)[number];
134
+ /** Membership test for {@link DeniedBy}. */
135
+ export declare function isDeniedBy(v: unknown): v is DeniedBy;
136
+ /** Which layers may VETO an approval a person already gave (invariant I3: a `human_allowed` settlement
137
+ * beside a `denied` disposition names one of these). The approval-edit re-screen (hook), the policy and
138
+ * org re-checks, and the write-protection re-judge run AFTER the person's yes; the classifier, plan mode
139
+ * and compliance run before any ask exists, and `ask_resolution` IS the settlement, not a veto of it. */
140
+ export declare const DENIED_BY_MAY_VETO: {
141
+ readonly policy: true;
142
+ readonly hook: true;
143
+ readonly org: true;
144
+ readonly classifier: false;
145
+ readonly plan_mode: false;
146
+ readonly compliance: false;
147
+ readonly write_protection: true;
148
+ readonly ask_resolution: false;
149
+ };
150
+ /** Compile-time fence: `never` while every deny layer has a veto row. */
151
+ export type VetoTableCoversEveryDeniedBy = AssertAllKeysHandled<Exclude<DeniedBy, keyof typeof DENIED_BY_MAY_VETO>>;
152
+ /** The final disposition of a gate pass. */
153
+ export type GateDisposition = {
154
+ kind: "allowed";
155
+ } | {
156
+ kind: "denied";
157
+ deniedBy: DeniedBy;
158
+ };
159
+ /**
160
+ * The WHOLE record of one tool-gate pass: the final disposition, plus — when this pass settled an ask —
161
+ * the settlement it consumed and the ask's origin. Minted ONCE (the gate's exit; the decide lane for a
162
+ * durable park) and projected three times without re-derivation: the `permissionDenied` observer's
163
+ * payload, the call's `tool_end` frame, and the durable row's resolved outcome. The three faces are the
164
+ * same object graph, so they cannot tell different stories.
165
+ *
166
+ * The three members are orthogonal but bound by four invariants ({@link screenGateOutcome}):
167
+ * I1 `settlement` present ⇔ `origin` present (they describe the same ask);
168
+ * I2 `disposition.deniedBy === "ask_resolution"` ⇒ `settlement` present and a refusal kind;
169
+ * I3 `settlement.kind === "human_allowed"` ∧ `disposition.kind === "denied"` ⇒ `deniedBy` is a veto
170
+ * layer ({@link DENIED_BY_MAY_VETO}) — a person approved, a later re-check refused; the approver stays
171
+ * on the settlement (it is not the approver's refusal);
172
+ * I4 `disposition.kind === "allowed"` ⇒ `settlement` absent or `human_allowed`.
173
+ * Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
174
+ * a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
175
+ * settlement because neither settled one.
176
+ */
177
+ export interface GateOutcome {
178
+ disposition: GateDisposition;
179
+ settlement?: Settlement;
180
+ origin?: AskOrigin;
181
+ }
182
+ /**
183
+ * The one invariant screen for a {@link GateOutcome} (I1–I4 above, plus the shape of each member). Runs
184
+ * where an outcome enters a wire face — the engine's settlement sideband before the `tool_end` frame, and
185
+ * the decide lane before a durable row is resolved. Returns the defects as sentences (an empty list is a
186
+ * clean record); the caller decides the fail-loud form (report + withhold, or refuse), never silently
187
+ * repairs the record.
188
+ */
189
+ export declare function screenGateOutcome(g: GateOutcome): string[];
@@ -0,0 +1,70 @@
1
+ import { isAskOrigin } from "./ask-origin.js";
2
+ export const SETTLEMENT_KINDS = [
3
+ "human_allowed",
4
+ "human_refused",
5
+ "approval_window_expired",
6
+ "denial_limit_window_expired",
7
+ "park_sla_expired",
8
+ "no_approver",
9
+ "approver_unavailable",
10
+ "approver_error",
11
+ "approver_contract",
12
+ "presentation_failed",
13
+ "blanket_allow_refused",
14
+ "task_aborted",
15
+ ];
16
+ const SETTLEMENT_KIND_SET = new Set(SETTLEMENT_KINDS);
17
+ export function isSettlementKind(v) {
18
+ return SETTLEMENT_KIND_SET.has(v);
19
+ }
20
+ export const SETTLEMENT_IS_REFUSAL = {
21
+ human_allowed: false,
22
+ human_refused: true,
23
+ approval_window_expired: true,
24
+ denial_limit_window_expired: true,
25
+ park_sla_expired: true,
26
+ no_approver: true,
27
+ approver_unavailable: true,
28
+ approver_error: true,
29
+ approver_contract: true,
30
+ presentation_failed: true,
31
+ blanket_allow_refused: true,
32
+ task_aborted: true,
33
+ };
34
+ export const DENIED_BY_VALUES = ["policy", "hook", "org", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
35
+ const DENIED_BY_SET = new Set(DENIED_BY_VALUES);
36
+ export function isDeniedBy(v) {
37
+ return DENIED_BY_SET.has(v);
38
+ }
39
+ export const DENIED_BY_MAY_VETO = {
40
+ policy: true,
41
+ hook: true,
42
+ org: true,
43
+ classifier: false,
44
+ plan_mode: false,
45
+ compliance: false,
46
+ write_protection: true,
47
+ ask_resolution: false,
48
+ };
49
+ export function screenGateOutcome(g) {
50
+ const defects = [];
51
+ const d = g.disposition;
52
+ if (d.kind === "denied" && !isDeniedBy(d.deniedBy))
53
+ defects.push(`disposition.deniedBy "${String(d.deniedBy)}" is outside the closed set`);
54
+ const s = g.settlement;
55
+ if (s !== undefined && !isSettlementKind(s.kind))
56
+ defects.push(`settlement.kind "${String(s.kind)}" is outside the closed set`);
57
+ if (g.origin !== undefined && !isAskOrigin(g.origin))
58
+ defects.push(`origin "${String(g.origin)}" is outside the closed set`);
59
+ if ((s !== undefined) !== (g.origin !== undefined))
60
+ defects.push("I1: settlement and origin must be present together — they describe the same ask");
61
+ if (d.kind === "denied" && d.deniedBy === "ask_resolution" && (s === undefined || !isSettlementKind(s.kind) || !SETTLEMENT_IS_REFUSAL[s.kind])) {
62
+ defects.push("I2: deniedBy ask_resolution requires a settlement whose kind is a refusal");
63
+ }
64
+ if (s !== undefined && s.kind === "human_allowed" && d.kind === "denied" && isDeniedBy(d.deniedBy) && !DENIED_BY_MAY_VETO[d.deniedBy]) {
65
+ defects.push(`I3: a human_allowed settlement beside a deny must name a veto layer, not "${d.deniedBy}"`);
66
+ }
67
+ if (d.kind === "allowed" && s !== undefined && s.kind !== "human_allowed")
68
+ defects.push(`I4: an allowed disposition cannot carry a "${s.kind}" settlement`);
69
+ return defects;
70
+ }