@deftai/directive-runtime 0.126.0-beta.1 → 0.126.0-beta.3

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 (212) hide show
  1. package/UPGRADING.md +4 -3
  2. package/commands.md +9 -6
  3. package/contracts/design-critique.md +39 -1
  4. package/contracts/human-origin-authz.md +14 -8
  5. package/dist/coverage-hotspots.d.ts +3 -0
  6. package/dist/coverage-hotspots.js +42 -2
  7. package/dist/doctor-diagnose.d.ts +1 -1
  8. package/dist/doctor-diagnose.js +29 -3
  9. package/dist/doctor.js +1 -1
  10. package/dist/occupancy-release.d.ts +5 -0
  11. package/dist/occupancy-release.js +11 -3
  12. package/dist/policy.d.ts +2 -0
  13. package/dist/policy.js +6 -0
  14. package/dist/session-start.js +28 -1
  15. package/dist/triage-actions.js +10 -2
  16. package/dist/verify-review-monitor.d.ts +7 -0
  17. package/dist/verify-review-monitor.js +21 -0
  18. package/docs/framework-gap-filing.md +85 -0
  19. package/docs/getting-started.md +13 -1
  20. package/docs/product-signal.md +1 -1
  21. package/package.json +1 -1
  22. package/packs/skills/skills-pack-0.1.json +10 -7
  23. package/packs/strategies/strategies-pack-0.1.json +32 -19
  24. package/scm/github.md +1 -1
  25. package/skills/deft-directive-article-review/SKILL.md +104 -24
  26. package/skills/deft-directive-design-critique/SKILL.md +10 -9
  27. package/skills/deft-directive-feedback/SKILL.md +37 -21
  28. package/skills/deft-directive-pre-pr/SKILL.md +14 -0
  29. package/skills/deft-directive-product-signal/SKILL.md +1 -1
  30. package/skills/deft-directive-review-cycle/SKILL.md +18 -1
  31. package/skills/deft-directive-setup/SKILL.md +41 -11
  32. package/skills/deft-directive-swarm/references/host-grok-build.md +1 -1
  33. package/skills/deft-directive-tutorial/SKILL.md +1 -1
  34. package/strategies/README.md +1 -0
  35. package/strategies/artifact-guards.md +2 -2
  36. package/strategies/design-thinking.md +182 -0
  37. package/strategies/discuss.md +2 -0
  38. package/strategies/interview.md +76 -23
  39. package/strategies/rapid.md +22 -12
  40. package/templates/agents-entry.md +4 -3
  41. package/templates/design-critique-brief.md +5 -1
  42. package/tutorial/beats.json +2 -2
  43. package/vendor/directive-content/UPGRADING.md +4 -3
  44. package/vendor/directive-content/commands.md +9 -6
  45. package/vendor/directive-content/contracts/design-critique.md +39 -1
  46. package/vendor/directive-content/contracts/human-origin-authz.md +14 -8
  47. package/vendor/directive-content/docs/framework-gap-filing.md +85 -0
  48. package/vendor/directive-content/docs/getting-started.md +13 -1
  49. package/vendor/directive-content/docs/product-signal.md +1 -1
  50. package/vendor/directive-content/packs/skills/skills-pack-0.1.json +10 -7
  51. package/vendor/directive-content/packs/strategies/strategies-pack-0.1.json +32 -19
  52. package/vendor/directive-content/scm/github.md +1 -1
  53. package/vendor/directive-content/skills/deft-directive-article-review/SKILL.md +104 -24
  54. package/vendor/directive-content/skills/deft-directive-design-critique/SKILL.md +10 -9
  55. package/vendor/directive-content/skills/deft-directive-feedback/SKILL.md +37 -21
  56. package/vendor/directive-content/skills/deft-directive-pre-pr/SKILL.md +14 -0
  57. package/vendor/directive-content/skills/deft-directive-product-signal/SKILL.md +1 -1
  58. package/vendor/directive-content/skills/deft-directive-review-cycle/SKILL.md +18 -1
  59. package/vendor/directive-content/skills/deft-directive-setup/SKILL.md +41 -11
  60. package/vendor/directive-content/skills/deft-directive-swarm/references/host-grok-build.md +1 -1
  61. package/vendor/directive-content/skills/deft-directive-tutorial/SKILL.md +1 -1
  62. package/vendor/directive-content/strategies/README.md +1 -0
  63. package/vendor/directive-content/strategies/artifact-guards.md +2 -2
  64. package/vendor/directive-content/strategies/design-thinking.md +182 -0
  65. package/vendor/directive-content/strategies/discuss.md +2 -0
  66. package/vendor/directive-content/strategies/interview.md +76 -23
  67. package/vendor/directive-content/strategies/rapid.md +22 -12
  68. package/vendor/directive-content/templates/agents-entry.md +4 -3
  69. package/vendor/directive-content/templates/design-critique-brief.md +5 -1
  70. package/vendor/directive-content/tutorial/beats.json +2 -2
  71. package/vendor/directive-core/dist/.tsbuildinfo +1 -1
  72. package/vendor/directive-core/dist/content-contracts/skills/helpers.d.ts +11 -1
  73. package/vendor/directive-core/dist/content-contracts/skills/helpers.d.ts.map +1 -1
  74. package/vendor/directive-core/dist/content-contracts/skills/helpers.js +18 -5
  75. package/vendor/directive-core/dist/content-contracts/skills/helpers.js.map +1 -1
  76. package/vendor/directive-core/dist/coverage-applicability/evaluate.d.ts.map +1 -1
  77. package/vendor/directive-core/dist/coverage-applicability/evaluate.js +39 -0
  78. package/vendor/directive-core/dist/coverage-applicability/evaluate.js.map +1 -1
  79. package/vendor/directive-core/dist/coverage-hotspots/evaluate.d.ts +24 -5
  80. package/vendor/directive-core/dist/coverage-hotspots/evaluate.d.ts.map +1 -1
  81. package/vendor/directive-core/dist/coverage-hotspots/evaluate.js +89 -8
  82. package/vendor/directive-core/dist/coverage-hotspots/evaluate.js.map +1 -1
  83. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.d.ts +40 -0
  84. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.d.ts.map +1 -1
  85. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.js +116 -0
  86. package/vendor/directive-core/dist/design-critique/auto-stamp-path1.js.map +1 -1
  87. package/vendor/directive-core/dist/design-critique/citation-grammar.d.ts +36 -0
  88. package/vendor/directive-core/dist/design-critique/citation-grammar.d.ts.map +1 -1
  89. package/vendor/directive-core/dist/design-critique/citation-grammar.js +60 -3
  90. package/vendor/directive-core/dist/design-critique/citation-grammar.js.map +1 -1
  91. package/vendor/directive-core/dist/design-critique/completed-arc-record.d.ts +103 -3
  92. package/vendor/directive-core/dist/design-critique/completed-arc-record.d.ts.map +1 -1
  93. package/vendor/directive-core/dist/design-critique/completed-arc-record.js +127 -5
  94. package/vendor/directive-core/dist/design-critique/completed-arc-record.js.map +1 -1
  95. package/vendor/directive-core/dist/design-critique/ingest-posture.d.ts +27 -0
  96. package/vendor/directive-core/dist/design-critique/ingest-posture.d.ts.map +1 -0
  97. package/vendor/directive-core/dist/design-critique/ingest-posture.js +66 -0
  98. package/vendor/directive-core/dist/design-critique/ingest-posture.js.map +1 -0
  99. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.d.ts +106 -7
  100. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.d.ts.map +1 -1
  101. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.js +284 -17
  102. package/vendor/directive-core/dist/design-critique/panel-handback-acceptance.js.map +1 -1
  103. package/vendor/directive-core/dist/docs/docs-impact.d.ts +13 -0
  104. package/vendor/directive-core/dist/docs/docs-impact.d.ts.map +1 -1
  105. package/vendor/directive-core/dist/docs/docs-impact.js +49 -4
  106. package/vendor/directive-core/dist/docs/docs-impact.js.map +1 -1
  107. package/vendor/directive-core/dist/doctor/main.d.ts +48 -0
  108. package/vendor/directive-core/dist/doctor/main.d.ts.map +1 -1
  109. package/vendor/directive-core/dist/doctor/main.js +221 -4
  110. package/vendor/directive-core/dist/doctor/main.js.map +1 -1
  111. package/vendor/directive-core/dist/hooks/dispatcher.d.ts.map +1 -1
  112. package/vendor/directive-core/dist/hooks/dispatcher.js +160 -14
  113. package/vendor/directive-core/dist/hooks/dispatcher.js.map +1 -1
  114. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts +4 -0
  115. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts.map +1 -1
  116. package/vendor/directive-core/dist/init-deposit/agent-hooks.js +25 -4
  117. package/vendor/directive-core/dist/init-deposit/agent-hooks.js.map +1 -1
  118. package/vendor/directive-core/dist/init-deposit/skill-discovery-deposit.d.ts.map +1 -1
  119. package/vendor/directive-core/dist/init-deposit/skill-discovery-deposit.js +15 -0
  120. package/vendor/directive-core/dist/init-deposit/skill-discovery-deposit.js.map +1 -1
  121. package/vendor/directive-core/dist/intake/issue-ingest.d.ts +15 -0
  122. package/vendor/directive-core/dist/intake/issue-ingest.d.ts.map +1 -1
  123. package/vendor/directive-core/dist/intake/issue-ingest.js +190 -22
  124. package/vendor/directive-core/dist/intake/issue-ingest.js.map +1 -1
  125. package/vendor/directive-core/dist/pr-closing-keywords/detect.d.ts.map +1 -1
  126. package/vendor/directive-core/dist/pr-closing-keywords/detect.js +52 -7
  127. package/vendor/directive-core/dist/pr-closing-keywords/detect.js.map +1 -1
  128. package/vendor/directive-core/dist/pr-watch/main.d.ts +5 -0
  129. package/vendor/directive-core/dist/pr-watch/main.d.ts.map +1 -1
  130. package/vendor/directive-core/dist/pr-watch/main.js.map +1 -1
  131. package/vendor/directive-core/dist/pr-watch/watch.d.ts.map +1 -1
  132. package/vendor/directive-core/dist/pr-watch/watch.js +5 -0
  133. package/vendor/directive-core/dist/pr-watch/watch.js.map +1 -1
  134. package/vendor/directive-core/dist/pre-pr-controller/controller.d.ts.map +1 -1
  135. package/vendor/directive-core/dist/pre-pr-controller/controller.js +118 -12
  136. package/vendor/directive-core/dist/pre-pr-controller/controller.js.map +1 -1
  137. package/vendor/directive-core/dist/pre-pr-controller/index.d.ts +1 -1
  138. package/vendor/directive-core/dist/pre-pr-controller/index.d.ts.map +1 -1
  139. package/vendor/directive-core/dist/pre-pr-controller/index.js +1 -1
  140. package/vendor/directive-core/dist/pre-pr-controller/index.js.map +1 -1
  141. package/vendor/directive-core/dist/pre-pr-controller/phases.d.ts +5 -0
  142. package/vendor/directive-core/dist/pre-pr-controller/phases.d.ts.map +1 -1
  143. package/vendor/directive-core/dist/pre-pr-controller/phases.js +10 -0
  144. package/vendor/directive-core/dist/pre-pr-controller/phases.js.map +1 -1
  145. package/vendor/directive-core/dist/presentation-ceiling/evaluate.d.ts.map +1 -1
  146. package/vendor/directive-core/dist/presentation-ceiling/evaluate.js +3 -1
  147. package/vendor/directive-core/dist/presentation-ceiling/evaluate.js.map +1 -1
  148. package/vendor/directive-core/dist/release/auto-hatch.d.ts +6 -0
  149. package/vendor/directive-core/dist/release/auto-hatch.d.ts.map +1 -1
  150. package/vendor/directive-core/dist/release/auto-hatch.js +13 -4
  151. package/vendor/directive-core/dist/release/auto-hatch.js.map +1 -1
  152. package/vendor/directive-core/dist/review-monitor/record.d.ts +11 -0
  153. package/vendor/directive-core/dist/review-monitor/record.d.ts.map +1 -1
  154. package/vendor/directive-core/dist/review-monitor/record.js +23 -0
  155. package/vendor/directive-core/dist/review-monitor/record.js.map +1 -1
  156. package/vendor/directive-core/dist/run-summary/emit.d.ts +2 -1
  157. package/vendor/directive-core/dist/run-summary/emit.d.ts.map +1 -1
  158. package/vendor/directive-core/dist/run-summary/emit.js +3 -0
  159. package/vendor/directive-core/dist/run-summary/emit.js.map +1 -1
  160. package/vendor/directive-core/dist/run-summary/index.d.ts +1 -1
  161. package/vendor/directive-core/dist/run-summary/index.d.ts.map +1 -1
  162. package/vendor/directive-core/dist/run-summary/index.js.map +1 -1
  163. package/vendor/directive-core/dist/run-summary/types.d.ts +8 -2
  164. package/vendor/directive-core/dist/run-summary/types.d.ts.map +1 -1
  165. package/vendor/directive-core/dist/run-summary/types.js +1 -0
  166. package/vendor/directive-core/dist/run-summary/types.js.map +1 -1
  167. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts +11 -0
  168. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts.map +1 -1
  169. package/vendor/directive-core/dist/scope/delivery-evidence.js +34 -0
  170. package/vendor/directive-core/dist/scope/delivery-evidence.js.map +1 -1
  171. package/vendor/directive-core/dist/scope/promote-from-issue.d.ts +2 -1
  172. package/vendor/directive-core/dist/scope/promote-from-issue.d.ts.map +1 -1
  173. package/vendor/directive-core/dist/scope/promote-from-issue.js +17 -61
  174. package/vendor/directive-core/dist/scope/promote-from-issue.js.map +1 -1
  175. package/vendor/directive-core/dist/session/occupancy-recovery.d.ts +10 -0
  176. package/vendor/directive-core/dist/session/occupancy-recovery.d.ts.map +1 -1
  177. package/vendor/directive-core/dist/session/occupancy-recovery.js +43 -7
  178. package/vendor/directive-core/dist/session/occupancy-recovery.js.map +1 -1
  179. package/vendor/directive-core/dist/session/occupancy.d.ts +148 -1
  180. package/vendor/directive-core/dist/session/occupancy.d.ts.map +1 -1
  181. package/vendor/directive-core/dist/session/occupancy.js +516 -34
  182. package/vendor/directive-core/dist/session/occupancy.js.map +1 -1
  183. package/vendor/directive-core/dist/session/session-start.d.ts.map +1 -1
  184. package/vendor/directive-core/dist/session/session-start.js +6 -1
  185. package/vendor/directive-core/dist/session/session-start.js.map +1 -1
  186. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.d.ts.map +1 -1
  187. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.js +5 -1
  188. package/vendor/directive-core/dist/swarm/cohort-review-monitors-cli.js.map +1 -1
  189. package/vendor/directive-core/dist/swarm/cohort-review-monitors.d.ts +71 -0
  190. package/vendor/directive-core/dist/swarm/cohort-review-monitors.d.ts.map +1 -1
  191. package/vendor/directive-core/dist/swarm/cohort-review-monitors.js +125 -2
  192. package/vendor/directive-core/dist/swarm/cohort-review-monitors.js.map +1 -1
  193. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.d.ts.map +1 -1
  194. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.js +9 -0
  195. package/vendor/directive-core/dist/telemetry-coverage/fake-trial.js.map +1 -1
  196. package/vendor/directive-core/dist/telemetry-coverage/kinds.d.ts.map +1 -1
  197. package/vendor/directive-core/dist/telemetry-coverage/kinds.js +2 -0
  198. package/vendor/directive-core/dist/telemetry-coverage/kinds.js.map +1 -1
  199. package/vendor/directive-core/dist/triage/actions/index.d.ts +8 -5
  200. package/vendor/directive-core/dist/triage/actions/index.d.ts.map +1 -1
  201. package/vendor/directive-core/dist/triage/actions/index.js +248 -22
  202. package/vendor/directive-core/dist/triage/actions/index.js.map +1 -1
  203. package/vendor/directive-core/dist/triage/actions/types.d.ts +16 -2
  204. package/vendor/directive-core/dist/triage/actions/types.d.ts.map +1 -1
  205. package/vendor/directive-core/dist/triage/bulk/index.d.ts +8 -1
  206. package/vendor/directive-core/dist/triage/bulk/index.d.ts.map +1 -1
  207. package/vendor/directive-core/dist/triage/bulk/index.js +30 -11
  208. package/vendor/directive-core/dist/triage/bulk/index.js.map +1 -1
  209. package/vendor/directive-core/dist/value/feedback-file.d.ts +54 -6
  210. package/vendor/directive-core/dist/value/feedback-file.d.ts.map +1 -1
  211. package/vendor/directive-core/dist/value/feedback-file.js +470 -43
  212. package/vendor/directive-core/dist/value/feedback-file.js.map +1 -1
