@deftai/directive-core 0.108.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.
- package/dist/cache/operations.js +1 -1
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +7 -0
- package/dist/design-critique/citation-grammar.js +1 -1
- package/dist/design-critique/completed-arc-record.d.ts +8 -1
- package/dist/design-critique/completed-arc-record.js +98 -14
- package/dist/hooks/classify/host-session-identity.d.ts +25 -12
- package/dist/hooks/classify/host-session-identity.js +64 -49
- package/dist/hooks/classify/index.d.ts +2 -2
- package/dist/hooks/classify/index.js +2 -2
- package/dist/hooks/classify/paths.d.ts +2 -0
- package/dist/hooks/classify/paths.js +8 -4
- package/dist/hooks/classify/stdin.js +69 -3
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +22 -2
- package/dist/hooks/dispatcher.js +338 -66
- package/dist/hooks/fixtures/cases.d.ts +6 -0
- package/dist/hooks/fixtures/cases.js +53 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/owner-liveness.d.ts +92 -0
- package/dist/hooks/owner-liveness.js +103 -0
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +49 -2
- package/dist/hooks/tools.js +96 -1
- package/dist/init-deposit/agent-hooks.d.ts +10 -0
- package/dist/init-deposit/agent-hooks.js +39 -0
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
- package/dist/init-deposit/host-tool-coverage.js +150 -0
- package/dist/init-deposit/index.d.ts +1 -0
- package/dist/init-deposit/index.js +1 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +21 -2
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orchestration/subagent-monitor.d.ts +6 -0
- package/dist/orchestration/subagent-monitor.js +23 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +27 -0
- package/dist/orphan-active/evaluate.js +63 -6
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -0
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.js +36 -9
- package/dist/pr-wait-mergeable/wrappers.js +3 -3
- package/dist/product-first-done-gate/evaluate.js +6 -2
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +2 -0
- package/dist/release/index.js +2 -0
- package/dist/release/pipeline.js +55 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +6 -0
- package/dist/review-monitor/github-lease.js +4 -3
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +41 -11
- package/dist/scm/gh-rest.d.ts +25 -12
- package/dist/scm/gh-rest.js +65 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/child-occupancy.d.ts +72 -0
- package/dist/session/child-occupancy.js +209 -0
- package/dist/session/host-session-owner.d.ts +93 -0
- package/dist/session/host-session-owner.js +148 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/occupancy.d.ts +107 -2
- package/dist/session/occupancy.js +272 -34
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/swarm/complete-cohort.js +2 -0
- package/dist/swarm/pre-dispatch.js +2 -0
- package/dist/swarm/subagent-status-dir.d.ts +2 -1
- package/dist/swarm/subagent-status-dir.js +11 -2
- package/dist/swarm/worktrees.js +2 -0
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-env/agent-hooks.d.ts +6 -1
- package/dist/verify-env/agent-hooks.js +28 -2
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +3 -3
|
@@ -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
|
|
@@ -40,6 +59,8 @@ import { containedRemove, containedWrite } from "../fs/contained-write.js";
|
|
|
40
59
|
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
41
60
|
import { assertAppendLockOwned, withAppendLock } from "../slice/lock.js";
|
|
42
61
|
import { SWARM_WORKER_ROLES } from "../swarm/routing.js";
|
|
62
|
+
import { recordChildOccupancyLease } from "./child-occupancy.js";
|
|
63
|
+
import { ambientHostSessionOwner, claimsHostSessionIdShape, parseCanonicalHostSessionId, } from "./host-session-owner.js";
|
|
43
64
|
import { stableJson } from "./json.js";
|
|
44
65
|
import { parseTimestamp, timestampIso } from "./time.js";
|
|
45
66
|
export const OCCUPANCY_SCHEMA_VERSION = 1;
|
|
@@ -166,6 +187,19 @@ export function occupancyAdmission(record, sessionId, now = new Date()) {
|
|
|
166
187
|
return "owner";
|
|
167
188
|
return occupancyGrantFor(record, presented, now) === null ? "stranger" : "member";
|
|
168
189
|
}
|
|
190
|
+
// Session ids reach remediation text from operator flags, host environments and
|
|
191
|
+
// whatever a peer wrote into the lease, so a value can carry whitespace or shell
|
|
192
|
+
// metacharacters. Only a value a shell would take as one bare token is inlined
|
|
193
|
+
// into a printed command; anything else keeps its placeholder, because the right
|
|
194
|
+
// quoting differs per shell and a mis-parsed copyable command is worse than one
|
|
195
|
+
// the reader has to fill in. The id itself is still named in the prose above.
|
|
196
|
+
// A leading dash is excluded as well: every CLI parser here reads such a value
|
|
197
|
+
// as another option, so `--occupant --weird-id` fails argument parsing even
|
|
198
|
+
// though the shell itself would have passed the token through intact.
|
|
199
|
+
const SHELL_SAFE_SESSION_ID = /^(?!-)[A-Za-z0-9_.:+=,/-]+$/;
|
|
200
|
+
function commandSessionId(sessionId, placeholder) {
|
|
201
|
+
return SHELL_SAFE_SESSION_ID.test(sessionId) ? sessionId : placeholder;
|
|
202
|
+
}
|
|
169
203
|
function occupancyClockLine(record) {
|
|
170
204
|
const lastWrite = record.lastWriteAt === null ? "" : ` last_write_at=${timestampIso(record.lastWriteAt)}`;
|
|
171
205
|
return `claimed_at=${timestampIso(record.claimedAt)} heartbeat_at=${timestampIso(record.heartbeatAt)}${lastWrite}`;
|
|
@@ -178,7 +212,7 @@ export function formatOccupancyStaleWarning(record, now = new Date(), ttlMs = OC
|
|
|
178
212
|
const age = heartbeatAgeSeconds(record, now);
|
|
179
213
|
return (`Occupancy lease for session ${record.sessionId} has not beaten for ${age}s of its ` +
|
|
180
214
|
`${Math.round(ttlMs / 1000)}s window; another session may read it as abandoned. ` +
|
|
181
|
-
`Refresh it with \`deft occupancy:heartbeat --session-id=${record.sessionId}\`.`);
|
|
215
|
+
`Refresh it with \`deft occupancy:heartbeat --session-id=${commandSessionId(record.sessionId, "<your-session-id>")}\`.`);
|
|
182
216
|
}
|
|
183
217
|
/**
|
|
184
218
|
* Tell the holder its lease aged out of the absolute cap (#3599). Distinct
|
|
@@ -191,17 +225,45 @@ export function formatOccupancyAgeCapRemediation(record, now = new Date(), maxLe
|
|
|
191
225
|
`(claimed ${leaseAgeSeconds(record, now)}s ago, ${occupancyClockLine(record)}), so this ` +
|
|
192
226
|
"worktree is no longer held and a peer may claim it at any moment. Heartbeats cannot " +
|
|
193
227
|
"extend a capped lease — re-claim the worktree with " +
|
|
194
|
-
`\`deft session:start --session-id=${record.sessionId}\` before writing again.`);
|
|
228
|
+
`\`deft session:start --session-id=${commandSessionId(record.sessionId, "<your-session-id>")}\` before writing again.`);
|
|
195
229
|
}
|
|
196
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Tell a refused caller who holds the lease and what it can actually run.
|
|
232
|
+
*
|
|
233
|
+
* `presented` is the id the refused caller offered (#3873). Without it the
|
|
234
|
+
* message can only print `<your-session-id>` placeholders, which is fine for a
|
|
235
|
+
* CLI caller that passed its own `--session-id` and useless to a hook process,
|
|
236
|
+
* which does not know what identity it presented. Passing it also keeps the
|
|
237
|
+
* message honest when there is none: a grant cannot name an empty child --
|
|
238
|
+
* `occupancy:grant --child-session-id=` is refused at parse and at membership --
|
|
239
|
+
* so that remediation is not printed to a caller who could never run it.
|
|
240
|
+
*/
|
|
241
|
+
export function formatOccupancyRemediation(record, now = new Date(), presented) {
|
|
197
242
|
const age = heartbeatAgeSeconds(record, now);
|
|
198
|
-
|
|
199
|
-
`${formatLastWritePhrase(record, now)}, ${occupancyClockLine(record)}).\n
|
|
243
|
+
const header = `Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago, ` +
|
|
244
|
+
`${formatLastWritePhrase(record, now)}, ${occupancyClockLine(record)}).\n`;
|
|
245
|
+
const tail = "\nThe occupant may release (`occupancy:release` / `session:end`).";
|
|
246
|
+
if (presented === undefined) {
|
|
247
|
+
return (`${header}Stay read-only (\`session:start --read-only\`), use another worktree,\n` +
|
|
248
|
+
"ask the occupant for a write grant (`occupancy:grant --child-session-id=<your-session-id> " +
|
|
249
|
+
"--role <worker-role>`, run by the occupant), or run a confirmed owner transition " +
|
|
250
|
+
`(\`session:start --steal --confirm --occupant <reported-session-id> --session-id=<your-session-id>\`).${tail}`);
|
|
251
|
+
}
|
|
252
|
+
const actor = presented.trim();
|
|
253
|
+
const occupantArg = commandSessionId(record.sessionId, "<reported-session-id>");
|
|
254
|
+
if (actor.length === 0) {
|
|
255
|
+
return (`${header}This process presented no session identity, so a write grant cannot name it ` +
|
|
256
|
+
"and an owner transition would not be recognised on its next write.\n" +
|
|
257
|
+
"Stay read-only (`session:start --read-only`), use another worktree, or ask the occupant " +
|
|
258
|
+
`to release the lease (\`occupancy:release --session-id=${occupantArg}\` / \`session:end\`).${tail}`);
|
|
259
|
+
}
|
|
260
|
+
const actorArg = commandSessionId(actor, "<your-session-id>");
|
|
261
|
+
return (`${header}This process presented session ${actor}, which neither holds that lease nor has a ` +
|
|
262
|
+
"write grant on it.\n" +
|
|
200
263
|
"Stay read-only (`session:start --read-only`), use another worktree,\n" +
|
|
201
|
-
|
|
264
|
+
`ask the occupant for a write grant (\`occupancy:grant --child-session-id=${actorArg} ` +
|
|
202
265
|
"--role <worker-role>`, run by the occupant), or run a confirmed owner transition " +
|
|
203
|
-
|
|
204
|
-
"The occupant may release (`occupancy:release` / `session:end`).");
|
|
266
|
+
`(\`session:start --steal --confirm --occupant ${occupantArg} --session-id=${actorArg}\`).${tail}`);
|
|
205
267
|
}
|
|
206
268
|
/**
|
|
207
269
|
* Refuse an administrative verb to a granted child (#3755). Named apart from
|
|
@@ -215,13 +277,74 @@ export function formatOccupancyMemberAdministrationRefusal(record, grant, verb)
|
|
|
215
277
|
"grant never escalates into administration.\n" +
|
|
216
278
|
`Ask the occupant (session ${record.sessionId}) to run it, or wait for the grant to expire.`);
|
|
217
279
|
}
|
|
218
|
-
|
|
280
|
+
/**
|
|
281
|
+
* The one lookup order every occupancy surface shares (#3954): an explicit
|
|
282
|
+
* `--session-id`, then `DEFT_SESSION_ID`, then the owner the running host
|
|
283
|
+
* published.
|
|
284
|
+
*
|
|
285
|
+
* The terminal is the caller's, not this function's. Claim mints, because
|
|
286
|
+
* claiming establishes an identity where none exists. Release, heartbeat and
|
|
287
|
+
* grant/revoke are proving one, so they take the empty string and keep the
|
|
288
|
+
* diagnosis written for it -- a shared mint would replace "you presented
|
|
289
|
+
* nothing" with a plausible id no later hook will ever present, on every host
|
|
290
|
+
* that publishes no owner of its own.
|
|
291
|
+
*
|
|
292
|
+
* Disagreement is reported, not resolved. The order stands, so an explicit id
|
|
293
|
+
* beats the environment and the environment beats the host; what changes is
|
|
294
|
+
* that a refused caller is told the host names someone else, which is the state
|
|
295
|
+
* a stale inherited `DEFT_SESSION_ID` produces and the one an operator cannot
|
|
296
|
+
* otherwise see.
|
|
297
|
+
*/
|
|
298
|
+
export function resolvePresentedIdentity(input = {}) {
|
|
299
|
+
const env = input.env ?? process.env;
|
|
300
|
+
const hostOwner = ambientHostSessionOwner(env);
|
|
301
|
+
const disagreement = (chosen) => hostOwner !== null && hostOwner !== chosen ? hostOwner : null;
|
|
219
302
|
const explicit = input.sessionId?.trim();
|
|
220
|
-
if (explicit)
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
303
|
+
if (explicit) {
|
|
304
|
+
return {
|
|
305
|
+
sessionId: explicit,
|
|
306
|
+
source: "explicit",
|
|
307
|
+
disagreeingHostOwner: disagreement(explicit),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const envId = env.DEFT_SESSION_ID?.trim();
|
|
311
|
+
if (envId) {
|
|
312
|
+
return { sessionId: envId, source: "environment", disagreeingHostOwner: disagreement(envId) };
|
|
313
|
+
}
|
|
314
|
+
if (hostOwner !== null) {
|
|
315
|
+
return { sessionId: hostOwner, source: "host", disagreeingHostOwner: null };
|
|
316
|
+
}
|
|
317
|
+
return { sessionId: "", source: "none", disagreeingHostOwner: null };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Name a claimer-versus-presenter split on a refusal, or return "" (#3954).
|
|
321
|
+
*
|
|
322
|
+
* Appended only to denials: while the caller is admitted the split costs it
|
|
323
|
+
* nothing, and on a refusal it is the one fact that explains why an id the
|
|
324
|
+
* operator believes is theirs is being treated as a stranger's.
|
|
325
|
+
*/
|
|
326
|
+
export function formatPresentedIdentityDisagreement(identity) {
|
|
327
|
+
const other = identity.disagreeingHostOwner;
|
|
328
|
+
if (other === null)
|
|
329
|
+
return "";
|
|
330
|
+
const named = identity.source === "explicit" ? "The id passed on the command line" : "DEFT_SESSION_ID";
|
|
331
|
+
return (`\n${named} names session ${identity.sessionId}, but this host published ` +
|
|
332
|
+
`${other}. Those are different sessions: re-run with ` +
|
|
333
|
+
`\`--session-id=${commandSessionId(other, "<host-published-id>")}\` to act as the host owner.`);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The owner a claim is made under: the shared lookup chain, then a mint.
|
|
337
|
+
*
|
|
338
|
+
* The host step is what makes an identified host's claim reachable (#3873).
|
|
339
|
+
* Minting instead binds the lease to an id no later hook process can present,
|
|
340
|
+
* so the session that claimed the worktree is refused by its own lease. The
|
|
341
|
+
* mint stays as the last resort for hosts that publish nothing, and it is the
|
|
342
|
+
* one terminal the prove-surfaces deliberately do not share (#3954).
|
|
343
|
+
*/
|
|
344
|
+
export function resolveOccupancySessionId(input = {}) {
|
|
345
|
+
const presented = resolvePresentedIdentity(input).sessionId;
|
|
346
|
+
if (presented.length > 0)
|
|
347
|
+
return presented;
|
|
225
348
|
return (input.newSessionId ?? randomUUID)();
|
|
226
349
|
}
|
|
227
350
|
export function readOccupancy(projectRoot) {
|
|
@@ -263,7 +386,10 @@ export function applyWorktreeOccupancy(projectRoot, input = {}) {
|
|
|
263
386
|
sessionId: incoming,
|
|
264
387
|
record: live,
|
|
265
388
|
path,
|
|
266
|
-
|
|
389
|
+
// A granted child reads the member refusal rather than an offer of the
|
|
390
|
+
// write grant it already holds (#3954 item 5): membership admits writes,
|
|
391
|
+
// and claiming the lease stays owner-only.
|
|
392
|
+
message: membershipOwnerDenial(live, incoming, now, "session:start"),
|
|
267
393
|
code: 1,
|
|
268
394
|
};
|
|
269
395
|
}
|
|
@@ -288,7 +414,7 @@ export function applyWorktreeOccupancy(projectRoot, input = {}) {
|
|
|
288
414
|
sessionId: incoming,
|
|
289
415
|
record: liveLocked,
|
|
290
416
|
path,
|
|
291
|
-
message:
|
|
417
|
+
message: membershipOwnerDenial(liveLocked, incoming, now, "session:start"),
|
|
292
418
|
code: 1,
|
|
293
419
|
};
|
|
294
420
|
}
|
|
@@ -309,6 +435,9 @@ export function applyWorktreeOccupancy(projectRoot, input = {}) {
|
|
|
309
435
|
grants: liveLocked === null ? [] : liveOccupancyGrants(liveLocked, now),
|
|
310
436
|
}, fence);
|
|
311
437
|
const action = liveLocked !== null ? "heartbeat" : "claimed";
|
|
438
|
+
if (action === "claimed") {
|
|
439
|
+
maybeRecordChildOccupancyOnClaim(projectRoot, incoming, input.env);
|
|
440
|
+
}
|
|
312
441
|
return {
|
|
313
442
|
action,
|
|
314
443
|
sessionId: record.sessionId,
|
|
@@ -321,6 +450,32 @@ export function applyWorktreeOccupancy(projectRoot, input = {}) {
|
|
|
321
450
|
};
|
|
322
451
|
}, input.lockDeps);
|
|
323
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Stamp the dispatch-recorded child occupancy store at claim time (#3999).
|
|
455
|
+
* Pre-dispatch / worktree mkdir cannot know a host-env child's occupancy
|
|
456
|
+
* owner; the claiming process does. Heartbeat `agent_id` on this host is the
|
|
457
|
+
* raw GROK_SESSION_ID, so that is the store key the terminal monitor looks up.
|
|
458
|
+
*/
|
|
459
|
+
function maybeRecordChildOccupancyOnClaim(projectRoot, occupancyOwner, env) {
|
|
460
|
+
const resolved = env ?? process.env;
|
|
461
|
+
const grokRaw = resolved.GROK_SESSION_ID?.trim() ?? "";
|
|
462
|
+
const agentId = grokRaw.length > 0 ? grokRaw : occupancyHost(env);
|
|
463
|
+
if (agentId.length === 0 || agentId === "none")
|
|
464
|
+
return;
|
|
465
|
+
const parentId = occupancyAddress(env);
|
|
466
|
+
try {
|
|
467
|
+
recordChildOccupancyLease(projectRoot, {
|
|
468
|
+
agentId,
|
|
469
|
+
parentId,
|
|
470
|
+
occupancyOwner,
|
|
471
|
+
worktreePath: resolve(projectRoot),
|
|
472
|
+
identitySourceKind: grokRaw.length > 0 ? "host-env" : "payload",
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// Claim already succeeded; a missing dispatch record is a no-op on terminal.
|
|
477
|
+
}
|
|
478
|
+
}
|
|
324
479
|
export function stealOccupancy(projectRoot, input = {}) {
|
|
325
480
|
const now = input.now ?? new Date();
|
|
326
481
|
const path = occupancyPath(projectRoot);
|
|
@@ -451,10 +606,26 @@ export function stealOccupancy(projectRoot, input = {}) {
|
|
|
451
606
|
};
|
|
452
607
|
}, input.lockDeps);
|
|
453
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Release the caller's own lease.
|
|
611
|
+
*
|
|
612
|
+
* Owner-only, deliberately (#3954 item 4, answering the open question the
|
|
613
|
+
* design-critique arc left for the builder). Letting an unidentified caller
|
|
614
|
+
* release the occupant the lease file itself records would make possession of
|
|
615
|
+
* that file path into authority to delete a live lease, which is exactly what
|
|
616
|
+
* the `!expired && !owns` refusal exists to prevent -- and the cooperative
|
|
617
|
+
* bearer model (#3755) has no second check behind it. The unreachable printed
|
|
618
|
+
* recovery is fixed by the shared lookup chain instead: on a host that
|
|
619
|
+
* publishes an owner, the occupant now resolves itself and a bare
|
|
620
|
+
* `occupancy:release` is the occupant, so the message the deny prints is one
|
|
621
|
+
* the party it addresses can actually run.
|
|
622
|
+
*/
|
|
454
623
|
export function releaseOccupancy(projectRoot, input = {}) {
|
|
455
624
|
const now = input.now ?? new Date();
|
|
456
625
|
const path = occupancyPath(projectRoot);
|
|
457
|
-
const
|
|
626
|
+
const identity = resolvePresentedIdentity(input);
|
|
627
|
+
const caller = identity.sessionId;
|
|
628
|
+
const split = formatPresentedIdentityDisagreement(identity);
|
|
458
629
|
return withOccupancyLock(projectRoot, (fence) => {
|
|
459
630
|
const existing = readOccupancy(projectRoot);
|
|
460
631
|
if (existing === null) {
|
|
@@ -475,7 +646,7 @@ export function releaseOccupancy(projectRoot, input = {}) {
|
|
|
475
646
|
sessionId: caller,
|
|
476
647
|
record: existing,
|
|
477
648
|
path,
|
|
478
|
-
message: membershipOwnerDenial(existing, caller, now, "occupancy:release"),
|
|
649
|
+
message: membershipOwnerDenial(existing, caller, now, "occupancy:release") + split,
|
|
479
650
|
code: 1,
|
|
480
651
|
};
|
|
481
652
|
}
|
|
@@ -500,7 +671,7 @@ export function releaseOccupancy(projectRoot, input = {}) {
|
|
|
500
671
|
sessionId: caller,
|
|
501
672
|
record: still,
|
|
502
673
|
path,
|
|
503
|
-
message: membershipOwnerDenial(still, caller, now, "occupancy:release"),
|
|
674
|
+
message: membershipOwnerDenial(still, caller, now, "occupancy:release") + split,
|
|
504
675
|
code: 1,
|
|
505
676
|
};
|
|
506
677
|
}
|
|
@@ -515,10 +686,18 @@ export function releaseOccupancy(projectRoot, input = {}) {
|
|
|
515
686
|
};
|
|
516
687
|
}, input.lockDeps);
|
|
517
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Refuse an owner-only verb, saying which of the three the caller is.
|
|
691
|
+
*
|
|
692
|
+
* The caller is passed to `formatOccupancyRemediation` as the presented id
|
|
693
|
+
* (#3954): every one of these surfaces now resolves an actor the caller may not
|
|
694
|
+
* have chosen explicitly, so a refusal that does not name what was presented
|
|
695
|
+
* leaves it guessing which identity it was refused under.
|
|
696
|
+
*/
|
|
518
697
|
function membershipOwnerDenial(live, caller, now, verb) {
|
|
519
698
|
const grant = occupancyGrantFor(live, caller, now);
|
|
520
699
|
return grant === null
|
|
521
|
-
? formatOccupancyRemediation(live, now)
|
|
700
|
+
? formatOccupancyRemediation(live, now, caller)
|
|
522
701
|
: formatOccupancyMemberAdministrationRefusal(live, grant, verb);
|
|
523
702
|
}
|
|
524
703
|
/**
|
|
@@ -533,7 +712,9 @@ function membershipOwnerDenial(live, caller, now, verb) {
|
|
|
533
712
|
export function grantOccupancyMembership(projectRoot, input = {}) {
|
|
534
713
|
const now = input.now ?? new Date();
|
|
535
714
|
const path = occupancyPath(projectRoot);
|
|
536
|
-
const
|
|
715
|
+
const identity = resolvePresentedIdentity(input);
|
|
716
|
+
const owner = identity.sessionId;
|
|
717
|
+
const split = formatPresentedIdentityDisagreement(identity);
|
|
537
718
|
const child = input.childSessionId?.trim() ?? "";
|
|
538
719
|
const role = input.role?.trim() ?? "";
|
|
539
720
|
if (owner.length === 0) {
|
|
@@ -569,6 +750,46 @@ export function grantOccupancyMembership(projectRoot, input = {}) {
|
|
|
569
750
|
code: 2,
|
|
570
751
|
};
|
|
571
752
|
}
|
|
753
|
+
// #3954 item 3. A child id under the reserved `host:` prefix must be a
|
|
754
|
+
// well-formed canonical owner: measured, `host:nosuchhost:v9:zzzz` and
|
|
755
|
+
// `host:grok:v1:!!!not-base64url!!!` were granted and then admitted as
|
|
756
|
+
// `member` by the write gate, so the lease read as membership while admitting
|
|
757
|
+
// nobody. An id outside that prefix is still accepted, because a child on a
|
|
758
|
+
// host with no identity contract presents whatever `DEFT_SESSION_ID` holds
|
|
759
|
+
// and refusing that would deny a grant nothing has measured wrong.
|
|
760
|
+
if (claimsHostSessionIdShape(child) && parseCanonicalHostSessionId(child) === null) {
|
|
761
|
+
return {
|
|
762
|
+
action: "denied",
|
|
763
|
+
sessionId: owner,
|
|
764
|
+
record: readOccupancy(projectRoot),
|
|
765
|
+
path,
|
|
766
|
+
message: `occupancy:grant refuses the child id ${child}: the \`host:\` prefix is reserved for ` +
|
|
767
|
+
"host-published identity, and this is not a well-formed owner " +
|
|
768
|
+
"(`host:<provider>:v1:<base64url>`), so no session could ever present it. Pass the id " +
|
|
769
|
+
"the child's own host publishes, or an opaque id the child sets as DEFT_SESSION_ID.",
|
|
770
|
+
code: 2,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
// Same defect one step subtler: re-prefixing the owner's own payload under a
|
|
774
|
+
// second provider passes the shape check and is a self-grant in disguise --
|
|
775
|
+
// no session on that other host would present it, and the owner already holds
|
|
776
|
+
// the lease outright.
|
|
777
|
+
const childParts = parseCanonicalHostSessionId(child);
|
|
778
|
+
const ownerParts = parseCanonicalHostSessionId(owner);
|
|
779
|
+
if (childParts !== null &&
|
|
780
|
+
ownerParts !== null &&
|
|
781
|
+
childParts.rawSessionId === ownerParts.rawSessionId) {
|
|
782
|
+
return {
|
|
783
|
+
action: "denied",
|
|
784
|
+
sessionId: owner,
|
|
785
|
+
record: readOccupancy(projectRoot),
|
|
786
|
+
path,
|
|
787
|
+
message: `occupancy:grant refuses the child id ${child}: it carries this lease owner's own host ` +
|
|
788
|
+
`session (${ownerParts.rawSessionId}) under provider ${childParts.provider}. That is a ` +
|
|
789
|
+
"self-grant across a provider prefix, and the child it names cannot exist.",
|
|
790
|
+
code: 2,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
572
793
|
if (!SWARM_WORKER_ROLES.includes(role)) {
|
|
573
794
|
return {
|
|
574
795
|
action: "denied",
|
|
@@ -596,7 +817,7 @@ export function grantOccupancyMembership(projectRoot, input = {}) {
|
|
|
596
817
|
message: capped && current !== null
|
|
597
818
|
? formatOccupancyAgeCapRemediation(current, now)
|
|
598
819
|
: "occupancy:grant found no live lease to grant on. A grant is derived authority, " +
|
|
599
|
-
`so claim the worktree first with \`deft session:start --session-id=${owner}\`.`,
|
|
820
|
+
`so claim the worktree first with \`deft session:start --session-id=${commandSessionId(owner, "<your-session-id>")}\`.`,
|
|
600
821
|
code: 1,
|
|
601
822
|
};
|
|
602
823
|
}
|
|
@@ -606,7 +827,7 @@ export function grantOccupancyMembership(projectRoot, input = {}) {
|
|
|
606
827
|
sessionId: owner,
|
|
607
828
|
record: live,
|
|
608
829
|
path,
|
|
609
|
-
message: membershipOwnerDenial(live, owner, now, "occupancy:grant"),
|
|
830
|
+
message: membershipOwnerDenial(live, owner, now, "occupancy:grant") + split,
|
|
610
831
|
code: 1,
|
|
611
832
|
};
|
|
612
833
|
}
|
|
@@ -687,7 +908,11 @@ export function grantOccupancyMembership(projectRoot, input = {}) {
|
|
|
687
908
|
export function revokeOccupancyMembership(projectRoot, input = {}) {
|
|
688
909
|
const now = input.now ?? new Date();
|
|
689
910
|
const path = occupancyPath(projectRoot);
|
|
690
|
-
const
|
|
911
|
+
const identity = resolvePresentedIdentity(input);
|
|
912
|
+
const owner = identity.sessionId;
|
|
913
|
+
const split = formatPresentedIdentityDisagreement(identity);
|
|
914
|
+
// Revoke deliberately skips the grant-time child-id checks: a malformed grant
|
|
915
|
+
// written before those checks existed must stay withdrawable (#3954 item 3).
|
|
691
916
|
const child = input.childSessionId?.trim() ?? "";
|
|
692
917
|
if (owner.length === 0 || child.length === 0) {
|
|
693
918
|
return {
|
|
@@ -720,7 +945,7 @@ export function revokeOccupancyMembership(projectRoot, input = {}) {
|
|
|
720
945
|
sessionId: owner,
|
|
721
946
|
record: live,
|
|
722
947
|
path,
|
|
723
|
-
message: membershipOwnerDenial(live, owner, now, "occupancy:grant --revoke"),
|
|
948
|
+
message: membershipOwnerDenial(live, owner, now, "occupancy:grant --revoke") + split,
|
|
724
949
|
code: 1,
|
|
725
950
|
};
|
|
726
951
|
}
|
|
@@ -809,7 +1034,10 @@ export function evaluateOccupancyWriteGate(projectRoot, input = {}) {
|
|
|
809
1034
|
if (admission === "stranger") {
|
|
810
1035
|
return {
|
|
811
1036
|
allow: false,
|
|
812
|
-
|
|
1037
|
+
// The refused caller is told what identity it actually presented (#3873).
|
|
1038
|
+
// A hook process cannot otherwise know, and the grant this message offers
|
|
1039
|
+
// is only runnable when the occupant can name a non-empty child.
|
|
1040
|
+
message: formatOccupancyRemediation(live, now, incoming),
|
|
813
1041
|
occupant: live,
|
|
814
1042
|
refreshed: false,
|
|
815
1043
|
warning: null,
|
|
@@ -948,7 +1176,9 @@ memberSessionId) {
|
|
|
948
1176
|
export function heartbeatOccupancy(projectRoot, input = {}) {
|
|
949
1177
|
const now = input.now ?? new Date();
|
|
950
1178
|
const path = occupancyPath(projectRoot);
|
|
951
|
-
const
|
|
1179
|
+
const identity = resolvePresentedIdentity(input);
|
|
1180
|
+
const caller = identity.sessionId;
|
|
1181
|
+
const split = formatPresentedIdentityDisagreement(identity);
|
|
952
1182
|
if (caller.length === 0) {
|
|
953
1183
|
return {
|
|
954
1184
|
action: "denied",
|
|
@@ -974,7 +1204,7 @@ export function heartbeatOccupancy(projectRoot, input = {}) {
|
|
|
974
1204
|
message: capped && existing !== null
|
|
975
1205
|
? formatOccupancyAgeCapRemediation(existing, now)
|
|
976
1206
|
: "occupancy:heartbeat found no live lease to refresh. Claim one with " +
|
|
977
|
-
`\`deft session:start --session-id=${caller}\`.`,
|
|
1207
|
+
`\`deft session:start --session-id=${commandSessionId(caller, "<your-session-id>")}\`.`,
|
|
978
1208
|
code: 1,
|
|
979
1209
|
};
|
|
980
1210
|
}
|
|
@@ -984,7 +1214,7 @@ export function heartbeatOccupancy(projectRoot, input = {}) {
|
|
|
984
1214
|
sessionId: caller,
|
|
985
1215
|
record: live,
|
|
986
1216
|
path,
|
|
987
|
-
message: membershipOwnerDenial(live, caller, now, "occupancy:heartbeat"),
|
|
1217
|
+
message: membershipOwnerDenial(live, caller, now, "occupancy:heartbeat") + split,
|
|
988
1218
|
code: 1,
|
|
989
1219
|
};
|
|
990
1220
|
}
|
|
@@ -1014,10 +1244,18 @@ export function heartbeatOccupancy(projectRoot, input = {}) {
|
|
|
1014
1244
|
code: 0,
|
|
1015
1245
|
};
|
|
1016
1246
|
}
|
|
1017
|
-
/**
|
|
1247
|
+
/**
|
|
1248
|
+
* Close-out identity comes from the launch manifest, `DEFT_SESSION_ID`, or the
|
|
1249
|
+
* owner the running host published — never occupancy.json (#3954).
|
|
1250
|
+
*
|
|
1251
|
+
* Reading the lease for identity would be the anonymous recorded-occupant
|
|
1252
|
+
* release refused in `releaseOccupancy`; the host step is the same shared
|
|
1253
|
+
* lookup chain every other occupancy surface uses, so a cohort launched on a
|
|
1254
|
+
* host that publishes an owner can close out without an explicit id.
|
|
1255
|
+
*/
|
|
1018
1256
|
export function releaseSwarmOccupancy(projectRoot, input = {}) {
|
|
1019
1257
|
const env = input.env ?? process.env;
|
|
1020
|
-
const sessionId = input.sessionId
|
|
1258
|
+
const sessionId = resolvePresentedIdentity({ sessionId: input.sessionId, env }).sessionId;
|
|
1021
1259
|
if (sessionId.length === 0) {
|
|
1022
1260
|
const occupant = readOccupancy(projectRoot);
|
|
1023
1261
|
return {
|
|
@@ -1025,9 +1263,9 @@ export function releaseSwarmOccupancy(projectRoot, input = {}) {
|
|
|
1025
1263
|
sessionId: "",
|
|
1026
1264
|
record: occupant,
|
|
1027
1265
|
path: occupancyPath(projectRoot),
|
|
1028
|
-
message: "swarm close-out has no occupancy_session_id (manifest missing or predates the field) " +
|
|
1029
|
-
"
|
|
1030
|
-
"session:start --steal --confirm --occupant <reported-session-id> " +
|
|
1266
|
+
message: "swarm close-out has no occupancy_session_id (manifest missing or predates the field), " +
|
|
1267
|
+
"DEFT_SESSION_ID is unset, and this host published no owner. Re-establish an aligned " +
|
|
1268
|
+
"owner with session:start --steal --confirm --occupant <reported-session-id> " +
|
|
1031
1269
|
"--session-id=<your-session-id>.",
|
|
1032
1270
|
code: 1,
|
|
1033
1271
|
};
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Disk-backed so check composition (spawned verify:ac) can serve from_cache
|
|
5
5
|
* after a green run in the same session when plan + product hash still match.
|
|
6
6
|
*/
|
|
7
|
-
export declare const VERIFY_AC_SESSION_CACHE_DIR = ".deft/verify-ac-session-cache";
|
|
8
|
-
export declare const AC_SERVED_FROM: readonly ["bank", "cache", "executed"];
|
|
7
|
+
export declare const VERIFY_AC_SESSION_CACHE_DIR = ".deft/cache/verify-ac-session-cache";
|
|
8
|
+
export declare const AC_SERVED_FROM: readonly ["bank", "cache", "executed", "refused"];
|
|
9
9
|
export type AcServedFrom = (typeof AC_SERVED_FROM)[number];
|
|
10
10
|
export interface CachedVerifyAcSnapshot {
|
|
11
11
|
readonly ok: boolean;
|
|
@@ -8,8 +8,8 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { containedWrite } from "../fs/contained-write.js";
|
|
10
10
|
import { sanitizeScopeIdForFilename } from "./ac-pass-banking.js";
|
|
11
|
-
export const VERIFY_AC_SESSION_CACHE_DIR = ".deft/verify-ac-session-cache";
|
|
12
|
-
export const AC_SERVED_FROM = ["bank", "cache", "executed"];
|
|
11
|
+
export const VERIFY_AC_SESSION_CACHE_DIR = ".deft/cache/verify-ac-session-cache";
|
|
12
|
+
export const AC_SERVED_FROM = ["bank", "cache", "executed", "refused"];
|
|
13
13
|
function asRecord(value) {
|
|
14
14
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
15
15
|
return value;
|
|
@@ -496,6 +496,8 @@ export function completeCohort(args) {
|
|
|
496
496
|
}
|
|
497
497
|
}
|
|
498
498
|
if (result.ok && sessionId.length > 0) {
|
|
499
|
+
// Launcher occupancy only. Child owner leases release on the
|
|
500
|
+
// orchestration terminal transition (#3999), not here.
|
|
499
501
|
const released = releaseSwarmOccupancy(projectRoot, {
|
|
500
502
|
env,
|
|
501
503
|
sessionId,
|
|
@@ -177,6 +177,8 @@ function runBegin(input) {
|
|
|
177
177
|
// heuristic, so accept an on-disk worktree as the second arming signal.
|
|
178
178
|
const armTarget = resolve(input.projectRoot, input.targetId.trim());
|
|
179
179
|
if (looksLikeFilesystemTarget(input.targetId) || looksLikeWorktreeDir(armTarget)) {
|
|
180
|
+
// Heartbeat dir only. Child occupancy owner is recorded at claim time
|
|
181
|
+
// (#3999): this parent process does not know a host-env child's id.
|
|
180
182
|
ensureSubagentStatusDir(armTarget);
|
|
181
183
|
}
|
|
182
184
|
// Exclusive lock → reload → evaluate → begin+save (same decision under lock;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* worktree create and at pre-dispatch begin so a later missing record is
|
|
7
7
|
* exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
|
|
8
8
|
*/
|
|
9
|
+
import { type ChildOccupancyDispatchInput } from "../session/child-occupancy.js";
|
|
9
10
|
/**
|
|
10
11
|
* True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
|
|
11
12
|
* entry (a file for `git worktree add` trees, a directory for the main clone).
|
|
@@ -23,5 +24,5 @@ export declare function looksLikeWorktreeDir(resolvedPath: string): boolean;
|
|
|
23
24
|
* Returns the directory path, or null when the worktree is not on disk yet
|
|
24
25
|
* (do not mkdir a stray tree for a branch-name target).
|
|
25
26
|
*/
|
|
26
|
-
export declare function ensureSubagentStatusDir(worktreeRoot: string): string | null;
|
|
27
|
+
export declare function ensureSubagentStatusDir(worktreeRoot: string, childLease?: Omit<ChildOccupancyDispatchInput, "worktreePath">): string | null;
|
|
27
28
|
//# sourceMappingURL=subagent-status-dir.d.ts.map
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* exit 1. Does not write heartbeat records or liveness onto the C2 manifest.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
11
|
import { defaultScratchDir } from "../orchestration/subagent-monitor.js";
|
|
12
|
+
import { recordChildOccupancyLease, } from "../session/child-occupancy.js";
|
|
12
13
|
/**
|
|
13
14
|
* True when a resolved path is an on-disk git worktree, i.e. it carries a `.git`
|
|
14
15
|
* entry (a file for `git worktree add` trees, a directory for the main clone).
|
|
@@ -31,12 +32,20 @@ export function looksLikeWorktreeDir(resolvedPath) {
|
|
|
31
32
|
* Returns the directory path, or null when the worktree is not on disk yet
|
|
32
33
|
* (do not mkdir a stray tree for a branch-name target).
|
|
33
34
|
*/
|
|
34
|
-
export function ensureSubagentStatusDir(worktreeRoot) {
|
|
35
|
+
export function ensureSubagentStatusDir(worktreeRoot, childLease) {
|
|
35
36
|
const trimmed = worktreeRoot.trim();
|
|
36
37
|
if (trimmed.length === 0 || !existsSync(trimmed))
|
|
37
38
|
return null;
|
|
38
39
|
const dir = defaultScratchDir(trimmed);
|
|
39
40
|
mkdirSync(dir, { recursive: true });
|
|
41
|
+
// Optional: parent already knows the child's occupancy owner (spawn return).
|
|
42
|
+
// Host-env production claims also record via applyWorktreeOccupancy (#3999).
|
|
43
|
+
if (childLease !== undefined) {
|
|
44
|
+
recordChildOccupancyLease(trimmed, {
|
|
45
|
+
...childLease,
|
|
46
|
+
worktreePath: resolve(trimmed),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
40
49
|
return dir;
|
|
41
50
|
}
|
|
42
51
|
//# sourceMappingURL=subagent-status-dir.js.map
|
package/dist/swarm/worktrees.js
CHANGED
|
@@ -198,6 +198,7 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
|
|
|
198
198
|
`but requested base '${trimmedBase}' resolves to ${requestedOid}; ` +
|
|
199
199
|
`this is a snapshot check at resolution time, not a pin on the worker's start revision`);
|
|
200
200
|
}
|
|
201
|
+
// Heartbeat dir only. Child occupancy owner is recorded at claim (#3999).
|
|
201
202
|
ensureSubagentStatusDir(entry._abs);
|
|
202
203
|
continue;
|
|
203
204
|
}
|
|
@@ -205,6 +206,7 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
|
|
|
205
206
|
throw new MissingWorktreeError(`story '${entry.story_id}' maps to '${entry.worktree_path}' which is not a registered git worktree and create_missing is disabled`);
|
|
206
207
|
}
|
|
207
208
|
createWorktree(root, entry._abs, trimmedBase, git);
|
|
209
|
+
// Heartbeat dir only. Child occupancy owner is recorded at claim (#3999).
|
|
208
210
|
ensureSubagentStatusDir(entry._abs);
|
|
209
211
|
}
|
|
210
212
|
return resolved.map(({ story_id, worktree_path, base_branch }) => ({
|