@deftai/directive-core 0.106.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/gate-lists.js +2 -0
  4. package/dist/check/named-cause.js +18 -3
  5. package/dist/codebase/provider.js +7 -1
  6. package/dist/delivery-attempt/handoff.js +1 -1
  7. package/dist/design-critique/citation-grammar.d.ts +53 -0
  8. package/dist/design-critique/citation-grammar.js +303 -0
  9. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  10. package/dist/design-critique/completed-arc-record.js +232 -0
  11. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  12. package/dist/design-critique/exclusive-chip.js +68 -0
  13. package/dist/design-critique/parent-audit.d.ts +59 -0
  14. package/dist/design-critique/parent-audit.js +121 -0
  15. package/dist/doctor/checks.d.ts +6 -0
  16. package/dist/doctor/checks.js +35 -0
  17. package/dist/freshness/bind.d.ts +32 -3
  18. package/dist/freshness/bind.js +58 -12
  19. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  20. package/dist/hooks/classify/host-session-identity.js +470 -0
  21. package/dist/hooks/classify/index.d.ts +3 -2
  22. package/dist/hooks/classify/index.js +3 -2
  23. package/dist/hooks/classify/paths.d.ts +7 -0
  24. package/dist/hooks/classify/paths.js +35 -0
  25. package/dist/hooks/classify/stdin.d.ts +5 -0
  26. package/dist/hooks/classify/stdin.js +27 -0
  27. package/dist/hooks/dest-form.d.ts +46 -0
  28. package/dist/hooks/dest-form.js +575 -0
  29. package/dist/hooks/dispatcher.d.ts +69 -2
  30. package/dist/hooks/dispatcher.js +609 -45
  31. package/dist/hooks/fixtures/cases.d.ts +16 -2
  32. package/dist/hooks/fixtures/cases.js +175 -0
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/index.js +1 -0
  35. package/dist/hooks/scope.js +3 -1
  36. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  37. package/dist/init-deposit/agent-hooks.js +8 -1
  38. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  39. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  40. package/dist/intake/clause-derivation.js +9 -3
  41. package/dist/intake/github-auth-modes-cli.js +14 -2
  42. package/dist/intake/github-auth-modes.d.ts +54 -9
  43. package/dist/intake/github-auth-modes.js +326 -123
  44. package/dist/intake/issue-ingest.js +62 -2
  45. package/dist/intake/platform-capabilities.d.ts +9 -2
  46. package/dist/intake/platform-capabilities.js +38 -9
  47. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  48. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  49. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  50. package/dist/lifecycle/completed-write-guard.js +373 -0
  51. package/dist/lifecycle/index.d.ts +1 -0
  52. package/dist/lifecycle/index.js +1 -0
  53. package/dist/literal-acceptance/capture.d.ts +16 -0
  54. package/dist/literal-acceptance/capture.js +75 -7
  55. package/dist/literal-acceptance/evaluate.js +25 -3
  56. package/dist/literal-acceptance/index.d.ts +2 -2
  57. package/dist/literal-acceptance/index.js +2 -2
  58. package/dist/literal-acceptance/run.js +9 -3
  59. package/dist/literal-acceptance/types.d.ts +6 -0
  60. package/dist/literal-acceptance/types.js +6 -0
  61. package/dist/orphan-active/evaluate.d.ts +25 -0
  62. package/dist/orphan-active/evaluate.js +193 -128
  63. package/dist/orphan-active/index.d.ts +1 -0
  64. package/dist/orphan-active/index.js +1 -0
  65. package/dist/orphan-active/issue-state.d.ts +121 -0
  66. package/dist/orphan-active/issue-state.js +278 -0
  67. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  68. package/dist/platform/cursor-managed-runtime.js +241 -0
  69. package/dist/platform/index.d.ts +1 -0
  70. package/dist/platform/index.js +1 -0
  71. package/dist/platform/platform-capabilities.d.ts +23 -1
  72. package/dist/platform/platform-capabilities.js +55 -15
  73. package/dist/policy/ceremony-dial.js +5 -9
  74. package/dist/policy/host-hooks.js +4 -9
  75. package/dist/policy/org-force-on-migration.js +5 -10
  76. package/dist/policy/plan-extensions.d.ts +14 -2
  77. package/dist/policy/plan-extensions.js +24 -3
  78. package/dist/policy/product-signal.js +5 -10
  79. package/dist/policy/require-human-merge.js +5 -9
  80. package/dist/policy/resolve.js +77 -16
  81. package/dist/policy/runtime-authority.d.ts +18 -0
  82. package/dist/policy/runtime-authority.js +11 -0
  83. package/dist/policy/value-feedback.js +9 -18
  84. package/dist/policy/write-fence.js +3 -0
  85. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  86. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  87. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  88. package/dist/pr-closeout-attestable/index.js +2 -0
  89. package/dist/pr-protected-issues/gh.d.ts +6 -2
  90. package/dist/pr-protected-issues/gh.js +7 -3
  91. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  92. package/dist/pr-wait-mergeable/cascade.js +21 -1
  93. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  94. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  95. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  96. package/dist/preflight/evaluate.d.ts +15 -0
  97. package/dist/preflight/evaluate.js +29 -3
  98. package/dist/preflight-cache/evaluate.js +9 -3
  99. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  100. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  101. package/dist/product-first-done-gate/evaluate.js +53 -12
  102. package/dist/release/cli-drift-report.d.ts +68 -0
  103. package/dist/release/cli-drift-report.js +189 -0
  104. package/dist/release/index.d.ts +1 -0
  105. package/dist/release/index.js +1 -0
  106. package/dist/release/issue-state-fetch.d.ts +5 -3
  107. package/dist/release/issue-state-fetch.js +86 -21
  108. package/dist/release/native-steps.js +1 -0
  109. package/dist/release/pipeline.js +19 -0
  110. package/dist/release/types.d.ts +14 -0
  111. package/dist/render/constants.d.ts +4 -0
  112. package/dist/render/constants.js +11 -8
  113. package/dist/render/export-spec.js +31 -5
  114. package/dist/render/index.d.ts +1 -1
  115. package/dist/render/index.js +1 -1
  116. package/dist/render/prd-render.d.ts +4 -1
  117. package/dist/render/prd-render.js +82 -23
  118. package/dist/render/project-render.js +14 -7
  119. package/dist/render/scope-outlook.d.ts +2 -0
  120. package/dist/render/scope-outlook.js +3 -0
  121. package/dist/render/spec-render.js +2 -2
  122. package/dist/resolution/package-manager.d.ts +33 -4
  123. package/dist/resolution/package-manager.js +210 -9
  124. package/dist/review-monitor/constants.d.ts +7 -0
  125. package/dist/review-monitor/constants.js +7 -0
  126. package/dist/review-monitor/github-lease.d.ts +98 -1
  127. package/dist/review-monitor/github-lease.js +182 -1
  128. package/dist/review-monitor/lease-comment.d.ts +36 -0
  129. package/dist/review-monitor/lease-comment.js +103 -2
  130. package/dist/scm/call.js +12 -3
  131. package/dist/scm/design-critique-chip.d.ts +38 -0
  132. package/dist/scm/design-critique-chip.js +187 -0
  133. package/dist/scm/gh-rest.d.ts +9 -1
  134. package/dist/scm/gh-rest.js +103 -0
  135. package/dist/scm/index.d.ts +1 -0
  136. package/dist/scm/index.js +1 -0
  137. package/dist/scm/main.d.ts +3 -0
  138. package/dist/scm/main.js +15 -1
  139. package/dist/scm/readiness-cli.d.ts +2 -0
  140. package/dist/scm/readiness-cli.js +42 -1
  141. package/dist/scm/readiness.d.ts +4 -1
  142. package/dist/scm/readiness.js +35 -5
  143. package/dist/scope/decompose.js +2 -1
  144. package/dist/scope/delivery-evidence.d.ts +27 -1
  145. package/dist/scope/index.d.ts +1 -0
  146. package/dist/scope/index.js +1 -0
  147. package/dist/scope/lifecycle-write.d.ts +24 -0
  148. package/dist/scope/lifecycle-write.js +64 -0
  149. package/dist/scope/main.js +3 -1
  150. package/dist/scope/project-definition-sync.js +14 -7
  151. package/dist/scope/transition.js +90 -11
  152. package/dist/scope-provenance/evaluate.js +5 -1
  153. package/dist/session/git.d.ts +42 -0
  154. package/dist/session/git.js +219 -3
  155. package/dist/session/occupancy.d.ts +239 -12
  156. package/dist/session/occupancy.js +820 -27
  157. package/dist/session/ritual-sentinel.d.ts +5 -0
  158. package/dist/session/ritual-sentinel.js +5 -0
  159. package/dist/session/session-ready.d.ts +5 -1
  160. package/dist/session/session-ready.js +98 -8
  161. package/dist/session/session-start.d.ts +17 -1
  162. package/dist/session/session-start.js +49 -17
  163. package/dist/session/toolchain-preflight.d.ts +13 -3
  164. package/dist/session/toolchain-preflight.js +93 -18
  165. package/dist/session/verify-session-ritual.d.ts +33 -3
  166. package/dist/session/verify-session-ritual.js +89 -19
  167. package/dist/slice/constants.d.ts +1 -1
  168. package/dist/spec-authority/constants.d.ts +10 -2
  169. package/dist/spec-authority/constants.js +53 -8
  170. package/dist/spec-authority/resolver.d.ts +3 -0
  171. package/dist/spec-authority/resolver.js +55 -7
  172. package/dist/subprocess/max-buffer.d.ts +14 -0
  173. package/dist/subprocess/max-buffer.js +15 -0
  174. package/dist/swarm/complete-cohort.d.ts +2 -0
  175. package/dist/swarm/complete-cohort.js +21 -13
  176. package/dist/swarm/index.d.ts +1 -0
  177. package/dist/swarm/index.js +1 -0
  178. package/dist/swarm/launch-cli.js +53 -33
  179. package/dist/swarm/launch.d.ts +77 -0
  180. package/dist/swarm/launch.js +248 -38
  181. package/dist/swarm/pre-dispatch-cli.js +2 -1
  182. package/dist/swarm/pre-dispatch.js +10 -1
  183. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  184. package/dist/swarm/subagent-status-dir.js +42 -0
  185. package/dist/swarm/worktrees.d.ts +17 -3
  186. package/dist/swarm/worktrees.js +48 -4
  187. package/dist/triage/actions/index.d.ts +4 -0
  188. package/dist/triage/actions/index.js +6 -0
  189. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  190. package/dist/triage/evaluate/evaluate.js +169 -0
  191. package/dist/triage/evaluate/github.d.ts +16 -0
  192. package/dist/triage/evaluate/github.js +153 -0
  193. package/dist/triage/evaluate/index.d.ts +9 -0
  194. package/dist/triage/evaluate/index.js +8 -0
  195. package/dist/triage/evaluate/paths.d.ts +7 -0
  196. package/dist/triage/evaluate/paths.js +25 -0
  197. package/dist/triage/evaluate/sink.d.ts +5 -0
  198. package/dist/triage/evaluate/sink.js +52 -0
  199. package/dist/triage/evaluate/types.d.ts +93 -0
  200. package/dist/triage/evaluate/types.js +13 -0
  201. package/dist/triage/evaluate/validity.d.ts +8 -0
  202. package/dist/triage/evaluate/validity.js +87 -0
  203. package/dist/triage/evaluate/value.d.ts +12 -0
  204. package/dist/triage/evaluate/value.js +33 -0
  205. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  206. package/dist/triage/evaluate/wip-census.js +39 -0
  207. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  208. package/dist/triage/evaluate/worktrees.js +68 -0
  209. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  210. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  211. package/dist/triage/help/registry-data.d.ts +21 -7
  212. package/dist/triage/help/registry-data.js +53 -5
  213. package/dist/triage/index.d.ts +1 -0
  214. package/dist/triage/index.js +1 -0
  215. package/dist/triage/scope/mutations-core.d.ts +2 -6
  216. package/dist/triage/scope/mutations-core.js +23 -55
  217. package/dist/triage/scope-drift/add-ignore.js +5 -39
  218. package/dist/triage/subscribe/index.d.ts +2 -6
  219. package/dist/triage/subscribe/index.js +15 -65
  220. package/dist/triage/welcome/writers.js +15 -74
  221. package/dist/value/feedback-file.d.ts +17 -2
  222. package/dist/value/feedback-file.js +80 -6
  223. package/dist/vbrief-build/index.d.ts +2 -1
  224. package/dist/vbrief-build/index.js +2 -1
  225. package/dist/vbrief-build/parity-scenarios.js +5 -5
  226. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  227. package/dist/vbrief-build/project-definition-io.js +357 -31
  228. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  229. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  230. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  231. package/dist/vbrief-reconcile/labels.js +30 -0
  232. package/dist/vbrief-validate/conformance.d.ts +1 -0
  233. package/dist/vbrief-validate/conformance.js +85 -8
  234. package/dist/vbrief-validate/main.js +2 -0
  235. package/dist/vbrief-validate/precutover.js +5 -10
  236. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  237. package/dist/vbrief-validation/story-quality.js +8 -1
  238. package/dist/verify-ac/clauses.d.ts +62 -2
  239. package/dist/verify-ac/clauses.js +183 -111
  240. package/dist/verify-ac/index.d.ts +1 -1
  241. package/dist/verify-ac/index.js +1 -1
  242. package/dist/verify-env/node-runtime.d.ts +8 -4
  243. package/dist/verify-env/node-runtime.js +9 -6
  244. package/dist/verify-env/toolchain-check.d.ts +35 -3
  245. package/dist/verify-env/toolchain-check.js +155 -36
  246. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  247. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  248. package/dist/xbrief-migrate/agents-header.js +69 -7
  249. package/package.json +7 -3
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Host-agnostic storage:
5
5
  * - Default (no sessionId): `.deft/session-bind.json` — single-operator convenience.