@@ -75,13 +75,30 @@ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
75
75
  import { assertAppendLockOwned, withAppendLock } from "../slice/lock.js";
76
76
  import { SWARM_WORKER_ROLES } from "../swarm/routing.js";
77
77
  import { recordChildOccupancyLease } from "./child-occupancy.js";
78
- import { ambientHostSessionOwner, claimsHostSessionIdShape, detectDeclaredIdentityHosts, isUsableHostSessionId, parseCanonicalHostSessionId, printCompanionHostOwner, } from "./host-session-owner.js";
78
+ import { worktreePathOrNull } from "./git.js";
79
+ import { ambientHostSessionOwner, canonicalHostSessionId, claimsHostSessionIdShape, detectDeclaredIdentityHosts, HOST_IDENTITY_PROVIDERS, isUsableHostSessionId, parseCanonicalHostSessionId, printCompanionHostOwner, } from "./host-session-owner.js";
79
80
  import { stableJson } from "./json.js";
80
81
  import { isLinkedWorktreePath, isMainWorktreePath, listLinkedWorktreeCheckouts, mainWorktreeRoot, } from "./main-worktree.js";
81
82
  import { readRitualState } from "./ritual-sentinel.js";
82
83
  import { parseTimestamp, timestampIso } from "./time.js";
