@deftai/directive-core 0.109.0 → 0.109.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/design-critique/completed-arc-record.d.ts +8 -1
  2. package/dist/design-critique/completed-arc-record.js +43 -4
  3. package/dist/hooks/classify/host-session-identity.d.ts +1 -1
  4. package/dist/hooks/classify/host-session-identity.js +23 -5
  5. package/dist/hooks/dispatcher.d.ts +17 -1
  6. package/dist/hooks/dispatcher.js +83 -7
  7. package/dist/hooks/index.d.ts +1 -0
  8. package/dist/hooks/index.js +1 -0
  9. package/dist/hooks/owner-liveness.d.ts +92 -0
  10. package/dist/hooks/owner-liveness.js +103 -0
  11. package/dist/hooks/tools.d.ts +47 -18
  12. package/dist/hooks/tools.js +82 -16
  13. package/dist/init-deposit/agent-hooks.d.ts +10 -0
  14. package/dist/init-deposit/agent-hooks.js +39 -0
  15. package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
  16. package/dist/init-deposit/host-tool-coverage.js +150 -0
  17. package/dist/init-deposit/index.d.ts +1 -0
  18. package/dist/init-deposit/index.js +1 -0
  19. package/dist/orchestration/subagent-monitor.d.ts +6 -0
  20. package/dist/orchestration/subagent-monitor.js +23 -1
  21. package/dist/session/child-occupancy.d.ts +72 -0
  22. package/dist/session/child-occupancy.js +209 -0
  23. package/dist/session/host-session-owner.d.ts +40 -0
  24. package/dist/session/host-session-owner.js +64 -5
  25. package/dist/session/index.d.ts +1 -0
  26. package/dist/session/index.js +1 -0
  27. package/dist/session/occupancy.d.ts +89 -4
  28. package/dist/session/occupancy.js +211 -31
  29. package/dist/swarm/complete-cohort.js +2 -0
  30. package/dist/swarm/pre-dispatch.js +2 -0
  31. package/dist/swarm/subagent-status-dir.d.ts +2 -1
  32. package/dist/swarm/subagent-status-dir.js +11 -2
  33. package/dist/swarm/worktrees.js +2 -0
  34. package/dist/verify-env/agent-hooks.d.ts +6 -1
  35. package/dist/verify-env/agent-hooks.js +28 -2
  36. package/package.json +3 -3
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Dispatch-recorded child occupancy leases (#3999).
3
+ *
4
+ * A parent records the child's occupancy owner and the exact worktree root at
5
+ * dispatch in `.deft/child-occupancy/` — lease-gated, not `.deft-scratch/**`.
6
+ * The orchestration terminal transition already carries agent_id / parent_id /
7
+ * phase; this store is the missing occupancy-owner datum. Release reuses
8
+ * `releaseOccupancy` under the occupancy lock and only fires when the recorded
9
+ * child is still the current owner of the recorded tree.
10
+ *
11
+ * Per identity-source kind: `host-env` children are strangers and strand —
12
+ * that is the defect. `payload` parents share one id with their children, so
13
+ * the same transition is a no-op; auto-release would drop a live parent lease
14
+ * mid-flight. Swarm close-out of the launcher's occupancy_session_id is not
15
+ * the precedent and is not copied here.
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
18
+ import { join, resolve } from "node:path";
19
+ import { containedRemove, containedWrite } from "../fs/contained-write.js";
20
+ import { hookHostIdentitySource } from "./host-session-owner.js";
21
+ import { stableJson } from "./json.js";
22
+ import { readOccupancy, releaseOccupancy } from "./occupancy.js";
23
+ export const CHILD_OCCUPANCY_SCHEMA_VERSION = 1;
24
+ export const CHILD_OCCUPANCY_DIR = [".deft", "child-occupancy"];
25
+ export const CHILD_OCCUPANCY_IDENTITY_SOURCE_KINDS = ["host-env", "payload"];
26
+ function isIdentitySourceKind(value) {
27
+ return CHILD_OCCUPANCY_IDENTITY_SOURCE_KINDS.includes(value);
28
+ }
29
+ /** Filename-safe agent id; the payload keeps the original. */
30
+ export function childOccupancyFileSegment(agentId) {
31
+ let cleaned = "";
32
+ for (const ch of agentId.trim()) {
33
+ if ((ch >= "A" && ch <= "Z") ||
34
+ (ch >= "a" && ch <= "z") ||
35
+ (ch >= "0" && ch <= "9") ||
36
+ ch === "." ||
37
+ ch === "_" ||
38
+ ch === "-") {
39
+ cleaned += ch;
40
+ }
41
+ else {
42
+ cleaned += "-";
43
+ }
44
+ }
45
+ let start = 0;
46
+ let end = cleaned.length;
47
+ while (start < end && (cleaned[start] === "-" || cleaned[start] === "."))
48
+ start += 1;
49
+ while (end > start && (cleaned[end - 1] === "-" || cleaned[end - 1] === "."))
50
+ end -= 1;
51
+ cleaned = cleaned.slice(start, end);
52
+ return cleaned.length > 0 ? cleaned : "agent";
53
+ }
54
+ export function childOccupancyRelpath(agentId) {
55
+ return [...CHILD_OCCUPANCY_DIR, `${childOccupancyFileSegment(agentId)}.json`];
56
+ }
57
+ export function childOccupancyPath(storeRoot, agentId) {
58
+ return join(resolve(storeRoot), ...childOccupancyRelpath(agentId));
59
+ }
60
+ export function childOccupancyIdentitySourceKind(host) {
61
+ const source = hookHostIdentitySource(host);
62
+ if (source === null)
63
+ return null;
64
+ return source.kind;
65
+ }
66
+ function parseChildOccupancyRecord(payload) {
67
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
68
+ return null;
69
+ const obj = payload;
70
+ const agentId = typeof obj.agent_id === "string" ? obj.agent_id.trim() : "";
71
+ const parentId = typeof obj.parent_id === "string" ? obj.parent_id.trim() : "";
72
+ const occupancyOwner = typeof obj.occupancy_owner === "string" ? obj.occupancy_owner.trim() : "";
73
+ const worktreePath = typeof obj.worktree_path === "string" ? obj.worktree_path.trim() : "";
74
+ const kindRaw = typeof obj.identity_source_kind === "string" ? obj.identity_source_kind.trim() : "";
75
+ if (agentId.length === 0 ||
76
+ parentId.length === 0 ||
77
+ occupancyOwner.length === 0 ||
78
+ worktreePath.length === 0 ||
79
+ !isIdentitySourceKind(kindRaw)) {
80
+ return null;
81
+ }
82
+ return {
83
+ schemaVersion: typeof obj.schemaVersion === "number" ? obj.schemaVersion : CHILD_OCCUPANCY_SCHEMA_VERSION,
84
+ agentId,
85
+ parentId,
86
+ occupancyOwner,
87
+ worktreePath,
88
+ identitySourceKind: kindRaw,
89
+ };
90
+ }
91
+ export function readChildOccupancyLease(storeRoot, agentId) {
92
+ const path = childOccupancyPath(storeRoot, agentId);
93
+ try {
94
+ if (!existsSync(path))
95
+ return null;
96
+ return parseChildOccupancyRecord(JSON.parse(readFileSync(path, { encoding: "utf8" })));
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /**
103
+ * Parent-only write at dispatch. Workers cannot author this store: `.deft/` is
104
+ * not assist-scratch, so a mutation write is occupancy-gated and an assist
105
+ * writer does not get the scratch carve-out.
106
+ */
107
+ export function recordChildOccupancyLease(storeRoot, input) {
108
+ const agentId = input.agentId.trim();
109
+ const parentId = input.parentId.trim();
110
+ const occupancyOwner = input.occupancyOwner.trim();
111
+ const worktreePath = resolve(input.worktreePath.trim());
112
+ if (agentId.length === 0)
113
+ throw new Error("recordChildOccupancyLease needs agentId");
114
+ if (parentId.length === 0)
115
+ throw new Error("recordChildOccupancyLease needs parentId");
116
+ if (occupancyOwner.length === 0) {
117
+ throw new Error("recordChildOccupancyLease needs occupancyOwner");
118
+ }
119
+ if (input.worktreePath.trim().length === 0) {
120
+ throw new Error("recordChildOccupancyLease needs worktreePath");
121
+ }
122
+ const record = {
123
+ schemaVersion: CHILD_OCCUPANCY_SCHEMA_VERSION,
124
+ agentId,
125
+ parentId,
126
+ occupancyOwner,
127
+ worktreePath,
128
+ identitySourceKind: input.identitySourceKind,
129
+ };
130
+ const root = resolve(storeRoot);
131
+ const relpath = childOccupancyRelpath(agentId);
132
+ mkdirSync(join(root, ...CHILD_OCCUPANCY_DIR), { recursive: true });
133
+ containedWrite({
134
+ root,
135
+ target: join(...relpath),
136
+ data: `${stableJson({
137
+ schemaVersion: record.schemaVersion,
138
+ agent_id: record.agentId,
139
+ parent_id: record.parentId,
140
+ occupancy_owner: record.occupancyOwner,
141
+ worktree_path: record.worktreePath,
142
+ identity_source_kind: record.identitySourceKind,
143
+ }, 2)}\n`,
144
+ mode: "replace",
145
+ });
146
+ return record;
147
+ }
148
+ function removeChildOccupancyLease(storeRoot, agentId) {
149
+ const root = resolve(storeRoot);
150
+ const relpath = childOccupancyRelpath(agentId);
151
+ const abs = join(root, ...relpath);
152
+ if (!existsSync(abs))
153
+ return;
154
+ containedRemove({ root, target: join(...relpath) });
155
+ }
156
+ /**
157
+ * Compare-and-release under the occupancy lock. Caller identity is the id the
158
+ * parent recorded at dispatch — not the occupant currently named in the lease
159
+ * file, and not a field on a worker-authored heartbeat.
160
+ */
161
+ export function releaseChildOccupancyOnTerminal(storeRoot, input) {
162
+ const agentId = input.agentId.trim();
163
+ if (agentId.length === 0) {
164
+ return { reason: "missing-record", record: null, occupancy: null };
165
+ }
166
+ const record = readChildOccupancyLease(storeRoot, agentId);
167
+ if (record === null) {
168
+ return { reason: "missing-record", record: null, occupancy: null };
169
+ }
170
+ if (record.identitySourceKind === "payload") {
171
+ return { reason: "payload-skip", record, occupancy: null };
172
+ }
173
+ const tree = resolve(record.worktreePath);
174
+ const now = input.now ?? new Date();
175
+ const live = readOccupancy(tree);
176
+ if (live === null) {
177
+ removeChildOccupancyLease(storeRoot, agentId);
178
+ return { reason: "already-free", record, occupancy: null };
179
+ }
180
+ if (live.sessionId !== record.occupancyOwner) {
181
+ return { reason: "owner-changed", record, occupancy: null };
182
+ }
183
+ const occupancy = releaseOccupancy(tree, {
184
+ sessionId: record.occupancyOwner,
185
+ now,
186
+ env: {},
187
+ lockDeps: input.lockDeps,
188
+ });
189
+ if (occupancy.action === "released") {
190
+ removeChildOccupancyLease(storeRoot, agentId);
191
+ if (resolve(storeRoot) !== tree)
192
+ removeChildOccupancyLease(tree, agentId);
193
+ return { reason: "released", record, occupancy };
194
+ }
195
+ return { reason: "denied", record, occupancy };
196
+ }
197
+ /**
198
+ * Worktree guesses for a heartbeat file. Canonical layout is
199
+ * `<worktree>/.deft-scratch/subagent-status/<agent>.json`; cwd is the fallback
200
+ * when the scratch dir was passed as a custom path.
201
+ */
202
+ export function worktreeCandidatesForHeartbeat(heartbeatPath, cwd) {
203
+ const fromHeartbeat = resolve(heartbeatPath, "..", "..", "..");
204
+ const fromCwd = resolve(cwd);
205
+ if (fromHeartbeat === fromCwd)
206
+ return [fromHeartbeat];
207
+ return [fromHeartbeat, fromCwd];
208
+ }
209
+ //# sourceMappingURL=child-occupancy.js.map
@@ -27,11 +27,51 @@ export type HookHostIdentitySource = {
27
27
  readonly kind: "host-env";
28
28
  readonly variable: string;
29
29
  };
30
+ /**
31
+ * Every variable a `host-env` provider publishes.
32
+ *
33
+ * Exported because the ambient step is now read by the CLI occupancy surfaces
34
+ * as well as the hook, so callers that must control the whole ambient identity
35
+ * surface — a hermetic test process, a dispatcher scrubbing a child's
36
+ * environment — need the list rather than the one variable its author knew
37
+ * about (#3954).
38
+ */
39
+ export declare const HOST_ENV_IDENTITY_VARIABLES: readonly string[];
30
40
  /** The identity source for a host, or null when the host has no contract. */
31
41
  export declare function hookHostIdentitySource(host: string): HookHostIdentitySource | null;
32
42
  /** Bound a raw host id by UTF-8 bytes, control characters and surrogate pairing. */
33
43
  export declare function isUsableHostSessionId(raw: string): boolean;
34
44
  export declare function canonicalHostSessionId(provider: HookHostIdentityProvider, rawSessionId: string): string;
45
+ /**
46
+ * The canonical owner form, derived from the provider list so every surface
47
+ * that checks it moves when a provider is added: the lifecycle-rewrite bridge
48
+ * and grant-time child validation both read this one pattern (#3873 / #3954).
49
+ * Provider ids are lowercase ASCII words, so the alternation needs no escaping.
50
+ */
51
+ export declare const CANONICAL_OWNER_PATTERN: RegExp;
52
+ /**
53
+ * True when a value claims the canonical owner shape, well-formed or not.
54
+ *
55
+ * The `host:` prefix is reserved for host-published identity, so a value under
56
+ * it that is not canonical is a malformed owner rather than an opaque id some
57
+ * session could present (#3954).
58
+ */
59
+ export declare function claimsHostSessionIdShape(value: string): boolean;
60
+ export interface HostSessionIdParts {
61
+ readonly provider: HookHostIdentityProvider;
62
+ readonly rawSessionId: string;
63
+ }
64
+ /**
65
+ * Split a canonical owner back into the provider and the raw id the host
66
+ * published, or null when the value is not canonical.
67
+ *
68
+ * The round-trip check is the point: `host:grok:v1:Z3Jvay1zZXNzaW9uLWF` decodes
69
+ * to the same raw id as `...LWE` does, so without it one session would have two
70
+ * canonical strings and a grant could name the one it never presents (#3954).
71
+ * The raw id is held to the same bound the identity surface applies, so a
72
+ * payload the host could not have published is not accepted here either.
73
+ */
74
+ export declare function parseCanonicalHostSessionId(value: string): HostSessionIdParts | null;
35
75
  export type HostEnvIdentityStatus = "ok" | "missing" | "invalid";
36
76
  export type HostEnvIdentityResolution = {
37
77
  readonly status: "ok";
@@ -17,6 +17,28 @@ const HOST_IDENTITY_SOURCES = {
17
17
  cursor: { kind: "payload", field: "conversation_id" },
18
18
  grok: { kind: "host-env", variable: "GROK_SESSION_ID" },
19
19
  };
20
+ /**
21
+ * The `host-env` half of the table above, resolved once.
22
+ *
23
+ * `ambientHostSessionOwner` reads it to build the owner list, and
24
+ * `HOST_ENV_IDENTITY_VARIABLES` projects the variable names out of the same
25
+ * entries, so the production scan and the surface a caller scrubs cannot name
26
+ * different providers (#3954).
27
+ */
28
+ const HOST_ENV_IDENTITY_ENTRIES = HOST_IDENTITY_PROVIDERS.flatMap((provider) => {
29
+ const source = HOST_IDENTITY_SOURCES[provider];
30
+ return source.kind === "host-env" ? [{ provider, variable: source.variable }] : [];
31
+ });
32
+ /**
33
+ * Every variable a `host-env` provider publishes.
34
+ *
35
+ * Exported because the ambient step is now read by the CLI occupancy surfaces
36
+ * as well as the hook, so callers that must control the whole ambient identity
37
+ * surface — a hermetic test process, a dispatcher scrubbing a child's
38
+ * environment — need the list rather than the one variable its author knew
39
+ * about (#3954).
40
+ */
41
+ export const HOST_ENV_IDENTITY_VARIABLES = HOST_ENV_IDENTITY_ENTRIES.map((entry) => entry.variable);
20
42
  /** The identity source for a host, or null when the host has no contract. */
21
43
  export function hookHostIdentitySource(host) {
22
44
  return (HOST_IDENTITY_SOURCES[host] ?? null);
@@ -56,6 +78,46 @@ export function canonicalHostSessionId(provider, rawSessionId) {
56
78
  const encoded = Buffer.from(rawSessionId, "utf8").toString("base64url");
57
79
  return `host:${provider}:v1:${encoded}`;
58
80
  }
81
+ /**
82
+ * The canonical owner form, derived from the provider list so every surface
83
+ * that checks it moves when a provider is added: the lifecycle-rewrite bridge
84
+ * and grant-time child validation both read this one pattern (#3873 / #3954).
85
+ * Provider ids are lowercase ASCII words, so the alternation needs no escaping.
86
+ */
87
+ export const CANONICAL_OWNER_PATTERN = new RegExp(`^host:(?:${HOST_IDENTITY_PROVIDERS.join("|")}):v1:[A-Za-z0-9_-]+$`);
88
+ /**
89
+ * True when a value claims the canonical owner shape, well-formed or not.
90
+ *
91
+ * The `host:` prefix is reserved for host-published identity, so a value under
92
+ * it that is not canonical is a malformed owner rather than an opaque id some
93
+ * session could present (#3954).
94
+ */
95
+ export function claimsHostSessionIdShape(value) {
96
+ return value.startsWith("host:");
97
+ }
98
+ /**
99
+ * Split a canonical owner back into the provider and the raw id the host
100
+ * published, or null when the value is not canonical.
101
+ *
102
+ * The round-trip check is the point: `host:grok:v1:Z3Jvay1zZXNzaW9uLWF` decodes
103
+ * to the same raw id as `...LWE` does, so without it one session would have two
104
+ * canonical strings and a grant could name the one it never presents (#3954).
105
+ * The raw id is held to the same bound the identity surface applies, so a
106
+ * payload the host could not have published is not accepted here either.
107
+ */
108
+ export function parseCanonicalHostSessionId(value) {
109
+ if (!CANONICAL_OWNER_PATTERN.test(value))
110
+ return null;
111
+ const segments = value.split(":");
112
+ const provider = segments[1];
113
+ const encoded = segments[3] ?? "";
114
+ const rawSessionId = Buffer.from(encoded, "base64url").toString("utf8");
115
+ if (!isUsableHostSessionId(rawSessionId))
116
+ return null;
117
+ if (canonicalHostSessionId(provider, rawSessionId) !== value)
118
+ return null;
119
+ return { provider, rawSessionId };
120
+ }
59
121
  /** Read one `host-env` provider's variable out of a process environment. */
60
122
  export function readHostEnvIdentity(environ, variable) {
61
123
  const raw = environ[variable];
@@ -76,11 +138,8 @@ export function readHostEnvIdentity(environ, variable) {
76
138
  */
77
139
  export function ambientHostSessionOwner(environ = process.env) {
78
140
  const resolved = [];
79
- for (const provider of HOST_IDENTITY_PROVIDERS) {
80
- const source = HOST_IDENTITY_SOURCES[provider];
81
- if (source.kind !== "host-env")
82
- continue;
83
- const value = readHostEnvIdentity(environ, source.variable);
141
+ for (const { provider, variable } of HOST_ENV_IDENTITY_ENTRIES) {
142
+ const value = readHostEnvIdentity(environ, variable);
84
143
  if (value.status === "ok")
85
144
  resolved.push(canonicalHostSessionId(provider, value.rawSessionId));
86
145
  }
@@ -2,6 +2,7 @@ export * from "./ac-pass-banking.js";
2
2
  export * from "./ac-pass-reuse.js";
3
3
  export * from "./active-cli.js";
4
4
  export * from "./ceremony-dial-evidence.js";
5
+ export * from "./child-occupancy.js";
5
6
  export * from "./compact-ritual.js";
6
7
  export * from "./deposit-sha.js";
7
8
  export * from "./effort-budget.js";
@@ -2,6 +2,7 @@ export * from "./ac-pass-banking.js";
2
2
  export * from "./ac-pass-reuse.js";
3
3
  export * from "./active-cli.js";
4
4
  export * from "./ceremony-dial-evidence.js";
5
+ export * from "./child-occupancy.js";
5
6
  export * from "./compact-ritual.js";
6
7
  export * from "./deposit-sha.js";
7
8
  export * from "./effort-budget.js";
@@ -19,6 +19,25 @@
19
19
  * composite hook write gate measures the tree's verified ritual owner against
20
20
  * the occupant that issued the grant, not against the writer.
21
21
  *
22
+ * Parent and child, answered per identity-source kind (#3954, and it does not
23
+ * have one answer). On a `host-env` host the parent and its dispatched children
24
+ * are different actors, because the host publishes a different id into each
25
+ * agent session. The answer there is identity, not automatic membership: each
26
+ * side resolves its own owner through the shared lookup chain below and claims
27
+ * its own worktree, which is where the dispatch envelope already puts it.
28
+ * Membership stays explicit and owner-issued for the deliberate same-tree case,
29
+ * and it stays affordable only that way -- 32 grants at a four-hour TTL against
30
+ * a twenty-minute lease means granting on every dispatch exhausts a busy
31
+ * parent's lease inside a day. The revocation trigger is therefore the owner's
32
+ * own `occupancy:grant --revoke`, or expiry; releasing a child's lease on its
33
+ * terminal event is dispatcher lifecycle in `child-occupancy.ts` (#3999).
34
+ * On a `payload` host parent and subagents share one id, so there is no foreign
35
+ * child lease to admit and nothing to grant -- and the live consequence is the
36
+ * inverse one: `owns` is true for both, so a parent's `occupancy:release`
37
+ * removes a working child's lease mid-flight with no denial. That is a property
38
+ * of shared host identity, not of this module; a bearer boundary cannot
39
+ * distinguish two processes presenting one string.
40
+ *
22
41
  * Concurrency model:
23
42
  * - Assumptions: local filesystem; cooperating processes on one machine.
24
43
  * - Guarantees: mutual exclusion under crash-free operation; detect-and-abort
@@ -237,20 +256,78 @@ export declare function formatOccupancyRemediation(record: OccupancyRecord, now?
237
256
  * derives from. A grant admits writes; the lease has one owner.
238
257
  */
239
258
  export declare function formatOccupancyMemberAdministrationRefusal(record: OccupancyRecord, grant: OccupancyGrant, verb: string): string;
259
+ /** Which step of the shared lookup chain produced the actor (#3954). */
260
+ export type PresentedIdentitySource = "explicit" | "environment" | "host" | "none";
261
+ export interface PresentedIdentity {
262
+ /** The id this surface acts under; empty when nothing was presented. */
263
+ readonly sessionId: string;
264
+ readonly source: PresentedIdentitySource;
265
+ /**
266
+ * The owner the running host published, when it names a different session
267
+ * than `sessionId` does; otherwise null. This is the claimer-versus-presenter
268
+ * split itself: the id a session claims under and the id its hook process
269
+ * presents are two different sessions (#3954).
270
+ */
271
+ readonly disagreeingHostOwner: string | null;
272
+ }
273
+ /**
274
+ * The one lookup order every occupancy surface shares (#3954): an explicit
275
+ * `--session-id`, then `DEFT_SESSION_ID`, then the owner the running host
276
+ * published.
277
+ *
278
+ * The terminal is the caller's, not this function's. Claim mints, because
279
+ * claiming establishes an identity where none exists. Release, heartbeat and
280
+ * grant/revoke are proving one, so they take the empty string and keep the
281
+ * diagnosis written for it -- a shared mint would replace "you presented
282
+ * nothing" with a plausible id no later hook will ever present, on every host
283
+ * that publishes no owner of its own.
284
+ *
285
+ * Disagreement is reported, not resolved. The order stands, so an explicit id
286
+ * beats the environment and the environment beats the host; what changes is
287
+ * that a refused caller is told the host names someone else, which is the state
288
+ * a stale inherited `DEFT_SESSION_ID` produces and the one an operator cannot
289
+ * otherwise see.
290
+ */
291
+ export declare function resolvePresentedIdentity(input?: {
292
+ readonly sessionId?: string;
293
+ readonly env?: NodeJS.ProcessEnv;
294
+ }): PresentedIdentity;
240
295
  /**
241
- * The owner a claim is made under: an explicit id, then `DEFT_SESSION_ID`, then
242
- * the id the running host published, then a mint.
296
+ * Name a claimer-versus-presenter split on a refusal, or return "" (#3954).
297
+ *
298
+ * Appended only to denials: while the caller is admitted the split costs it
299
+ * nothing, and on a refusal it is the one fact that explains why an id the
300
+ * operator believes is theirs is being treated as a stranger's.
301
+ */
302
+ export declare function formatPresentedIdentityDisagreement(identity: PresentedIdentity): string;
303
+ /**
304
+ * The owner a claim is made under: the shared lookup chain, then a mint.
243
305
  *
244
306
  * The host step is what makes an identified host's claim reachable (#3873).
245
307
  * Minting instead binds the lease to an id no later hook process can present,
246
308
  * so the session that claimed the worktree is refused by its own lease. The
247
- * mint stays as the last resort for hosts that publish nothing.
309
+ * mint stays as the last resort for hosts that publish nothing, and it is the
310
+ * one terminal the prove-surfaces deliberately do not share (#3954).
248
311
  */
249
312
  export declare function resolveOccupancySessionId(input?: ApplyOccupancyInput): string;
250
313
  export declare function readOccupancy(projectRoot: string): OccupancyRecord | null;
251
314
  export declare function liveOccupant(projectRoot: string, now?: Date, ttlMs?: number, maxLeaseMs?: number): OccupancyRecord | null;
252
315
  export declare function applyWorktreeOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
253
316
  export declare function stealOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
317
+ /**
318
+ * Release the caller's own lease.
319
+ *
320
+ * Owner-only, deliberately (#3954 item 4, answering the open question the
321
+ * design-critique arc left for the builder). Letting an unidentified caller
322
+ * release the occupant the lease file itself records would make possession of
323
+ * that file path into authority to delete a live lease, which is exactly what
324
+ * the `!expired && !owns` refusal exists to prevent -- and the cooperative
325
+ * bearer model (#3755) has no second check behind it. The unreachable printed
326
+ * recovery is fixed by the shared lookup chain instead: on a host that
327
+ * publishes an owner, the occupant now resolves itself and a bare
328
+ * `occupancy:release` is the occupant, so the message the deny prints is one
329
+ * the party it addresses can actually run.
330
+ */
254
331
  export declare function releaseOccupancy(projectRoot: string, input?: {
255
332
  readonly sessionId?: string;
256
333
  readonly env?: NodeJS.ProcessEnv;
@@ -333,7 +410,15 @@ export declare function evaluateOccupancyWriteGate(projectRoot: string, input?:
333
410
  * Never claims and never mints an owner — an unheld or foreign lease is denied.
334
411
  */
335
412
  export declare function heartbeatOccupancy(projectRoot: string, input?: ApplyOccupancyInput): OccupancyDecision;
336
- /** Close-out identity comes from the launch manifest or DEFT_SESSION_ID — never occupancy.json. */
413
+ /**
414
+ * Close-out identity comes from the launch manifest, `DEFT_SESSION_ID`, or the
415
+ * owner the running host published — never occupancy.json (#3954).
416
+ *
417
+ * Reading the lease for identity would be the anonymous recorded-occupant
418
+ * release refused in `releaseOccupancy`; the host step is the same shared
419
+ * lookup chain every other occupancy surface uses, so a cohort launched on a
420
+ * host that publishes an owner can close out without an explicit id.
421
+ */
337
422
  export declare function releaseSwarmOccupancy(projectRoot: string, input?: {
338
423
  readonly sessionId?: string;
339
424
  readonly env?: NodeJS.ProcessEnv;