6
- * - With sessionId: `.deft/session-binds/<safeId>.json` — isolated multi-session binds.
6
+ * - With sessionId: `.deft/session-binds/<sha256-slice>.json` — isolated multi-session binds.
7
7
  *
8
8
  * Multi-agent hosts MUST pass a stable host session identity when binding and
9
9
  * reporting so one session cannot certify another as current.
@@ -26,14 +26,44 @@ export function sessionBindPath(projectRoot, sessionId) {
26
26
  }
27
27
  return join(root, SESSION_BINDS_DIR_REL, safeSessionFileName(id));
28
28
  }
29
+ function sessionIdHash(sessionId) {
30
+ return createHash("sha256").update(sessionId.trim(), "utf8").digest("hex").slice(0, 24);
31
+ }
29
32
  /**
30
33
  * Stable filesystem-safe file name for a host session id.
31
- * Keeps a short prefix for debug, hashes the rest to avoid path injection.
32
- * Character filter is O(n) (no regex) CodeQL poly-redos on uncontrolled ids.
34
+ *
35
+ * Hash only (#3768): a 24-hex SHA-256 slice of the id. Hex cannot carry a path
36
+ * separator, so path injection is structurally impossible, and the name stays
37
+ * derivable from the id — lookup is one computed path, never a directory scan.
38
+ *
39
+ * Records written before #3768 also carried the first 32 characters of the id
40
+ * (`legacySessionFileName`). Dropping that prefix is hygiene, not a security
41
+ * fix: the same id is already published in `.deft/occupancy.json`,
42
+ * `.deft/ritual-state.json` and the bind record body (#3611 / #3754).
43
+ *
44
+ * Coupling warning: if `.deft/` JSON bodies are ever tightened (say to `0600`)
45
+ * while names still carry the prefix, the prefix becomes independently
46
+ * load-bearing — directory read permission alone would recover an id that the
47
+ * file mode was meant to protect. Anyone tightening those modes must keep bind
48
+ * names prefix-free.
33
49
  */