83
84
  export const OCCUPANCY_SCHEMA_VERSION = 1;
84
85
  export const OCCUPANCY_RELPATH = [".deft", "occupancy.json"];
86
+ /**
87
+ * Harbor / cell bootstrap signal (#5544 Prefer-A): a declared agent host will
88
+ * attach to this tree. Claimer surface is Harbor ritual applying
89
+ * `applyWorktreeOccupancy`. When set, last-resort UUID mint is refused so the
90
+ * later host can self-claim.
91
+ */
92
+ export const DEFT_EXPECTED_DECLARED_HOST_ATTACH = "DEFT_EXPECTED_DECLARED_HOST_ATTACH";
93
+ /** Worktree applying-host record for multi-host PreToolUse deposits (#5676). */
94
+ export const APPLYING_HOST_RELPATH = [".deft", "applying-host.json"];
95
+ export const APPLYING_HOST_SCHEMA = "deft.applying-host.v1";
96
+ /**
97
+ * Applying-host staleness floor (#5676). Mirrors tip ritual staleness (8h) so a
98
+ * reconnect SessionStart / read-only session:start republish stays in the same
99
+ * recovery window as ceremony re-arm; UUID residual stays under #5544.
100
+ */
101
+ export const APPLYING_HOST_STALE_MS = Number("8") * 60 * 60 * 1000;
85
102
  /** Crash recovery TTL: 20 minutes without heartbeat (15–30 window). */
86
103
  export const OCCUPANCY_TTL_MS = 20 * 60 * 1000;
