@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.
- package/CHANGELOG.md +61 -0
- package/dist/agents/cascade.d.ts +2 -2
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +5 -3
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/subagent.d.ts +24 -42
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +4 -3
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +3 -3
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hooks.d.ts +18 -92
- package/dist/core/hooks.js +88 -85
- package/dist/core/mcp-failure.d.ts +104 -0
- package/dist/core/mcp-failure.js +128 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/protocol-table.d.ts +5 -0
- package/dist/core/protocol-table.js +1 -0
- package/dist/core/runner/abort-race.d.ts +41 -0
- package/dist/core/runner/abort-race.js +38 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/checkpoint-scope.d.ts +15 -3
- package/dist/core/runner/checkpoint-scope.js +3 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/content-ask-bindings.d.ts +27 -0
- package/dist/core/runner/content-ask-bindings.js +1 -0
- package/dist/core/runner/contracts.d.ts +144 -50
- package/dist/core/runner/denial-limit-arms.d.ts +24 -1
- package/dist/core/runner/denial-limit-arms.js +24 -3
- package/dist/core/runner/gate-exit.d.ts +74 -0
- package/dist/core/runner/gate-exit.js +55 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
- package/dist/core/runner/inherited-ask-grants.js +29 -0
- package/dist/core/runner/park-commit.d.ts +102 -0
- package/dist/core/runner/park-commit.js +31 -0
- package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
- package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
- package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
- package/dist/core/runner/prepare-ask-lane.js +131 -0
- package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
- package/dist/core/runner/prepare-boundary-parks.js +169 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-context-lane.d.ts +119 -0
- package/dist/core/runner/prepare-context-lane.js +230 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
- package/dist/core/runner/prepare-gate-stations.js +265 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
- package/dist/core/runner/prepare-memory-engine-session.js +233 -0
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +162 -0
- package/dist/core/runner/prepare-park-ask.js +377 -0
- package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
- package/dist/core/runner/prepare-policy-chain.js +584 -0
- package/dist/core/runner/prepare-project-context.d.ts +1 -13
- package/dist/core/runner/prepare-project-context.js +1 -3
- package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
- package/dist/core/runner/prepare-prompt-assembly.js +162 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
- package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
- package/dist/core/runner/prepare-protocol-tools.js +0 -3
- package/dist/core/runner/prepare-question-face.d.ts +3 -21
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
- package/dist/core/runner/prepare-safety-scan.js +1 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
- package/dist/core/runner/prepare-suspend-saga.js +308 -0
- package/dist/core/runner/prepare-task.d.ts +9 -136
- package/dist/core/runner/prepare-task.js +47 -2745
- package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
- package/dist/core/runner/prepare-turn-wiring.js +201 -0
- package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
- package/dist/core/runner/prepare-wiring-manifest.js +16 -10
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
- package/dist/core/runner/prepare-workspace-restore.js +3 -16
- package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
- package/dist/core/runner/prompt-hash-salt.js +2 -0
- package/dist/core/runner/remote-env-retry.d.ts +29 -0
- package/dist/core/runner/remote-env-retry.js +16 -0
- package/dist/core/runner/runtask.d.ts +16 -31
- package/dist/core/runner/runtask.js +110 -121
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +3 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/types.d.ts +67 -164
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +11 -0
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +74 -22
- /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
- /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
|
|
443
|
-
*
|
|
444
|
-
|
|
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
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
609
|
+
* The host's DECISION FACTS — who 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
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
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
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
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
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
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
|
-
|
|
631
|
+
hostDecision: {
|
|
632
|
+
decidedBy: "person" | "sla_timeout";
|
|
633
|
+
approver?: string;
|
|
634
|
+
};
|
|
632
635
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* `"
|
|
2079
|
-
*
|
|
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" | "
|
|
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
|
-
* `"
|
|
2261
|
-
*
|
|
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" | "
|
|
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
|
|
2678
|
-
* store at all (see `TaskSpec.checkpointStore`: the per-run off switch for the durable
|
|
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
|
-
*
|
|
2682
|
-
*
|
|
2683
|
-
*
|
|
2684
|
-
* it
|
|
2685
|
-
*
|
|
2686
|
-
*
|
|
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 |
|
|
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
|
|
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
|
+
}
|