34
50
  export function safeSessionFileName(sessionId) {
51
+ return `${sessionIdHash(sessionId)}.json`;
52
+ }
53
+ /**
54
+ * Pre-#3768 record name: sanitized 32-character id prefix plus the same hash.
55
+ *
56
+ * Existing records are **tolerated, not migrated**. `readBoundGeneration` falls
57
+ * back to this name when the hash-only record is absent, so freshness pins
58
+ * written before the rename are not orphaned; writes always use the hash-only
59
+ * name, so the next bind supersedes the legacy record. Both names derive from
60
+ * the id, so the fallback stays O(1) and adds no directory scan.
61
+ *
62
+ * Character filter is O(n) (no regex) — CodeQL poly-redos on uncontrolled ids.
63
+ */
64
+ export function legacySessionFileName(sessionId) {
35
65
  const trimmed = sessionId.trim();
36
- const hash = createHash("sha256").update(trimmed, "utf8").digest("hex").slice(0, 24);
66
+ const hash = sessionIdHash(trimmed);
37
67
  let prefix = "";
38
68
  for (let i = 0; i < trimmed.length && prefix.length < 32; i++) {
39
69
  const ch = trimmed[i] ?? "";
@@ -60,6 +90,12 @@ export function safeSessionFileName(sessionId) {
60
90
  const base = prefix.length > 0 ? `${prefix}-${hash}` : hash;
61
91
  return `${base}.json`;
62
92
  }
93
+ /**
94
+ * Derived path of a pre-#3768 record. Read fallback only — never a write target.
95
+ */
96
+ export function legacySessionBindPath(projectRoot, sessionId) {
97
+ return join(resolve(projectRoot), SESSION_BINDS_DIR_REL, legacySessionFileName(sessionId));
98
+ }
63
99
  function nowIsoDefault() {
64
100
  return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
65
101
  }
@@ -108,12 +144,7 @@ export function parseBoundGeneration(raw) {
108
144
  ...(payloadLoaded ? { payloadLoaded: true } : {}),
109
145
  };
110
146
  }
111
- /** Read the session bind record (null when absent/unreadable). */
112
- export function readBoundGeneration(projectRoot, options = {}) {
113
- const path = sessionBindPath(projectRoot, options.sessionId);
114
- if (!existsSync(path)) {
115
- return null;
116
- }
147
+ function readBoundAt(path, wantSessionId) {
117
148
  try {
118
149
  const text = readFileSync(path, "utf8");
119
150
  const bound = parseBoundGeneration(JSON.parse(text));
@@ -122,8 +153,7 @@ export function readBoundGeneration(projectRoot, options = {}) {
122
153
  }
123
154
  // When reading a session-scoped bind, refuse a record whose embedded
124
155
  // sessionId disagrees (tamper / wrong file).
125
- const want = typeof options.sessionId === "string" ? options.sessionId.trim() : "";
126
- if (want.length > 0 && bound.sessionId && bound.sessionId !== want) {
156
+ if (wantSessionId.length > 0 && bound.sessionId && bound.sessionId !== wantSessionId) {
127
157
  return null;
128
158
  }
129
159
  return bound;
@@ -132,6 +162,22 @@ export function readBoundGeneration(projectRoot, options = {}) {
132
162
  return null;
133
163
  }
134
164
  }
165
+ /** Read the session bind record (null when absent/unreadable). */
166
+ export function readBoundGeneration(projectRoot, options = {}) {
167
+ const want = typeof options.sessionId === "string" ? options.sessionId.trim() : "";
168
+ const path = sessionBindPath(projectRoot, options.sessionId);
169
+ if (existsSync(path)) {
170
+ return readBoundAt(path, want);
171
+ }
172
+ if (want.length === 0) {
173
+ return null;
174
+ }
175
+ // Pre-#3768 records are tolerated, not migrated: one extra derived path and
176
+ // still no directory scan, so pins written under the old name resolve until
177
+ // the next bind supersedes them.
178
+ const legacy = legacySessionBindPath(projectRoot, want);
179
+ return existsSync(legacy) ? readBoundAt(legacy, want) : null;
180
+ }
135
181
  function writeBoundAt(projectRoot, bound, sessionId) {
136
182
  const root = resolve(projectRoot);
137
183
  const target = sessionBindPath(projectRoot, sessionId);
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Cooperative host-session identity and exact lifecycle-command rewriting (#3611).
3
+ *
4
+ * This is payload classification, not authentication. Host hook JSON is locally
5
+ * forgeable under the occupancy model's cooperating-process assumption.
6
+ */
7
+ export declare const HOST_IDENTITY_PROVIDERS: readonly ["codex", "claude", "cursor"];
8
+ export type HookHostIdentityProvider = (typeof HOST_IDENTITY_PROVIDERS)[number];
9
+ export declare const MAX_HOOK_HOST_IDENTITY_UTF8_BYTES = 512;
10
+ export type HookHostIdentityStatus = "ok" | "missing" | "invalid" | "conflict" | "unsupported";
11
+ export type HookHostIdentityResolution = {
12
+ readonly status: "ok";
13
+ readonly provider: HookHostIdentityProvider;
14
+ readonly rawSessionId: string;
15
+ readonly sessionId: string;
16
+ readonly message: null;
17
+ } | {
18
+ readonly status: Exclude<HookHostIdentityStatus, "ok">;
19
+ readonly provider: string;
20
+ readonly rawSessionId: null;
21
+ readonly sessionId: null;
22
+ readonly message: string;
23
+ };
24
+ /**
25
+ * Resolve the stable conversation/session-family key documented by each host.
26
+ *
27
+ * - Codex: `session_id` (parent and subagents share it).
28
+ * - Claude Code: `session_id` (`agent_id` is deliberately not the owner key).
29
+ * - Cursor: `conversation_id`; simultaneous `session_id` must agree.
30
+ * - Grok/unknown: no guessed payload identity; callers keep explicit/env flow.
31
+ */
32
+ export declare function resolveHookHostIdentity(host: string, payload: unknown): HookHostIdentityResolution;
33
+ export declare const EXACT_LIFECYCLE_VERBS: readonly ["session:start", "session:ready", "session:end", "occupancy:steal", "occupancy:release", "occupancy:heartbeat", "swarm:launch"];
34
+ export type ExactLifecycleVerb = (typeof EXACT_LIFECYCLE_VERBS)[number];
35
+ export interface ExactLifecycleCommandRewrite {
36
+ readonly kind: "rewrite";
37
+ readonly verb: ExactLifecycleVerb;
38
+ readonly originalCommand: string;
39
+ readonly rewrittenCommand: string;
40
+ readonly updatedInput: Record<string, unknown>;
41
+ }
42
+ export interface ExactLifecycleCommandConflict {
43
+ readonly kind: "conflict";
44
+ readonly verb: ExactLifecycleVerb;
45
+ readonly existingSessionId: string;
46
+ readonly requestedSessionId: string;
47
+ readonly message: string;
48
+ }
49
+ export type ExactLifecycleCommandResult = ExactLifecycleCommandRewrite | ExactLifecycleCommandConflict | null;
50
+ /** Logical lifecycle verb for one exact, simple shell command; otherwise null. */
51
+ export declare function exactLifecycleCommandVerb(payload: unknown): ExactLifecycleVerb | null;
52
+ interface SessionIdArgs {
53
+ readonly status: "absent" | "present" | "invalid";
54
+ readonly values: readonly string[];
55
+ }
56
+ export interface ExactLifecycleCommandInspection {
57
+ readonly verb: ExactLifecycleVerb;
58
+ readonly task: boolean;
59
+ readonly rewriteSafe: boolean;
60
+ readonly requiresOwner: boolean;
61
+ readonly sessionIdStatus: SessionIdArgs["status"];
62
+ readonly sessionId: string | null;
63
+ }
64
+ /** Inspect one exact lifecycle command without requiring a host identity. */
65
+ export declare function inspectExactLifecycleCommand(payload: unknown): ExactLifecycleCommandInspection | null;
66
+ /**
67
+ * Append a canonical host owner to one exact Directive lifecycle command.
68
+ *
69
+ * The returned `updatedInput` is a complete shallow clone of the original
70
+ * host tool input. `null` means no safe rewrite. An explicit foreign identity
71
+ * is surfaced as a conflict so dispatcher wiring can deny instead of replacing
72
+ * operator input.
73
+ */
74
+ export declare function rewriteExactLifecycleCommand(payload: unknown, sessionId: string): ExactLifecycleCommandResult;
75
+ export {};
76
+ //# sourceMappingURL=host-session-identity.d.ts.map
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Cooperative host-session identity and exact lifecycle-command rewriting (#3611).
3
+ *
4
+ * This is payload classification, not authentication. Host hook JSON is locally
5
+ * forgeable under the occupancy model's cooperating-process assumption.
6
+ */
7
+ import { isShellTool } from "../tools.js";
8
+ import { record, toolInputRecord } from "./payload.js";
9
+ import { hookToolName } from "./tool-name.js";
10
+ export const HOST_IDENTITY_PROVIDERS = ["codex", "claude", "cursor"];
11
+ export const MAX_HOOK_HOST_IDENTITY_UTF8_BYTES = 512;
12
+ function hasControlCharacter(value) {
13
+ for (let index = 0; index < value.length; index += 1) {
14
+ const unit = value.charCodeAt(index);
15
+ if (unit <= 0x1f || (unit >= 0x7f && unit <= 0x9f))
16
+ return true;
17
+ }
18
+ return false;
19
+ }
20
+ function hasUnpairedUtf16Surrogate(value) {
21
+ for (let index = 0; index < value.length; index += 1) {
22
+ const unit = value.charCodeAt(index);
23
+ if (unit >= 0xd800 && unit <= 0xdbff) {
24
+ const next = value.charCodeAt(index + 1);
25
+ if (!(next >= 0xdc00 && next <= 0xdfff))
26
+ return true;
27
+ index += 1;
28
+ continue;
29
+ }
30
+ if (unit >= 0xdc00 && unit <= 0xdfff)
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ function identityField(input, fieldName) {
36
+ if (!(fieldName in input))
37
+ return { status: "missing", value: null };
38
+ const raw = input[fieldName];
39
+ if (typeof raw !== "string" ||
40
+ raw.length === 0 ||
41
+ raw !== raw.trim() ||
42
+ hasControlCharacter(raw) ||
43
+ hasUnpairedUtf16Surrogate(raw) ||
44
+ Buffer.byteLength(raw, "utf8") > MAX_HOOK_HOST_IDENTITY_UTF8_BYTES) {
45
+ return { status: "invalid", value: null };
46
+ }
47
+ return { status: "ok", value: raw };
48
+ }
49
+ function unresolvedHostIdentity(status, provider, message) {
50
+ return { status, provider, rawSessionId: null, sessionId: null, message };
51
+ }
52
+ function canonicalHostSessionId(provider, rawSessionId) {
53
+ const encoded = Buffer.from(rawSessionId, "utf8").toString("base64url");
54
+ return `host:${provider}:v1:${encoded}`;
55
+ }
56
+ function resolvedHostIdentity(provider, rawSessionId) {
57
+ return {
58
+ status: "ok",
59
+ provider,
60
+ rawSessionId,
61
+ sessionId: canonicalHostSessionId(provider, rawSessionId),
62
+ message: null,
63
+ };
64
+ }
65
+ /**
66
+ * Resolve the stable conversation/session-family key documented by each host.
67
+ *
68
+ * - Codex: `session_id` (parent and subagents share it).
69
+ * - Claude Code: `session_id` (`agent_id` is deliberately not the owner key).
70
+ * - Cursor: `conversation_id`; simultaneous `session_id` must agree.
71
+ * - Grok/unknown: no guessed payload identity; callers keep explicit/env flow.
72
+ */
73
+ export function resolveHookHostIdentity(host, payload) {
74
+ if (!HOST_IDENTITY_PROVIDERS.includes(host)) {
75
+ return unresolvedHostIdentity("unsupported", host, `Host ${host || "<empty>"} has no verified payload identity contract.`);
76
+ }
77
+ const provider = host;
78
+ const input = record(payload);
79
+ if (input === null) {
80
+ return unresolvedHostIdentity("invalid", provider, `${provider} hook payload is not an object.`);
81
+ }
82
+ if (provider === "codex" || provider === "claude") {
83
+ const session = identityField(input, "session_id");
84
+ if (session.status !== "ok") {
85
+ return unresolvedHostIdentity(session.status, provider, `${provider} hook payload ${session.status === "missing" ? "omits" : "has invalid"} session_id.`);
86
+ }
87
+ return resolvedHostIdentity(provider, session.value);
88
+ }
89
+ const conversation = identityField(input, "conversation_id");
90
+ if (conversation.status !== "ok") {
91
+ return unresolvedHostIdentity(conversation.status, provider, `Cursor hook payload ${conversation.status === "missing" ? "omits" : "has invalid"} conversation_id.`);
92
+ }
93
+ const simultaneousSession = identityField(input, "session_id");
94
+ if (simultaneousSession.status === "invalid") {
95
+ return unresolvedHostIdentity("invalid", provider, "Cursor hook payload has invalid session_id alongside conversation_id.");
96
+ }
97
+ if (simultaneousSession.status === "ok" && simultaneousSession.value !== conversation.value) {
98
+ return unresolvedHostIdentity("conflict", provider, "Cursor hook identity conflict: conversation_id and session_id differ.");
99
+ }
100
+ return resolvedHostIdentity(provider, conversation.value);
101
+ }
102
+ export const EXACT_LIFECYCLE_VERBS = [
103
+ "session:start",
104
+ "session:ready",
105
+ "session:end",
106
+ "occupancy:steal",
107
+ "occupancy:release",
108
+ "occupancy:heartbeat",
109
+ "swarm:launch",
110
+ ];
111
+ const DIRECT_LIFECYCLE_VERBS = {
112
+ "session:start": "session:start",
113
+ "session:ready": "session:ready",
114
+ "session:end": "session:end",
115
+ "occupancy:steal": "occupancy:steal",
116
+ "occupancy:release": "occupancy:release",
117
+ "occupancy:heartbeat": "occupancy:heartbeat",
118
+ "swarm-launch": "swarm:launch",
119
+ };
120
+ const CANONICAL_OWNER_PATTERN = /^host:(?:codex|claude|cursor):v1:[A-Za-z0-9_-]+$/;
121
+ // Shell expansion markers are deliberately absent: `$`/backticks for POSIX,
122
+ // `@` splatting for PowerShell, and `%NAME%` expansion for command shells.
123
+ // Backslashes are inspectable so Windows path-bearing lifecycle commands fail
124
+ // closed, but they remain outside the auto-approved rewrite surface below.
125
+ const INSPECTABLE_TOKEN_PATTERN = /^[A-Za-z0-9_./\\:+=,-]+$/;
126
+ const LIFECYCLE_ARGUMENT_POLICIES = {
127
+ "session:start": {
128
+ booleanFlags: new Set([
129
+ "--json",
130
+ "--no-history",
131
+ "--read-only",
132
+ "--with-network",
133
+ "--rearm",
134
+ "--hard-budget",
135
+ "--compact",
136
+ "--steal",
137
+ "--confirm",
138
+ ]),
139
+ valueFlags: new Set([
140
+ "--tier",
141
+ "--defer",
142
+ "--task-size",
143
+ "--ceremony-task-size",
144
+ "--model-tier",
145
+ "--ceremony-model-tier",
146
+ "--project-shape",
147
+ "--ceremony-project-shape",
148
+ "--ceremony-depth",
149
+ "--max-turns",
150
+ "--max-budget",
151
+ "--session-id",
152
+ "--occupant",
153
+ "--project-root",
154
+ ]),
155
+ rewriteUnsafeFlags: new Set(["--project-root"]),
156
+ },
157
+ "session:ready": {
158
+ booleanFlags: new Set(["--json", "--with-network"]),
159
+ valueFlags: new Set(["--repo", "--session-id", "--project-root"]),
160
+ rewriteUnsafeFlags: new Set(["--project-root"]),
161
+ },
162
+ "session:end": {
163
+ booleanFlags: new Set(),
164
+ valueFlags: new Set(["--session-id", "--project-root"]),
165
+ rewriteUnsafeFlags: new Set(["--project-root"]),
166
+ },
167
+ "occupancy:steal": {
168
+ booleanFlags: new Set(["--confirm"]),
169
+ valueFlags: new Set(["--occupant", "--session-id", "--project-root"]),
170
+ rewriteUnsafeFlags: new Set(["--project-root"]),
171
+ },
172
+ "occupancy:release": {
173
+ booleanFlags: new Set(),
174
+ valueFlags: new Set(["--session-id", "--project-root"]),
175
+ rewriteUnsafeFlags: new Set(["--project-root"]),
176
+ },
177
+ "occupancy:heartbeat": {
178
+ booleanFlags: new Set(),
179
+ valueFlags: new Set(["--session-id", "--project-root"]),
180
+ rewriteUnsafeFlags: new Set(["--project-root"]),
181
+ },
182
+ "swarm:launch": {
183
+ booleanFlags: new Set([
184
+ "--autonomous",
185
+ "--no-create-worktrees",
186
+ "--enforce-gates",
187
+ "--no-audit",
188
+ ]),
189
+ valueFlags: new Set([
190
+ "--stories",
191
+ "--paths",
192
+ "--group",
193
+ "--worktree-map",
194
+ "--base-branch",
195
+ "--allocation-plan-id",
196
+ "--batching-rationale",
197
+ "--operator-approval",
198
+ "--output",
199
+ "--gate-clearances",
200
+ "--project-root",
201
+ "--session-id",
202
+ ]),
203
+ rewriteUnsafeFlags: new Set([
204
+ "--paths",
205
+ "--worktree-map",
206
+ "--output",
207
+ "--gate-clearances",
208
+ "--project-root",
209
+ "--no-audit",
210
+ ]),
211
+ separateOnlyValueFlags: new Set([
212
+ "--stories",
213
+ "--paths",
214
+ "--group",
215
+ "--worktree-map",
216
+ "--base-branch",
217
+ "--allocation-plan-id",
218
+ "--batching-rationale",
219
+ "--operator-approval",
220
+ "--output",
221
+ "--gate-clearances",
222
+ "--project-root",
223
+ ]),
224
+ },
225
+ };
226
+ function lifecycleArgumentValueIsRewriteSafe(verb, flag, value) {
227
+ if (verb !== "swarm:launch" || flag !== "--stories")
228
+ return true;
229
+ // `swarm:launch --stories` accepts both logical IDs and filesystem paths.
230
+ // Only the ID form belongs in the auto-approved rewrite bridge; path-bearing
231
+ // invocations must keep the host's ordinary permission decision.
232
+ return value
233
+ .split(",")
234
+ .every((story) => story.length > 0 &&
235
+ !story.endsWith(".json") &&
236
+ !story.includes("/") &&
237
+ !story.includes("\\"));
238
+ }
239
+ /**
240
+ * Validate the complete CLI argument surface before returning an allow+rewrite.
241
+ * Path rebinding/destination flags and unknown/future flags are recognized but
242
+ * kept outside the auto-rewrite bridge. Supported hosts must carry the matching
243
+ * owner explicitly before those commands retain normal permission handling.
244
+ */
245
+ function analyzeLifecycleArguments(verb, args) {
246
+ const policy = LIFECYCLE_ARGUMENT_POLICIES[verb];
247
+ let rewriteSafe = true;
248
+ let readOnly = false;
249
+ for (let index = 0; index < args.length; index += 1) {
250
+ const token = args[index] ?? "";
251
+ if (policy.booleanFlags.has(token)) {
252
+ if (verb === "session:start" && token === "--read-only")
253
+ readOnly = true;
254
+ if (policy.rewriteUnsafeFlags.has(token))
255
+ rewriteSafe = false;
256
+ continue;
257
+ }
258
+ const equals = token.indexOf("=");
259
+ if (equals > 0) {
260
+ const flag = token.slice(0, equals);
261
+ const value = token.slice(equals + 1);
262
+ if (!policy.valueFlags.has(flag)) {
263
+ rewriteSafe = false;
264
+ continue;
265
+ }
266
+ if (policy.rewriteUnsafeFlags.has(flag))
267
+ rewriteSafe = false;
268
+ if (policy.separateOnlyValueFlags?.has(flag) === true)
269
+ rewriteSafe = false;
270
+ // Keep malformed session identity visible to inspection so the
271
+ // dispatcher can deny ambiguity instead of silently skipping rewrite.
272
+ if (value.length === 0 && flag !== "--session-id")
273
+ rewriteSafe = false;
274
+ if (!lifecycleArgumentValueIsRewriteSafe(verb, flag, value))
275
+ rewriteSafe = false;
276
+ continue;
277
+ }
278
+ if (!policy.valueFlags.has(token)) {
279
+ rewriteSafe = false;
280
+ continue;
281
+ }
282
+ if (policy.rewriteUnsafeFlags.has(token))
283
+ rewriteSafe = false;
284
+ const value = args[index + 1];
285
+ if (value === undefined) {
286
+ if (token !== "--session-id")
287
+ rewriteSafe = false;
288
+ continue;
289
+ }
290
+ if (value.startsWith("--"))
291
+ rewriteSafe = false;
292
+ if (!lifecycleArgumentValueIsRewriteSafe(verb, token, value))
293
+ rewriteSafe = false;
294
+ // Match the real lifecycle parsers: known value flags consume the next
295
+ // argv token even when it begins with `--`. This prevents a swallowed
296
+ // `--session-id=...` from being mistaken for an effective owner.
297
+ index += 1;
298
+ }
299
+ return { rewriteSafe, readOnly };
300
+ }
301
+ /**
302
+ * Inspect single-space-separated lifecycle commands while excluding shell
303
+ * quoting, substitution, redirection, chaining, aliases, and wrappers.
304
+ * Windows path separators remain inspectable but make rewriting unsafe.
305
+ */
306
+ function exactLifecycleInvocation(command) {
307
+ if (command.length === 0 || command !== command.trim())
308
+ return null;
309
+ const tokens = command.split(" ");
310
+ if (tokens.some((token) => token.length === 0 || !INSPECTABLE_TOKEN_PATTERN.test(token))) {
311
+ return null;
312
+ }
313
+ if (tokens.length < 2)
314
+ return null;
315
+ const executable = tokens[0];
316
+ const verb = tokens[1];
317
+ if (executable === "deft" || executable === "directive") {
318
+ const typedVerb = verb === undefined ? undefined : DIRECT_LIFECYCLE_VERBS[verb];
319
+ if (typedVerb === undefined)
320
+ return null;
321
+ const forwardedArgs = tokens.slice(2);
322
+ const { rewriteSafe, readOnly } = analyzeLifecycleArguments(typedVerb, forwardedArgs);
323
+ const requiresOwner = typedVerb !== "session:start" || !readOnly;
324
+ return {
325
+ verb: typedVerb,
326
+ task: false,
327
+ forwardedArgs,
328
+ rewriteSafe: rewriteSafe && forwardedArgs.every((token) => !token.includes("\\")),
329
+ requiresOwner,
330
+ };
331
+ }
332
+ if (executable !== "task")
333
+ return null;
334
+ if (!EXACT_LIFECYCLE_VERBS.includes(verb ?? ""))
335
+ return null;
336
+ const typedVerb = verb;
337
+ if (tokens.length === 2) {
338
+ return {
339
+ verb: typedVerb,
340
+ task: true,
341
+ forwardedArgs: [],
342
+ rewriteSafe: true,
343
+ requiresOwner: true,
344
+ };
345
+ }
346
+ // Go Task's canonical CLI_ARGS boundary. Flags without `--` are ambiguous
347
+ // Task CLI flags and must not receive an auto-approving rewrite.
348
+ if (tokens[2] !== "--")
349
+ return null;
350
+ const forwardedArgs = tokens.slice(3);
351
+ const { rewriteSafe, readOnly } = analyzeLifecycleArguments(typedVerb, forwardedArgs);
352
+ const requiresOwner = typedVerb !== "session:start" || !readOnly;
353
+ return {
354
+ verb: typedVerb,
355
+ task: true,
356
+ forwardedArgs,
357
+ rewriteSafe: rewriteSafe && forwardedArgs.every((token) => !token.includes("\\")),
358
+ requiresOwner,
359
+ };
360
+ }
361
+ function exactLifecyclePayload(payload) {
362
+ const toolName = hookToolName(payload);
363
+ if (toolName === null || !isShellTool(toolName)) {
364
+ return null;
365
+ }
366
+ const input = record(payload);
367
+ if (input === null)
368
+ return null;
369
+ const toolInput = toolInputRecord(input);
370
+ if (toolInput === null || typeof toolInput.command !== "string")
371
+ return null;
372
+ const originalCommand = toolInput.command;
373
+ const invocation = exactLifecycleInvocation(originalCommand);
374
+ return invocation === null ? null : { invocation, originalCommand, toolInput };
375
+ }
376
+ /** Logical lifecycle verb for one exact, simple shell command; otherwise null. */
377
+ export function exactLifecycleCommandVerb(payload) {
378
+ return exactLifecyclePayload(payload)?.invocation.verb ?? null;
379
+ }
380
+ function sessionIdArgs(verb, args) {
381
+ const policy = LIFECYCLE_ARGUMENT_POLICIES[verb];
382
+ const values = [];
383
+ for (let i = 0; i < args.length; i += 1) {
384
+ const token = args[i] ?? "";
385
+ if (token.startsWith("--session-id=")) {
386
+ const value = token.slice("--session-id=".length);
387
+ if (value.length === 0)
388
+ return { status: "invalid", values: [] };
389
+ values.push(value);
390
+ continue;
391
+ }
392
+ if (token === "--session-id") {
393
+ const value = args[i + 1];
394
+ if (value === undefined ||
395
+ value.length === 0 ||
396
+ (verb === "swarm:launch" && value.startsWith("--"))) {
397
+ return { status: "invalid", values: [] };
398
+ }
399
+ values.push(value);
400
+ i += 1;
401
+ continue;
402
+ }
403
+ if (token.indexOf("=") < 0 && policy.valueFlags.has(token) && args[i + 1] !== undefined)
404
+ i += 1;
405
+ }
406
+ if (values.length === 0)
407
+ return { status: "absent", values };
408
+ if (values.length !== 1)
409
+ return { status: "invalid", values };
410
+ return { status: "present", values };
411
+ }
412
+ /** Inspect one exact lifecycle command without requiring a host identity. */
413
+ export function inspectExactLifecycleCommand(payload) {
414
+ const exact = exactLifecyclePayload(payload);
415
+ if (exact === null)
416
+ return null;
417
+ const session = sessionIdArgs(exact.invocation.verb, exact.invocation.forwardedArgs);
418
+ return {
419
+ verb: exact.invocation.verb,
420
+ task: exact.invocation.task,
421
+ rewriteSafe: exact.invocation.rewriteSafe,
422
+ requiresOwner: exact.invocation.requiresOwner,
423
+ sessionIdStatus: session.status,
424
+ sessionId: session.status === "present" ? (session.values[0] ?? null) : null,
425
+ };
426
+ }
427
+ /**
428
+ * Append a canonical host owner to one exact Directive lifecycle command.
429
+ *
430
+ * The returned `updatedInput` is a complete shallow clone of the original
431
+ * host tool input. `null` means no safe rewrite. An explicit foreign identity
432
+ * is surfaced as a conflict so dispatcher wiring can deny instead of replacing
433
+ * operator input.
434
+ */
435
+ export function rewriteExactLifecycleCommand(payload, sessionId) {
436
+ if (!CANONICAL_OWNER_PATTERN.test(sessionId))
437
+ return null;
438
+ const exact = exactLifecyclePayload(payload);
439
+ if (exact === null)
440
+ return null;
441
+ const { invocation, originalCommand, toolInput } = exact;
442
+ if (!invocation.rewriteSafe || !invocation.requiresOwner)
443
+ return null;
444
+ const existing = sessionIdArgs(invocation.verb, invocation.forwardedArgs);
445
+ if (existing.status === "invalid")
446
+ return null;
447
+ if (existing.status === "present") {
448
+ const existingSessionId = existing.values[0] ?? "";
449
+ if (existingSessionId === sessionId)
450
+ return null;
451
+ return {
452
+ kind: "conflict",
453
+ verb: invocation.verb,
454
+ existingSessionId,
455
+ requestedSessionId: sessionId,
456
+ message: `Lifecycle command ${invocation.verb} already names a different ` +
457
+ "--session-id; refusing rewrite.",
458
+ };
459
+ }
460
+ const taskForwarding = invocation.task && originalCommand.split(" ").length === 2 ? " --" : "";
461
+ const rewrittenCommand = `${originalCommand}${taskForwarding} --session-id=${sessionId}`;
462
+ return {
463
+ kind: "rewrite",
464
+ verb: invocation.verb,
465
+ originalCommand,
466
+ rewrittenCommand,
467
+ updatedInput: { ...toolInput, command: rewrittenCommand },
468
+ };
469
+ }
470
+ //# sourceMappingURL=host-session-identity.js.map
@@ -5,9 +5,10 @@
5
5
  * Policy / ritual / scope / permission emission stay in dispatcher + CLI adapter.
6
6
  */
7
7
  export { classifyHookPayload } from "./classify.js";
8
- export { hookMcpArgsText, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
8
+ export { EXACT_LIFECYCLE_VERBS, type ExactLifecycleCommandConflict, type ExactLifecycleCommandInspection, type ExactLifecycleCommandResult, type ExactLifecycleCommandRewrite, type ExactLifecycleVerb, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentityResolution, type HookHostIdentityStatus, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
9
+ export { hookApplyPatchBodyPaths, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
9
10
  export { fieldPresent, fieldString, firstString, hookPayloadTopLevelKeys, record, toolInputRecord, } from "./payload.js";
10
- export { parseHookStdin, stripUtf8Bom } from "./stdin.js";
11
+ export { applyPatchMutationPaths, parseHookStdin, stripUtf8Bom } from "./stdin.js";
11
12
  export { hookToolName, inferCursorDirectWriteToolName, type MissingToolNameInput, missingToolNameMessage, } from "./tool-name.js";
12
13
  export type { ClassifyHookHost, ClassifyHookPayloadInput, HookClassification, HookPayloadContext, HookWriteIntent, ParsedHookPayload, } from "./types.js";
13
14
  export { CLASSIFY_HOOK_HOSTS } from "./types.js";