87
104
  /**
@@ -227,6 +244,381 @@ export const OCCUPANCY_MAX_GRANTS = 32;
227
244
  export function occupancyPath(projectRoot) {
228
245
  return join(resolve(projectRoot), ...OCCUPANCY_RELPATH);
229
246
  }
247
+ export function applyingHostPath(projectRoot) {
248
+ return join(resolve(projectRoot), ...APPLYING_HOST_RELPATH);
249
+ }
250
+ function isHookHostIdentityProvider(value) {
251
+ return HOST_IDENTITY_PROVIDERS.includes(value);
252
+ }
253
+ function parseApplyingHostPayload(payload) {
254
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
255
+ return null;
256
+ const row = payload;
257
+ if (row.schema !== APPLYING_HOST_SCHEMA)
258
+ return null;
259
+ if (typeof row.provider !== "string" || !isHookHostIdentityProvider(row.provider))
260
+ return null;
261
+ if (typeof row.rawSessionId !== "string" || !isUsableHostSessionId(row.rawSessionId))
262
+ return null;
263
+ if (typeof row.applyingHostSessionId !== "string")
264
+ return null;
265
+ if (typeof row.claimed_at !== "string" || parseTimestamp(row.claimed_at) === null)
266
+ return null;
267
+ const transport = row.transport === "grok-hook-event" ? "grok-hook-event" : "native";
268
+ const expected = canonicalHostSessionId(row.provider, row.rawSessionId);
269
+ if (row.applyingHostSessionId !== expected)
270
+ return null;
271
+ return {
272
+ schema: APPLYING_HOST_SCHEMA,
273
+ provider: row.provider,
274
+ rawSessionId: row.rawSessionId,
275
+ applyingHostSessionId: expected,
276
+ claimed_at: row.claimed_at,
277
+ transport,
278
+ };
279
+ }
280
+ /** Read the worktree applying-host record (#5676). */
281
+ export function readApplyingHost(projectRoot, now = new Date()) {
282
+ const path = applyingHostPath(projectRoot);
283
+ try {
284
+ if (!existsSync(path))
285
+ return { status: "missing", record: null };
286
+ }
287
+ catch {
288
+ return { status: "missing", record: null };
289
+ }
290
+ let payload;
291
+ try {
292
+ payload = JSON.parse(readFileSync(path, { encoding: "utf8" }));
293
+ }
294
+ catch {
295
+ return {
296
+ status: "invalid",
297
+ record: null,
298
+ message: "applying-host record is unreadable; republish via SessionStart or read-only session:start",
299
+ };
300
+ }
301
+ const record = parseApplyingHostPayload(payload);
302
+ if (record === null) {
303
+ return {
304
+ status: "invalid",
305
+ record: null,
306
+ message: "applying-host record is malformed; republish via SessionStart or read-only session:start",
307
+ };
308
+ }
309
+ const claimedAt = parseTimestamp(record.claimed_at);
310
+ if (claimedAt === null) {
311
+ return {
312
+ status: "invalid",
313
+ record: null,
314
+ message: "applying-host record has invalid claimed_at; republish via SessionStart or read-only session:start",
315
+ };
316
+ }
317
+ if (now.getTime() - claimedAt.getTime() > APPLYING_HOST_STALE_MS) {
318
+ return {
319
+ status: "stale",
320
+ record,
321
+ message: formatApplyingHostRepairDiagnostic("stale"),
322
+ };
323
+ }
324
+ return { status: "ok", record };
325
+ }
326
+ /**
327
+ * Supported repair when the applying-host record is missing, stale, or
328
+ * conflicting (#5676 Prefer-A Recut-5). Mutation session:start is reinforcement
329
+ * only — first publication is SessionStart or read-only session:start.
330
+ */
331
+ export function formatApplyingHostRepairDiagnostic(reason = "missing") {
332
+ const subject = reason === "missing"
333
+ ? "Applying-host record is missing"
334
+ : reason === "stale"
335
+ ? "Applying-host record is stale"
336
+ : reason === "conflicting"
337
+ ? "Applying-host record conflicts with this host session"
338
+ : "Applying-host record is invalid";
339
+ return (`${subject}. Republish via host SessionStart reconnect and/or ` +
340
+ "`deft session:start --read-only --session-id=<applyingHostSessionId>` " +
341
+ "before retrying mutation lifecycle. UUID / Harbor residuals stay under #5544.");
342
+ }
343
+ /** Write/replace the worktree applying-host record (#5676). */
344
+ export function publishApplyingHost(projectRoot, input) {
345
+ if (!isUsableHostSessionId(input.rawSessionId)) {
346
+ throw new Error("publishApplyingHost: rawSessionId is not a usable host session id");
347
+ }
348
+ const root = resolve(projectRoot);
349
+ const claimedAt = input.claimedAt ?? new Date();
350
+ const record = {
351
+ schema: APPLYING_HOST_SCHEMA,
352
+ provider: input.provider,
353
+ rawSessionId: input.rawSessionId,
354
+ applyingHostSessionId: canonicalHostSessionId(input.provider, input.rawSessionId),
355
+ claimed_at: timestampIso(claimedAt),
356
+ transport: input.transport ?? "native",
357
+ };
358
+ const target = applyingHostPath(root);
359
+ assertWriteTargetSafe(root, target);
360
+ const dir = dirname(target);
361
+ const tmpName = join(dir, `.applying-host.${process.pid}.json.tmp`);
362
+ const text = `${stableJson(record, 2)}\n`;
363
+ try {
364
+ containedWrite({ root, target: tmpName, data: text, mode: "create" });
365
+ renameSync(tmpName, target);
366
+ }
367
+ finally {
368
+ try {
369
+ rmSync(tmpName, { force: true });
370
+ }
371
+ catch {
372
+ /* best-effort cleanup of tmp only */
373
+ }
374
+ }
375
+ return record;
376
+ }
377
+ /**
378
+ * Publish from a canonical `host:<provider>:v1:<base64url>` owner (#5676).
379
+ * Returns null when the id is not canonical (opaque UUID leftovers stay #5544).
380
+ */
381
+ export function publishApplyingHostFromCanonicalSessionId(projectRoot, sessionId, opts = {}) {
382
+ const parsed = parseCanonicalHostSessionId(sessionId.trim());
383
+ if (parsed === null)
384
+ return null;
385
+ return publishApplyingHost(projectRoot, {
386
+ provider: parsed.provider,
387
+ rawSessionId: parsed.rawSessionId,
388
+ claimedAt: opts.claimedAt,
389
+ transport: opts.transport,
390
+ });
391
+ }
392
+ /**
393
+ * Lifecycle admission against the applying-host record (#5676).
394
+ *
395
+ * Matching deposits enforce `applyingHostSessionId`. Non-matching deposits
396
+ * soft-pass only when the command already names that id. Missing/stale/
397
+ * conflicting records name SessionStart / read-only repair. Absent record with
398
+ * no flip-flop shape returns `absent` so single-host flows keep prior behavior.
399
+ */
400
+ export function evaluateApplyingHostLifecycleAdmission(input) {
401
+ const read = readApplyingHost(input.projectRoot, input.now ?? new Date());
402
+ if (read.status === "invalid") {
403
+ return {
404
+ kind: "repair",
405
+ record: null,
406
+ message: read.message,
407
+ };
408
+ }
409
+ if (read.status === "stale") {
410
+ // Live occupancy owner cleanup when applying-host TTL elapsed but the lease
411
+ // is still heartbeat-fresh (#5676 Greptile P1). Soft-pass only when argv
412
+ // already names that live owner so bare commands keep the rewrite path.
413
+ const staleLive = liveOccupant(input.projectRoot, input.now ?? new Date());
414
+ const staleLiveId = staleLive?.sessionId ?? null;
415
+ const staleNamed = input.namedSessionId?.trim() || null;
416
+ if (staleLiveId !== null &&
417
+ read.record.applyingHostSessionId === staleLiveId &&
418
+ staleNamed === staleLiveId) {
419
+ return {
420
+ kind: "soft-pass",
421
+ record: read.record,
422
+ reason: "live-lease-owner-cleanup",
423
+ };
424
+ }
425
+ return { kind: "repair", record: read.record, message: read.message };
426
+ }
427
+ if (read.status === "missing" || read.record === null) {
428
+ const named = input.namedSessionId?.trim() || null;
429
+ const deposit = input.depositSessionId?.trim() || null;
430
+ if (named !== null && deposit !== null && named !== deposit) {
431
+ const namedParts = parseCanonicalHostSessionId(named);
432
+ const depositParts = parseCanonicalHostSessionId(deposit);
433
+ if (namedParts !== null &&
434
+ depositParts !== null &&
435
+ namedParts.rawSessionId === depositParts.rawSessionId &&
436
+ namedParts.provider !== depositParts.provider) {
437
+ return {
438
+ kind: "repair",
439
+ record: null,
440
+ message: formatApplyingHostRepairDiagnostic("missing"),
441
+ };
442
+ }
443
+ }
444
+ return { kind: "absent", record: null };
445
+ }
446
+ const record = read.record;
447
+ // Live occupancy owner cleanup when a later SessionStart replaced applying-host
448
+ // (#5676 Greptile P1). Only soft-pass when argv already names that live owner so
449
+ // bare commands still take the rewrite path (do not skip --session-id injection).
450
+ const live = liveOccupant(input.projectRoot, input.now ?? new Date());
451
+ const liveId = live?.sessionId ?? null;
452
+ const named = input.namedSessionId?.trim() || null;
453
+ if (liveId !== null && record.applyingHostSessionId !== liveId && named === liveId) {
454
+ return {
455
+ kind: "soft-pass",
456
+ record,
457
+ reason: "live-lease-owner-cleanup",
458
+ };
459
+ }
460
+ if (isHookHostIdentityProvider(input.depositHost) &&
461
+ input.depositHost === record.provider &&
462
+ input.depositSessionId !== null &&
463
+ input.depositSessionId !== record.applyingHostSessionId) {
464
+ const depositParts = parseCanonicalHostSessionId(input.depositSessionId);
465
+ if (depositParts === null || depositParts.rawSessionId !== record.rawSessionId) {
466
+ return {
467
+ kind: "repair",
468
+ record,
469
+ message: formatApplyingHostRepairDiagnostic("conflicting"),
470
+ };
471
+ }
472
+ }
473
+ const matchingDeposit = input.depositHost === record.provider;
474
+ if (!matchingDeposit) {
475
+ // Soft-pass only when argv already names applyingHostSessionId (#5676).
476
+ // Bare commands must not skip the owner rewrite / explicit-owner gate.
477
+ if (named === record.applyingHostSessionId) {
478
+ return {
479
+ kind: "soft-pass",
480
+ record,
481
+ reason: "non-matching-deposit-names-applying-host",
482
+ };
483
+ }
484
+ return {
485
+ kind: "foreign-owner",
486
+ record,
487
+ namedSessionId: named,
488
+ message: named === null
489
+ ? `Directive refused bare lifecycle command; applying-host is ` +
490
+ `${record.applyingHostSessionId}. Re-run with ` +
491
+ `--session-id=${record.applyingHostSessionId}.`
492
+ : `Directive refused foreign owner ${named}; applying-host is ` +
493
+ `${record.applyingHostSessionId}. Re-run with ` +
494
+ `--session-id=${record.applyingHostSessionId}.`,
495
+ };
496
+ }
497
+ if (named !== null && named !== record.applyingHostSessionId) {
498
+ return {
499
+ kind: "foreign-owner",
500
+ record,
501
+ namedSessionId: named,
502
+ message: `Directive refused foreign owner ${named}; applying-host is ` +
503
+ `${record.applyingHostSessionId}. Re-run with ` +
504
+ `--session-id=${record.applyingHostSessionId}.`,
505
+ };
506
+ }
507
+ return {
508
+ kind: "enforce",
509
+ record,
510
+ matchingDeposit: true,
511
+ };
512
+ }
513
+ /**
514
+ * Resolve the lease root CLI and hooks share for the same worktree (#5544 P2).
515
+ *
516
+ * Uses the Git worktree toplevel when available (same tree hooks select when
517
+ * payload alone gates, or when writes stay inside that worktree). Linked
518
+ * worktrees keep their own toplevel — never collapsed into the main checkout.
519
+ */
520
+ const occupancyLeaseRootCache = new Map();
521
+ /**
522
+ * Process-local cache so Write-hook double probes (preview + refresh) and
523
+ * nested readOccupancy calls do not re-shell `git rev-parse` for the same path.
524
+ */
525
+ export function resolveOccupancyLeaseRoot(projectRoot) {
526
+ const absolute = resolve(projectRoot);
527
+ const cached = occupancyLeaseRootCache.get(absolute);
528
+ if (cached !== undefined)
529
+ return cached;
530
+ const toplevel = worktreePathOrNull(absolute);
531
+ const root = toplevel ?? absolute;
532
+ occupancyLeaseRootCache.set(absolute, root);
533
+ occupancyLeaseRootCache.set(root, root);
534
+ return root;
535
+ }
536
+ /** Print the occupancy file (and optional Write-gate effectiveRoot) (#5544 P2). */
537
+ export function formatOccupancyLeaseViewNote(occupancyFilePath, options = {}) {
538
+ const parts = [`occupancyFile=${occupancyFilePath}`];
539
+ if (options.effectiveRoot !== undefined && options.effectiveRoot.length > 0) {
540
+ parts.push(`effectiveRoot=${options.effectiveRoot}`);
541
+ }
542
+ return parts.join(" ");
543
+ }
544
+ export function formatOccupancyAlreadyFree(occupancyFilePath, options = {}) {
545
+ return `occupancy already free (${formatOccupancyLeaseViewNote(occupancyFilePath, options)})`;
546
+ }
547
+ export function expectedDeclaredHostAttachSignaled(input = {}) {
548
+ if (input.expectedDeclaredHostAttach === true)
549
+ return true;
550
+ const raw = (input.env ?? process.env)[DEFT_EXPECTED_DECLARED_HOST_ATTACH]?.trim() ?? "";
551
+ if (raw.length === 0)
552
+ return false;
553
+ return raw === "1" || raw.toLowerCase() === "true" || raw.toLowerCase() === "yes";
554
+ }
555
+ export function formatOccupancyExpectedHostMintRefusal() {
556
+ return ("occupancy refuses last-resort mint: Harbor/bootstrap signaled a declared agent host will attach " +
557
+ `(${DEFT_EXPECTED_DECLARED_HOST_ATTACH}=1 or expectedDeclaredHostAttach). ` +
558
+ "Claimer surface: Harbor ritual applying applyWorktreeOccupancy. " +
559
+ "Default is host-self-claim after attach. Stay read-only (`session:start --read-only`) or skip mutation session:start. " +
560
+ "If a pre-attach mutation lease is required, bind the exact later host session id " +
561
+ "(`session:start --session-id=<host:provider:v1:...>`) and release successfully before agent launch — " +
562
+ "failed cleanup blocks launch (assertOccupancyFreeForHostLaunch).");
563
+ }
564
+ /**
565
+ * Harbor launch gate (#5544 Prefer-A): failed pre-attach occupancy cleanup
566
+ * blocks agent launch. Exit 0 only when this lease root is free.
567
+ */
568
+ export function assertOccupancyFreeForHostLaunch(projectRoot, input = {}) {
569
+ const root = resolveOccupancyLeaseRoot(projectRoot);
570
+ const path = occupancyPath(root);
571
+ const live = liveOccupant(root, input.now ?? new Date());
572
+ if (live === null) {
573
+ return {
574
+ action: "released",
575
+ sessionId: "",
576
+ record: null,
577
+ path,
578
+ message: formatOccupancyAlreadyFree(path),
579
+ code: 0,
580
+ };
581
+ }
582
+ const occupantArg = commandSessionId(live.sessionId, "<reported-session-id>");
583
+ return {
584
+ action: "denied",
585
+ sessionId: "",
586
+ record: live,
587
+ path,
588
+ message: `Harbor launch blocked: live occupancy lease still holds (${formatOccupancyLeaseViewNote(path)}). ` +
589
+ `session=${live.sessionId}. Failed pre-attach cleanup blocks launch. ` +
590
+ `Release with \`occupancy:release --session-id=${occupantArg} --project-root=${root}\` before attaching the agent host.`,
591
+ code: 1,
592
+ };
593
+ }
594
+ /**
595
+ * Narrow #5544 Prefer-A P3 gate: live minted lease + canonical host-shaped
596
+ * presenter may steal under confirm. Does not broaden steal defaults.
597
+ */
598
+ export function occupancyMintedDeclaredHostStealAdmitted(record, presented) {
599
+ if (record.identityProvenance !== "minted")
600
+ return false;
601
+ const actor = presented?.trim() ?? "";
602
+ if (actor.length === 0)
603
+ return false;
604
+ return parseCanonicalHostSessionId(actor) !== null;
605
+ }
606
+ export function formatOccupancyMintedDeclaredHostStealRemediation(record, presented, projectRoot) {
607
+ const root = projectRoot !== undefined ? resolveOccupancyLeaseRoot(projectRoot) : undefined;
608
+ const path = root !== undefined ? occupancyPath(root) : undefined;
609
+ const occupantArg = commandSessionId(record.sessionId, "<reported-session-id>");
610
+ const hostArg = commandSessionId(presented, "<host-published-id>");
611
+ const rootFlag = root !== undefined ? ` --project-root=${JSON.stringify(root)}` : "";
612
+ const view = path !== undefined ? ` Lease view: ${formatOccupancyLeaseViewNote(path)}.` : "";
613
+ const rootNote = root !== undefined ? ` Target root: ${root}.` : "";
614
+ return (`Closed recovery for a minted lease while host-shaped presenter ${presented} is already on this tree:${view}${rootNote} ` +
615
+ `Run \`occupancy:steal --occupant=${occupantArg} --session-id=${hostArg} --confirm${rootFlag}\`` +
616
+ `. That is the permitted lease operation under the minted+declared-host gate (not a broad steal default). ` +
617
+ "Steal changes the lease only; then restore ceremony as the same presenter: " +
618
+ `\`session:start --rearm --session-id=${hostArg}${rootFlag}\` when re-arm is eligible, otherwise ` +
619
+ `\`session:start --session-id=${hostArg}${rootFlag}\` for a cold ceremony. ` +
620
+ "Owner-only UUID release then host re-claim remains available but is not the primary agent path.");
621
+ }
230
622
  /**
231
623
  * Whether a stored lease names this worktree (#3926).
232
624
  *
@@ -430,7 +822,7 @@ function isOwnInheritedPresentation(occupantId, presented) {
430
822
  }
431
823
  /** Advisory coordination preface for occupancy denials (#3729 Prefer-A AC4). */
432
824
  export const OCCUPANCY_ADVISORY_COORDINATION_PREFACE = "Occupancy is advisory coordination for accidental same-machine collision, not same-user authorization.";
433
- export function formatOccupancyRemediation(record, now = new Date(), presented, env) {
825
+ export function formatOccupancyRemediation(record, now = new Date(), presented, env, projectRoot) {
434
826
  const age = heartbeatAgeSeconds(record, now);
435
827
  const header = `${OCCUPANCY_ADVISORY_COORDINATION_PREFACE}\n` +
436
828
  `Worktree occupied by session ${record.sessionId} (intent=${record.intent}, heartbeat ${age}s ago, ` +
@@ -439,6 +831,13 @@ export function formatOccupancyRemediation(record, now = new Date(), presented,
439
831
  const worktreeFirst = "Use another worktree.";
440
832
  const occupantArg = commandSessionId(record.sessionId, "<reported-session-id>");
441
833
  if (record.identityProvenance === "minted") {
834
+ // #5544 Prefer-A P3: when a canonical host-shaped id is already presenting,
835
+ // replace "Do not steal" with the closed steal-then-ceremony path.
836
+ const hostPresenter = presented?.trim() ?? "";
837
+ if (occupancyMintedDeclaredHostStealAdmitted(record, hostPresenter)) {
838
+ return (`${header}This lease was claimed under a minted owner. No later hook process can present that id.\n` +
839
+ `${formatOccupancyMintedDeclaredHostStealRemediation(record, hostPresenter, projectRoot)}`);
840
+ }
442
841
  const hosts = detectDeclaredIdentityHosts(env ?? process.env);
443
842
  const suggested = uniquePrintCompanion(hosts, env ?? process.env);
444
843
  const named = suggested === null
@@ -446,7 +845,8 @@ export function formatOccupancyRemediation(record, now = new Date(), presented,
446
845
  : commandSessionId(suggested, "<host-published-id>");
447
846
  return (`${header}This lease was claimed under a minted owner. No later hook process can present that id.\n` +
448
847
  `${formatOccupancySkipMutationClaimWhenMinted()}\n` +
449
- `${worktreeFirst} Do not steal this lease. Stay read-only (\`session:start --read-only\`), or ` +
848
+ `${worktreeFirst} Do not steal this lease unless a declared host-shaped id is already presenting ` +
849
+ `(then use the minted+declared-host \`occupancy:steal\` gate). Stay read-only (\`session:start --read-only\`), or ` +
450
850
  `release it (\`occupancy:release --session-id=${occupantArg}\` / \`session:end\`) and re-claim ` +
451
851
  `with the host-published owner (\`session:start --session-id=${named}\`).${tail}`);
452
852
  }
@@ -624,6 +1024,35 @@ export function formatPresentedIdentityDisagreement(identity) {
624
1024
  */
625
1025
  export function resolveOccupancySessionClaim(input = {}) {
626
1026
  const identity = resolvePresentedIdentity(input);
1027
+ // #5544 Prefer-A: when Harbor signals expected declared-host attach, the only
1028
+ // pre-attach mutation exception is an explicit argv --session-id that is
1029
+ // already host-shaped. Inherited DEFT_SESSION_ID (even host-shaped) and
1030
+ // opaque ids are refused so the later host is not locked out.
1031
+ if (expectedDeclaredHostAttachSignaled(input)) {
1032
+ const explicitHostBind = identity.source === "explicit" &&
1033
+ identity.sessionId.length > 0 &&
1034
+ parseCanonicalHostSessionId(identity.sessionId) !== null;
1035
+ if (explicitHostBind) {
1036
+ return {
1037
+ status: "ok",
1038
+ sessionId: identity.sessionId,
1039
+ provenance: "explicit",
1040
+ source: "explicit",
1041
+ };
1042
+ }
1043
+ if (identity.sessionId.length > 0 || identity.disagreeingHostOwner !== null) {
1044
+ return {
1045
+ status: "refuse-mint",
1046
+ sessionId: "",
1047
+ provenance: null,
1048
+ source: "mint",
1049
+ hosts: [],
1050
+ suggestedSessionId: null,
1051
+ message: formatOccupancyExpectedHostMintRefusal(),
1052
+ };
1053
+ }
1054
+ // No presented identity yet — fall through to declared-host / empty-mint refuse.
1055
+ }
627
1056
  // #4066: host-authoritative claim. Inherited DEFT_SESSION_ID must not beat the
628
1057
  // host-published owner -- that split is the measured steal-from-self loop.
629
1058
  if (identity.source === "environment" && identity.disagreeingHostOwner !== null) {
@@ -658,6 +1087,20 @@ export function resolveOccupancySessionClaim(input = {}) {
658
1087
  message,
659
1088
  };
660
1089
  }
1090
+ // #5544 Prefer-A P1: Harbor pre-attach empty-hosts mint window. Signal closes
1091
+ // last-resort mint before the declared host attaches; exact --session-id bind
1092
+ // is the only pre-attach mutation exception (already returned above when set).
1093
+ if (expectedDeclaredHostAttachSignaled(input)) {
1094
+ return {
1095
+ status: "refuse-mint",
1096
+ sessionId: "",
1097
+ provenance: null,
1098
+ source: "mint",
1099
+ hosts: [],
1100
+ suggestedSessionId: null,
1101
+ message: formatOccupancyExpectedHostMintRefusal(),
1102
+ };
1103
+ }
661
1104
  return {
662
1105
  status: "ok",
663
1106
  sessionId: (input.newSessionId ?? randomUUID)(),
@@ -696,6 +1139,8 @@ function occupancyMintRefusalDecision(projectRoot, claim) {
696
1139
  };
697
1140
  }
698
1141
  export function readOccupancy(projectRoot) {
1142
+ // #5544: reads share the claim/release lease-root view.
1143
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
699
1144
  const path = occupancyPath(projectRoot);
700
1145
  try {
701
1146
  if (!existsSync(path))
@@ -796,6 +1241,8 @@ export const OCCUPANCY_CEREMONY_CASES = [
796
1241
  * session:ready. Matches persist, including the sibling-lease primary fence.
797
1242
  */
798
1243
  export function evaluateOccupancyCeremonyEligibility(projectRoot, input = {}) {
1244
+ // #5544: same lease-root view as claim / release / Write-gate.
1245
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
799
1246
  const now = input.now ?? new Date();
800
1247
  const claim = resolveOccupancySessionClaim(input);
801
1248
  if (claim.status === "refuse-mint") {
@@ -822,7 +1269,7 @@ export function evaluateOccupancyCeremonyEligibility(projectRoot, input = {}) {
822
1269
  sessionId: incoming,
823
1270
  occupantId: live.sessionId,
824
1271
  restrictedPrimary,
825
- denialMessage: formatOccupancyRemediation(live, now, incoming, input.env),
1272
+ denialMessage: formatOccupancyRemediation(live, now, incoming, input.env, projectRoot),
826
1273
  };
827
1274
  }
828
1275
  if (live !== null) {
@@ -830,7 +1277,7 @@ export function evaluateOccupancyCeremonyEligibility(projectRoot, input = {}) {
830
1277
  if (admission === "member") {
831
1278
  const grant = occupancyGrantFor(live, incoming, now);
832
1279
  const memberText = grant === null
833
- ? formatOccupancyRemediation(live, now, incoming, input.env)
1280
+ ? formatOccupancyRemediation(live, now, incoming, input.env, projectRoot)
834
1281
  : formatOccupancyMemberAdministrationRefusal(live, grant, "session:start");
835
1282
  return {
836
1283
  admitCeremony: false,
@@ -848,7 +1295,7 @@ export function evaluateOccupancyCeremonyEligibility(projectRoot, input = {}) {
848
1295
  sessionId: incoming,
849
1296
  occupantId: live.sessionId,
850
1297
  restrictedPrimary,
851
- denialMessage: formatOccupancyRemediation(live, now, incoming, input.env),
1298
+ denialMessage: formatOccupancyRemediation(live, now, incoming, input.env, projectRoot),
852
1299
  };
853
1300
  }
854
1301
  return {
@@ -903,6 +1350,8 @@ export function evaluateOccupancyCeremonyEligibility(projectRoot, input = {}) {
903
1350
  };
904
1351
  }
905
1352
  export function applyWorktreeOccupancy(projectRoot, input = {}) {
1353
+ // #5544: claim/read/release share one lease-root view (git worktree toplevel).
1354
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
906
1355
  const now = input.now ?? new Date();
907
1356
  const path = occupancyPath(projectRoot);
908
1357
  const claim = resolveOccupancySessionClaim(input);
@@ -942,7 +1391,7 @@ export function applyWorktreeOccupancy(projectRoot, input = {}) {
942
1391
  // A granted child reads the member refusal rather than an offer of the
943
1392
  // write grant it already holds (#3954 item 5): membership admits writes,
944
1393
  // and claiming the lease stays owner-only.
945
- message: membershipOwnerDenial(live, incoming, now, "session:start"),
1394
+ message: membershipOwnerDenial(live, incoming, now, "session:start", projectRoot),
946
1395
  code: 1,
947
1396
  };
948
1397
  }
@@ -976,7 +1425,7 @@ export function applyWorktreeOccupancy(projectRoot, input = {}) {
976
1425
  sessionId: incoming,
977
1426
  record: liveLocked,
978
1427
  path,
979
- message: membershipOwnerDenial(liveLocked, incoming, now, "session:start"),
1428
+ message: membershipOwnerDenial(liveLocked, incoming, now, "session:start", projectRoot),
980
1429
  code: 1,
981
1430
  };
982
1431
  }
@@ -1068,6 +1517,8 @@ function occupancyStealUsesMintedWriter(input, claim) {
1068
1517
  return claim.status === "ok" && (claim.provenance === "minted" || claim.source === "mint");
1069
1518
  }
1070
1519
  export function stealOccupancy(projectRoot, input = {}) {
1520
+ // #5544: steal uses the same lease-root view as claim/release.
1521
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
1071
1522
  const now = input.now ?? new Date();
1072
1523
  const path = occupancyPath(projectRoot);
1073
1524
  const claim = resolveOccupancySessionClaim(input);
@@ -1099,7 +1550,9 @@ export function stealOccupancy(projectRoot, input = {}) {
1099
1550
  // Show the occupant's write recency before the steal, not only after it
1100
1551
  // (#3599): heartbeat age alone hides an occupant that is mid-edit.
1101
1552
  const liveCurrent = liveOccupancyOnTree(projectRoot, current, now);
1102
- const occupantDetail = liveCurrent !== null ? `\n${formatOccupancyRemediation(liveCurrent, now)}` : "";
1553
+ const occupantDetail = liveCurrent !== null
1554
+ ? `\n${formatOccupancyRemediation(liveCurrent, now, undefined, undefined, projectRoot)}`
1555
+ : "";
1103
1556
  return {
1104
1557
  action: "denied",
1105
1558
  sessionId: incoming,
@@ -1145,7 +1598,7 @@ export function stealOccupancy(projectRoot, input = {}) {
1145
1598
  record: live,
1146
1599
  path,
1147
1600
  message: `occupancy:steal named occupant ${named} does not match live occupant ${live.sessionId}.\n` +
1148
- formatOccupancyRemediation(live, now),
1601
+ formatOccupancyRemediation(live, now, undefined, undefined, projectRoot),
1149
1602
  code: 1,
1150
1603
  };
1151
1604
  }
@@ -1189,7 +1642,7 @@ export function stealOccupancy(projectRoot, input = {}) {
1189
1642
  record: liveLocked,
1190
1643
  path,
1191
1644
  message: `occupancy:steal named occupant ${named} does not match live occupant ${liveLocked.sessionId}.\n` +
1192
- formatOccupancyRemediation(liveLocked, now),
1645
+ formatOccupancyRemediation(liveLocked, now, undefined, undefined, projectRoot),
1193
1646
  code: 1,
1194
1647
  };
1195
1648
  }
@@ -1224,16 +1677,19 @@ export function stealOccupancy(projectRoot, input = {}) {
1224
1677
  };
1225
1678
  }
1226
1679
  const record = written.record;
1680
+ const root = resolve(projectRoot);
1227
1681
  return {
1228
1682
  action: "stolen",
1229
1683
  sessionId: record.sessionId,
1230
1684
  record,
1231
1685
  path,
1232
1686
  message: `occupancy stolen from ${named}${priorClock}; writer is now session ${record.sessionId}. ` +
1687
+ `Lease view: ${formatOccupancyLeaseViewNote(path)}. Target root: ${root}. ` +
1688
+ `Presenter/writer: ${record.sessionId}. Permitted lease operation completed: occupancy:steal. ` +
1233
1689
  "This command changes the lease only; direct writes remain denied unless ritual state already names the same owner. " +
1234
- "If the owners differ, run `deft session:start --rearm --session-id=<same-session-id>` " +
1235
- "when re-arm is eligible; otherwise run `deft session:start --session-id=<same-session-id>` " +
1236
- "for a cold ceremony, using the writer ID above.",
1690
+ "Required ceremony: run " +
1691
+ "`deft session:start --rearm --session-id=<same-session-id>` when re-arm is eligible; otherwise run " +
1692
+ "`deft session:start --session-id=<same-session-id>` for a cold ceremony, using the writer ID above.",
1237
1693
  code: 0,
1238
1694
  };
1239
1695
  }, input.lockDeps);
@@ -1254,23 +1710,26 @@ export function stealOccupancy(projectRoot, input = {}) {
1254
1710
  */
1255
1711
  export function releaseOccupancy(projectRoot, input = {}) {
1256
1712
  const now = input.now ?? new Date();
1257
- const path = occupancyPath(projectRoot);
1713
+ // #5544 P2: share the lease-root view CLI and Write hooks use for this tree.
1714
+ const root = resolveOccupancyLeaseRoot(projectRoot);
1715
+ const path = occupancyPath(root);
1258
1716
  const identity = resolvePresentedIdentity(input);
1259
1717
  const caller = identity.sessionId;
1260
1718
  const split = formatPresentedIdentityDisagreement(identity);
1261
- return withOccupancyLock(projectRoot, (fence) => {
1262
- const existing = readOccupancy(projectRoot);
1719
+ const leaseNote = ` ${formatOccupancyLeaseViewNote(path)}`;
1720
+ return withOccupancyLock(root, (fence) => {
1721
+ const existing = readOccupancy(root);
1263
1722
  if (existing === null) {
1264
1723
  return {
1265
1724
  action: "released",
1266
1725
  sessionId: caller,
1267
1726
  record: null,
1268
1727
  path,
1269
- message: "occupancy already free",
1728
+ message: formatOccupancyAlreadyFree(path),
1270
1729
  code: 0,
1271
1730
  };
1272
1731
  }
1273
- const expired = liveOccupancyOnTree(projectRoot, existing, now) === null;
1732
+ const expired = liveOccupancyOnTree(root, existing, now) === null;
1274
1733
  const owns = caller.length > 0 && caller === existing.sessionId;
1275
1734
  if (!expired && !owns) {
1276
1735
  return {
@@ -1278,7 +1737,9 @@ export function releaseOccupancy(projectRoot, input = {}) {
1278
1737
  sessionId: caller,
1279
1738
  record: existing,
1280
1739
  path,
1281
- message: membershipOwnerDenial(existing, caller, now, "occupancy:release") + split,
1740
+ message: membershipOwnerDenial(existing, caller, now, "occupancy:release", root) +
1741
+ split +
1742
+ leaseNote,
1282
1743
  code: 1,
1283
1744
  };
1284
1745
  }
@@ -1286,6 +1747,8 @@ export function releaseOccupancy(projectRoot, input = {}) {
1286
1747
  // never stamp Prefer-A marker / rewrite AGENTS.md for a denied caller (#4544 P1).
1287
1748
  // Session exit with this-session untracked product evidence must still reach
1288
1749
  // enforce when markWrite persist never ran (Shell-bypass / hookless).
1750
+ // Prefer-A #5544 residual / babysit P1: header check uses the supplied project
1751
+ // root (consumer AGENTS.md), not the git lease root (which may be a parent repo).
1289
1752
  const evidenceGate = enforceConsumerHeaderPlaceholderWhenProductEvidence(projectRoot);
1290
1753
  if (!evidenceGate.ok) {
1291
1754
  return {
@@ -1293,19 +1756,19 @@ export function releaseOccupancy(projectRoot, input = {}) {
1293
1756
  sessionId: caller,
1294
1757
  record: existing,
1295
1758
  path,
1296
- message: evidenceGate.message,
1759
+ message: evidenceGate.message + leaseNote,
1297
1760
  code: 1,
1298
1761
  };
1299
1762
  }
1300
1763
  fence();
1301
- const still = readOccupancy(projectRoot);
1764
+ const still = readOccupancy(root);
1302
1765
  if (still === null) {
1303
1766
  return {
1304
1767
  action: "released",
1305
1768
  sessionId: caller,
1306
1769
  record: null,
1307
1770
  path,
1308
- message: "occupancy already free",
1771
+ message: formatOccupancyAlreadyFree(path),
1309
1772
  code: 0,
1310
1773
  };
1311
1774
  }
@@ -1318,17 +1781,19 @@ export function releaseOccupancy(projectRoot, input = {}) {
1318
1781
  sessionId: caller,
1319
1782
  record: still,
1320
1783
  path,
1321
- message: membershipOwnerDenial(still, caller, now, "occupancy:release") + split,
1784
+ message: membershipOwnerDenial(still, caller, now, "occupancy:release", root) +
1785
+ split +
1786
+ leaseNote,
1322
1787
  code: 1,
1323
1788
  };
1324
1789
  }
1325
- removeOccupancyFile(projectRoot, fence);
1790
+ removeOccupancyFile(root, fence);
1326
1791
  return {
1327
1792
  action: "released",
1328
1793
  sessionId: still.sessionId,
1329
1794
  record: null,
1330
1795
  path,
1331
- message: `occupancy released session ${still.sessionId}`,
1796
+ message: `occupancy released session ${still.sessionId} (${formatOccupancyLeaseViewNote(path)})`,
1332
1797
  code: 0,
1333
1798
  };
1334
1799
  }, input.lockDeps);
@@ -1374,6 +1839,8 @@ function ritualAlignedAfterAcquisition(projectRoot, acquired) {
1374
1839
  * this persist.
1375
1840
  */
1376
1841
  export function rollbackAcquiredOccupancy(projectRoot, acquired, input = {}) {
1842
+ // #5544: rollback must target the same lease root claim wrote.
1843
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
1377
1844
  const path = occupancyPath(projectRoot);
1378
1845
  if (!isPersistTimeAcquisition(acquired.action)) {
1379
1846
  return {
@@ -1393,7 +1860,7 @@ export function rollbackAcquiredOccupancy(projectRoot, acquired, input = {}) {
1393
1860
  sessionId: acquired.sessionId,
1394
1861
  record: null,
1395
1862
  path,
1396
- message: "occupancy already free",
1863
+ message: formatOccupancyAlreadyFree(path),
1397
1864
  code: 0,
1398
1865
  };
1399
1866
  }
@@ -1425,7 +1892,7 @@ export function rollbackAcquiredOccupancy(projectRoot, acquired, input = {}) {
1425
1892
  sessionId: acquired.sessionId,
1426
1893
  record: null,
1427
1894
  path,
1428
- message: "occupancy already free",
1895
+ message: formatOccupancyAlreadyFree(path),
1429
1896
  code: 0,
1430
1897
  };
1431
1898
  }
@@ -1470,10 +1937,10 @@ export function rollbackAcquiredOccupancy(projectRoot, acquired, input = {}) {
1470
1937
  * have chosen explicitly, so a refusal that does not name what was presented
1471
1938
  * leaves it guessing which identity it was refused under.
1472
1939
  */
1473
- function membershipOwnerDenial(live, caller, now, verb) {
1940
+ function membershipOwnerDenial(live, caller, now, verb, projectRoot) {
1474
1941
  const grant = occupancyGrantFor(live, caller, now);
1475
1942
  return grant === null
1476
- ? formatOccupancyRemediation(live, now, caller)
1943
+ ? formatOccupancyRemediation(live, now, caller, undefined, projectRoot)
1477
1944
  : formatOccupancyMemberAdministrationRefusal(live, grant, verb);
1478
1945
  }
1479
1946
  /**
@@ -1486,6 +1953,8 @@ function membershipOwnerDenial(live, caller, now, verb) {
1486
1953
  * cap — and it buys writes alone.
1487
1954
  */
1488
1955
  export function grantOccupancyMembership(projectRoot, input = {}) {
1956
+ // #5544: membership verbs share claim/release lease-root view.
1957
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
1489
1958
  const now = input.now ?? new Date();
1490
1959
  const path = occupancyPath(projectRoot);
1491
1960
  const identity = resolvePresentedIdentity(input);
@@ -1604,7 +2073,7 @@ export function grantOccupancyMembership(projectRoot, input = {}) {
1604
2073
  sessionId: owner,
1605
2074
  record: live,
1606
2075
  path,
1607
- message: membershipOwnerDenial(live, owner, now, "occupancy:grant") + split,
2076
+ message: membershipOwnerDenial(live, owner, now, "occupancy:grant", projectRoot) + split,
1608
2077
  code: 1,
1609
2078
  };
1610
2079
  }
@@ -1695,6 +2164,8 @@ export function grantOccupancyMembership(projectRoot, input = {}) {
1695
2164
  * never have been admitted, and the owner wants that true now.
1696
2165
  */
1697
2166
  export function revokeOccupancyMembership(projectRoot, input = {}) {
2167
+ // #5544: membership verbs share claim/release lease-root view.
2168
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
1698
2169
  const now = input.now ?? new Date();
1699
2170
  const path = occupancyPath(projectRoot);
1700
2171
  const identity = resolvePresentedIdentity(input);
@@ -1734,7 +2205,8 @@ export function revokeOccupancyMembership(projectRoot, input = {}) {
1734
2205
  sessionId: owner,
1735
2206
  record: live,
1736
2207
  path,
1737
- message: membershipOwnerDenial(live, owner, now, "occupancy:grant --revoke") + split,
2208
+ message: membershipOwnerDenial(live, owner, now, "occupancy:grant --revoke", projectRoot) +
2209
+ split,
1738
2210
  code: 1,
1739
2211
  };
1740
2212
  }
@@ -1795,6 +2267,10 @@ export function revokeOccupancyMembership(projectRoot, input = {}) {
1795
2267
  */
1796
2268
  export function evaluateOccupancyWriteGate(projectRoot, input = {}) {
1797
2269
  const now = input.now ?? new Date();
2270
+ // #5544: lease identity is always the git worktree toplevel. leaseRoot is only
2271
+ // a preferred base path (hook effectiveRoot may be a subdirectory when writes
2272
+ // leave the git tree); never skip resolve, or read/match disagree (#5544 P1).
2273
+ projectRoot = resolveOccupancyLeaseRoot(input.leaseRoot ?? projectRoot);
1798
2274
  const incoming = input.sessionId?.trim() || (input.env ?? process.env).DEFT_SESSION_ID?.trim() || "";
1799
2275
  const stored = readOccupancy(projectRoot);
1800
2276
  const record = stored !== null && occupancyWorktreeMatches(stored.worktreePath, projectRoot) ? stored : null;
@@ -1848,12 +2324,16 @@ export function evaluateOccupancyWriteGate(projectRoot, input = {}) {
1848
2324
  // Two-axis unknown-identity axis (#3729 Prefer-A): live lease + stranger →
1849
2325
  // fail closed (OCCUPANCY_UNKNOWN_IDENTITY_POLICY). Advisory coordination,
1850
2326
  // not authorization — prefer another worktree / grant over steal-as-primary.
2327
+ // #5544 P2: name the occupancy file (and effectiveRoot when the Write gate
2328
+ // supplies this projectRoot) so CLI/hook split-brain is diagnosable.
2329
+ const path = occupancyPath(projectRoot);
1851
2330
  return {
1852
2331
  allow: false,
1853
2332
  // The refused caller is told what identity it actually presented (#3873).
1854
2333
  // A hook process cannot otherwise know, and the grant this message offers
1855
2334
  // is only runnable when the occupant can name a non-empty child.
1856
- message: formatOccupancyRemediation(live, now, incoming, input.env),
2335
+ message: `${formatOccupancyRemediation(live, now, incoming, input.env, projectRoot)} ` +
2336
+ formatOccupancyLeaseViewNote(path, { effectiveRoot: projectRoot }),
1857
2337
  occupant: live,
1858
2338
  refreshed: false,
1859
2339
  warning: null,
@@ -2031,6 +2511,8 @@ persistProductMutationMarker = false) {
2031
2511
  * heartbeat into long verbs cannot close that deadlock: this verb is owner-only.
2032
2512
  */
2033
2513
  export function heartbeatOccupancy(projectRoot, input = {}) {
2514
+ // #5544: heartbeat shares claim/release lease-root view.
2515
+ projectRoot = resolveOccupancyLeaseRoot(projectRoot);
2034
2516
  const now = input.now ?? new Date();
2035
2517
  const path = occupancyPath(projectRoot);
2036
2518
  const identity = resolvePresentedIdentity(input);
@@ -2072,7 +2554,7 @@ export function heartbeatOccupancy(projectRoot, input = {}) {
2072
2554
  sessionId: caller,
2073
2555
  record: live,
2074
2556
  path,
2075
- message: membershipOwnerDenial(live, caller, now, "occupancy:heartbeat") + split,
2557
+ message: membershipOwnerDenial(live, caller, now, "occupancy:heartbeat", projectRoot) + split,
2076
2558
  code: 1,
2077
2559
  };
2078
2560
  }