@deftai/directive-core 0.108.0 → 0.109.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 (138) hide show
  1. package/dist/cache/operations.js +1 -1
  2. package/dist/check/gate-lists.d.ts +14 -0
  3. package/dist/check/gate-lists.js +26 -3
  4. package/dist/check/session-completed-ac.d.ts +1 -1
  5. package/dist/check/session-completed-ac.js +1 -1
  6. package/dist/consumer-check-contract/evaluate.d.ts +47 -0
  7. package/dist/consumer-check-contract/evaluate.js +181 -15
  8. package/dist/delivery-attempt/evaluate.d.ts +9 -1
  9. package/dist/delivery-attempt/evaluate.js +69 -0
  10. package/dist/delivery-attempt/index.d.ts +1 -1
  11. package/dist/delivery-attempt/index.js +1 -1
  12. package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
  13. package/dist/deposit/live-procedure-exclusions.js +110 -0
  14. package/dist/deposit/live-procedure-targets.d.ts +45 -0
  15. package/dist/deposit/live-procedure-targets.js +274 -0
  16. package/dist/deposit/python-free.d.ts +6 -0
  17. package/dist/deposit/python-free.js +15 -0
  18. package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
  19. package/dist/deposit/rewrite-deposit-links.js +148 -0
  20. package/dist/deposit/run-stage-content-pack.d.ts +2 -0
  21. package/dist/deposit/run-stage-content-pack.js +3 -0
  22. package/dist/deposit/stage-content-pack.d.ts +17 -0
  23. package/dist/deposit/stage-content-pack.js +91 -0
  24. package/dist/design-critique/citation-grammar.d.ts +7 -0
  25. package/dist/design-critique/citation-grammar.js +1 -1
  26. package/dist/design-critique/completed-arc-record.d.ts +1 -1
  27. package/dist/design-critique/completed-arc-record.js +59 -14
  28. package/dist/hooks/classify/host-session-identity.d.ts +24 -11
  29. package/dist/hooks/classify/host-session-identity.js +46 -49
  30. package/dist/hooks/classify/index.d.ts +2 -2
  31. package/dist/hooks/classify/index.js +2 -2
  32. package/dist/hooks/classify/paths.d.ts +2 -0
  33. package/dist/hooks/classify/paths.js +8 -4
  34. package/dist/hooks/classify/stdin.js +69 -3
  35. package/dist/hooks/dest-form.d.ts +20 -1
  36. package/dist/hooks/dest-form.js +158 -21
  37. package/dist/hooks/dispatcher.d.ts +6 -2
  38. package/dist/hooks/dispatcher.js +258 -62
  39. package/dist/hooks/fixtures/cases.d.ts +6 -0
  40. package/dist/hooks/fixtures/cases.js +53 -0
  41. package/dist/hooks/git-destructive-log.d.ts +32 -0
  42. package/dist/hooks/git-destructive-log.js +46 -0
  43. package/dist/hooks/index.d.ts +2 -0
  44. package/dist/hooks/index.js +2 -0
  45. package/dist/hooks/shell-write-targets.d.ts +10 -0
  46. package/dist/hooks/shell-write-targets.js +274 -0
  47. package/dist/hooks/tools.d.ts +20 -2
  48. package/dist/hooks/tools.js +30 -1
  49. package/dist/init-deposit/gitignore.d.ts +7 -0
  50. package/dist/init-deposit/gitignore.js +24 -0
  51. package/dist/init-deposit/init-deposit.js +3 -0
  52. package/dist/init-deposit/refresh.js +7 -0
  53. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  54. package/dist/init-deposit/runtime-writers.js +33 -0
  55. package/dist/intake/issue-ingest.d.ts +6 -1
  56. package/dist/intake/issue-ingest.js +21 -2
  57. package/dist/intake/reconcile-issues.js +13 -13
  58. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  59. package/dist/lifecycle/brief-envelope.js +42 -0
  60. package/dist/lifecycle/index.d.ts +1 -0
  61. package/dist/lifecycle/index.js +1 -0
  62. package/dist/literal-acceptance/evaluate.js +14 -5
  63. package/dist/literal-acceptance/index.d.ts +1 -1
  64. package/dist/literal-acceptance/index.js +1 -1
  65. package/dist/literal-acceptance/run.d.ts +2 -0
  66. package/dist/literal-acceptance/run.js +19 -1
  67. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  68. package/dist/orphan-active/candidate-scope.js +157 -0
  69. package/dist/orphan-active/evaluate.d.ts +27 -0
  70. package/dist/orphan-active/evaluate.js +63 -6
  71. package/dist/orphan-active/index.d.ts +1 -0
  72. package/dist/orphan-active/index.js +1 -0
  73. package/dist/policy/merge-approval-head.js +7 -6
  74. package/dist/pr-closing-keywords/gh.js +32 -9
  75. package/dist/pr-closing-keywords/main.d.ts +1 -0
  76. package/dist/pr-closing-keywords/main.js +68 -3
  77. package/dist/pr-closing-keywords/types.d.ts +2 -0
  78. package/dist/pr-merge-readiness/gh.js +32 -9
  79. package/dist/pr-protected-issues/gh.js +36 -9
  80. package/dist/pr-wait-mergeable/wrappers.js +3 -3
  81. package/dist/product-first-done-gate/evaluate.js +6 -2
  82. package/dist/product-first-done-gate/types.js +2 -0
  83. package/dist/release/consumer-hard-stops.d.ts +48 -0
  84. package/dist/release/consumer-hard-stops.js +140 -0
  85. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  86. package/dist/release/consumer-readiness-disclosure.js +51 -0
  87. package/dist/release/index.d.ts +2 -0
  88. package/dist/release/index.js +2 -0
  89. package/dist/release/pipeline.js +55 -0
  90. package/dist/release/run-consumer-readiness.d.ts +15 -0
  91. package/dist/release/run-consumer-readiness.js +24 -0
  92. package/dist/release/types.d.ts +6 -0
  93. package/dist/review-monitor/github-lease.js +4 -3
  94. package/dist/run-summary/types.d.ts +2 -2
  95. package/dist/scm/build-command.d.ts +2 -2
  96. package/dist/scm/build-command.js +2 -2
  97. package/dist/scm/call-shape.d.ts +25 -0
  98. package/dist/scm/call-shape.js +59 -0
  99. package/dist/scm/call.d.ts +7 -4
  100. package/dist/scm/call.js +41 -11
  101. package/dist/scm/gh-rest.d.ts +25 -12
  102. package/dist/scm/gh-rest.js +65 -15
  103. package/dist/scm/index.d.ts +2 -0
  104. package/dist/scm/index.js +2 -0
  105. package/dist/scm/spawn-status.d.ts +33 -0
  106. package/dist/scm/spawn-status.js +53 -0
  107. package/dist/scope/acceptance-evidence.d.ts +1 -1
  108. package/dist/scope/transition.js +4 -14
  109. package/dist/session/ac-pass-banking.d.ts +2 -2
  110. package/dist/session/ac-pass-banking.js +2 -2
  111. package/dist/session/host-session-owner.d.ts +53 -0
  112. package/dist/session/host-session-owner.js +89 -0
  113. package/dist/session/index.d.ts +1 -0
  114. package/dist/session/index.js +1 -0
  115. package/dist/session/occupancy.d.ts +21 -1
  116. package/dist/session/occupancy.js +70 -12
  117. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  118. package/dist/session/verify-ac-session-cache.js +2 -2
  119. package/dist/triage/evaluate/worktrees.js +153 -6
  120. package/dist/umbrella-current-shape/index.d.ts +51 -3
  121. package/dist/umbrella-current-shape/index.js +106 -18
  122. package/dist/validate-content/deposit-required.d.ts +39 -0
  123. package/dist/validate-content/deposit-required.js +147 -0
  124. package/dist/validate-content/index.d.ts +1 -0
  125. package/dist/validate-content/index.js +1 -0
  126. package/dist/validate-content/validate-links.d.ts +2 -3
  127. package/dist/validate-content/validate-links.js +29 -2
  128. package/dist/vbrief-activate/activate.d.ts +7 -2
  129. package/dist/vbrief-activate/activate.js +26 -13
  130. package/dist/verify-ac/evaluate.d.ts +9 -0
  131. package/dist/verify-ac/evaluate.js +32 -9
  132. package/dist/verify-source/deposit-closure.d.ts +23 -0
  133. package/dist/verify-source/deposit-closure.js +162 -0
  134. package/dist/verify-source/index.d.ts +2 -0
  135. package/dist/verify-source/index.js +2 -0
  136. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  137. package/dist/verify-source/semantic-single-source.js +349 -0
  138. package/package.json +3 -3
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Stage the @deftai/directive-content pack the same way prepack does, then
3
+ * rewrite relative markdown links onto the flattened deposit layout (#3937).
4
+ *
5
+ * Must not write into the source tree: destDir is the pack root (packages/content
6
+ * during prepack, or an out-of-tree temp dir in tests).
7
+ */
8
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+ import { containedWrite } from "../fs/contained-write.js";
11
+ import { rewriteMarkdownDepositLinks, sourceRelForPackRel } from "./rewrite-deposit-links.js";
12
+ const ENGINE_ENTRIES = [".githooks", "Taskfile.yml", "tasks"];
13
+ const HARNESS_ENTRIES = ["main.md", "SKILL.md"];
14
+ function keepCopy(src) {
15
+ return !src.includes("__pycache__") && !src.endsWith(".pyc") && !src.endsWith(".py");
16
+ }
17
+ function rewriteStagedMarkdown(destDir) {
18
+ let filesRewritten = 0;
19
+ const walk = (dir) => {
20
+ let entries;
21
+ try {
22
+ entries = readdirSync(dir, { withFileTypes: true });
23
+ }
24
+ catch {
25
+ return;
26
+ }
27
+ for (const entry of entries) {
28
+ const full = join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ walk(full);
31
+ continue;
32
+ }
33
+ if (!entry.isFile() || !entry.name.endsWith(".md"))
34
+ continue;
35
+ const packRel = relative(destDir, full).replace(/\\/g, "/");
36
+ const original = readFileSync(full, "utf8");
37
+ const { content, rewriteCount } = rewriteMarkdownDepositLinks({
38
+ content: original,
39
+ sourceFileRel: sourceRelForPackRel(packRel),
40
+ packFileRel: packRel,
41
+ });
42
+ if (rewriteCount > 0 && content !== original) {
43
+ containedWrite({
44
+ root: destDir,
45
+ target: packRel,
46
+ data: content,
47
+ mode: "replace",
48
+ });
49
+ filesRewritten += 1;
50
+ }
51
+ }
52
+ };
53
+ walk(destDir);
54
+ return filesRewritten;
55
+ }
56
+ /** Copy content/ + harness + engine entries, then rewrite markdown links. */
57
+ export function stageContentPack(options) {
58
+ const { repoRoot, destDir } = options;
59
+ mkdirSync(destDir, { recursive: true });
60
+ const contentSrc = join(repoRoot, "content");
61
+ for (const name of readdirSync(contentSrc)) {
62
+ const from = join(contentSrc, name);
63
+ const to = join(destDir, name);
64
+ if (existsSync(to))
65
+ rmSync(to, { recursive: true, force: true });
66
+ cpSync(from, to, { recursive: true, filter: (src) => keepCopy(src) });
67
+ }
68
+ for (const name of ENGINE_ENTRIES) {
69
+ const from = join(repoRoot, name);
70
+ if (!existsSync(from))
71
+ continue;
72
+ const to = join(destDir, name);
73
+ if (existsSync(to))
74
+ rmSync(to, { recursive: true, force: true });
75
+ cpSync(from, to, { recursive: true, filter: (src) => keepCopy(src) });
76
+ }
77
+ for (const name of HARNESS_ENTRIES) {
78
+ const from = join(repoRoot, name);
79
+ if (!existsSync(from))
80
+ continue;
81
+ cpSync(from, join(destDir, name));
82
+ }
83
+ const filesRewritten = rewriteStagedMarkdown(destDir);
84
+ return { filesRewritten };
85
+ }
86
+ /** prepack cwd is packages/content. */
87
+ export function runPrepackFromContentPackage(pkgDir) {
88
+ const repoRoot = join(pkgDir, "..", "..");
89
+ stageContentPack({ repoRoot, destDir: pkgDir });
90
+ }
91
+ //# sourceMappingURL=stage-content-pack.js.map
@@ -44,6 +44,13 @@ export type CitationScan = {
44
44
  };
45
45
  /** The published closed set, in the order the contract lists it. */
46
46
  export declare const ACCEPTED_CITATION_FORMS: readonly string[];
47
+ /**
48
+ * Refused positions are exactly the five published classes. An indented code
49
+ * block and an HTML comment are deliberately not classified: a four-space
50
+ * indent is also ordinary list-continuation content, so refusing it would block
51
+ * valid records more often than it would catch example text.
52
+ */
53
+ export declare function classifyPosition(body: string, offset: number): CitationRejectionClass | null;
47
54
  /**
48
55
  * Parse the closed citation grammar out of one comment body. Refused positions
49
56
  * are reported rather than dropped so a block detail can echo the observation
@@ -239,7 +239,7 @@ function blockPosition(body, offset) {
239
239
  * indent is also ordinary list-continuation content, so refusing it would block
240
240
  * valid records more often than it would catch example text.
241
241
  */
242
- function classifyPosition(body, offset) {
242
+ export function classifyPosition(body, offset) {
243
243
  const block = blockPosition(body, offset);
244
244
  if (block.insideFence)
245
245
  return "code-fence";
@@ -15,7 +15,7 @@ export type ThreadComment = {
15
15
  readonly id: number;
16
16
  readonly body: string;
17
17
  };
18
- export type CompletedArcBlockReason = "missing-record" | "lone-shape" | "cite-not-lean" | "missing-table-cite";
18
+ export type CompletedArcBlockReason = "missing-record" | "lone-shape" | "cite-not-lean" | "missing-table-cite" | "ambiguous-table-cite";
19
19
  export type CompletedArcVerdict = {
20
20
  readonly status: "not-in-arc";
21
21
  } | {
@@ -126,6 +126,61 @@ function resolveCitedLean(citations, byCommentId, latestLean) {
126
126
  .map((row) => byCommentId.get(row.id))
127
127
  .find((row) => row !== undefined && isSuccessorLeanBody(row.body));
128
128
  }
129
+ /**
130
+ * Table resolution precedence (#3932).
131
+ *
132
+ * A typed `verified-claims table <id>` citation is the synthesis naming its own
133
+ * table, so it decides resolution: a generic citation neither satisfies it nor
134
+ * masks it. Scanning every citation for the first table-shaped body -- what
135
+ * this did before -- let an unrelated table-shaped comment clear a record whose
136
+ * claimed table is not a table, because the `missing-table-cite` refusal ran
137
+ * only after that untyped search failed.
138
+ *
139
+ * Cardinality rule: every table-kind claim must resolve to the same
140
+ * verified-claims table artifact. One valid typed claim must not launder an
141
+ * invalid one -- filtering to typed citations and then taking the first
142
+ * table-shaped body finds the real table in a valid-plus-ghost pair and never
143
+ * examines the ghost. `scanCitations` deduplicates on `(id, kind)`, so naming
144
+ * one table id twice is a single claim, while two distinct typed ids are two
145
+ * claims and cannot both be this record's table.
146
+ *
147
+ * With no typed claim the generic scan stands unchanged: permalink and bare
148
+ * `comment` citations scan as kind `comment`, and that is the published form
149
+ * these records use. Narrowing the citation contract so a table must be named
150
+ * by keyword is a separate decision needing its own migration criteria.
151
+ */
152
+ function resolveCitedTable(citations, byCommentId) {
153
+ const claimed = citations.filter((row) => row.kind === "table");
154
+ if (claimed.length === 0) {
155
+ return {
156
+ ok: true,
157
+ table: citations
158
+ .map((row) => byCommentId.get(row.id))
159
+ .find((row) => row !== undefined && isVerifiedClaimsTableBody(row.body)),
160
+ };
161
+ }
162
+ const resolved = claimed.map((row) => ({ id: row.id, cited: byCommentId.get(row.id) }));
163
+ const unresolved = resolved.filter((row) => row.cited === undefined || !isVerifiedClaimsTableBody(row.cited.body));
164
+ if (unresolved.length > 0) {
165
+ return {
166
+ ok: false,
167
+ reason: "missing-table-cite",
168
+ detail: "synthesis cites a verified-claims table id that is not a table on this thread: " +
169
+ renderIds(unresolved.map((row) => row.id)),
170
+ };
171
+ }
172
+ const distinct = [...new Set(claimed.map((row) => row.id))];
173
+ if (distinct.length > 1) {
174
+ return {
175
+ ok: false,
176
+ reason: "ambiguous-table-cite",
177
+ detail: "synthesis claims more than one verified-claims table; every table citation must name " +
178
+ "the same table: " +
179
+ renderIds(distinct),
180
+ };
181
+ }
182
+ return { ok: true, table: resolved.find((row) => row.cited !== undefined)?.cited };
183
+ }
129
184
  function verdictForSynthesis(comment, comments) {
130
185
  const scan = scanCitations(comment.body);
131
186
  const citations = scan.citations;
@@ -146,25 +201,15 @@ function verdictForSynthesis(comment, comments) {
146
201
  renderIds(citations.map((row) => row.id)),
147
202
  };
148
203
  }
149
- const citedTable = citations
150
- .map((row) => byCommentId.get(row.id))
151
- .find((row) => row !== undefined && isVerifiedClaimsTableBody(row.body));
152
- if (citedTable === undefined) {
153
- const claimed = citations.filter((row) => row.kind === "table");
154
- if (claimed.length > 0) {
155
- return {
156
- status: "blocked",
157
- reason: "missing-table-cite",
158
- detail: "synthesis cites a verified-claims table id that is not a table on this thread: " +
159
- renderIds(claimed.map((row) => row.id)),
160
- };
161
- }
204
+ const citedTable = resolveCitedTable(citations, byCommentId);
205
+ if (!citedTable.ok) {
206
+ return { status: "blocked", reason: citedTable.reason, detail: citedTable.detail };
162
207
  }
163
208
  return {
164
209
  status: "complete",
165
210
  synthesisCommentId: comment.id,
166
211
  citedLeanId: citedLean.id,
167
- citedTableId: citedTable?.id ?? null,
212
+ citedTableId: citedTable.table?.id ?? null,
168
213
  };
169
214
  }
170
215
  /**
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Cooperative host-session identity and exact lifecycle-command rewriting (#3611).
3
3
  *
4
- * This is payload classification, not authentication. Host hook JSON is locally
5
- * forgeable under the occupancy model's cooperating-process assumption.
4
+ * This is payload/environment classification, not authentication. Host hook JSON
5
+ * is locally forgeable under the occupancy model's cooperating-process
6
+ * assumption. The provider table and the canonical owner form live in
7
+ * `session/host-session-owner.ts` because the CLI claim path resolves the same
8
+ * owner from the same host (#3873).
6
9
  */
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
+ import { ambientHostSessionOwner, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentitySource, hookHostIdentitySource, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES } from "../../session/host-session-owner.js";
11
+ export { ambientHostSessionOwner, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentitySource, hookHostIdentitySource, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, };
10
12
  export type HookHostIdentityStatus = "ok" | "missing" | "invalid" | "conflict" | "unsupported";
11
13
  export type HookHostIdentityResolution = {
12
14
  readonly status: "ok";
@@ -24,12 +26,24 @@ export type HookHostIdentityResolution = {
24
26
  /**
25
27
  * Resolve the stable conversation/session-family key documented by each host.
26
28
  *
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.
29
+ * - Codex: payload `session_id` (parent and subagents share it).
30
+ * - Claude Code: payload `session_id` (`agent_id` is not the owner key).
31
+ * - Cursor: payload `conversation_id`; simultaneous `session_id` must agree.
32
+ * - Grok: hook process `GROK_SESSION_ID` (#3873). The payload `session_id` is
33
+ * deliberately not read -- that contract is unverified.
34
+ * - Unknown hosts: no guessed identity; callers keep the explicit/env flow.
31
35
  */
32
- export declare function resolveHookHostIdentity(host: string, payload: unknown): HookHostIdentityResolution;
36
+ export declare function resolveHookHostIdentity(host: string, payload: unknown, environ?: NodeJS.ProcessEnv): HookHostIdentityResolution;
37
+ /**
38
+ * True when a `host-env` provider simply did not publish its variable (#3873).
39
+ *
40
+ * Absence is the pre-#3873 state rather than a broken contract, so callers keep
41
+ * the documented explicit `--session-id` / `DEFT_SESSION_ID` flow instead of
42
+ * failing a host that never had a hook identity to begin with. It is never an
43
+ * admission: with no explicit owner the actor stays unset and the write gate
44
+ * still denies. A present-but-malformed variable is `invalid` and fails closed.
45
+ */
46
+ export declare function hostIdentityFallsBackToExplicitOwner(host: string, resolution: HookHostIdentityResolution): boolean;
33
47
  export declare const EXACT_LIFECYCLE_VERBS: readonly ["session:start", "session:ready", "session:end", "occupancy:steal", "occupancy:release", "occupancy:heartbeat", "swarm:launch"];
34
48
  export type ExactLifecycleVerb = (typeof EXACT_LIFECYCLE_VERBS)[number];
35
49
  export interface ExactLifecycleCommandRewrite {
@@ -72,5 +86,4 @@ export declare function inspectExactLifecycleCommand(payload: unknown): ExactLif
72
86
  * operator input.
73
87
  */
74
88
  export declare function rewriteExactLifecycleCommand(payload: unknown, sessionId: string): ExactLifecycleCommandResult;
75
- export {};
76
89
  //# sourceMappingURL=host-session-identity.d.ts.map
@@ -1,47 +1,22 @@
1
1
  /**
2
2
  * Cooperative host-session identity and exact lifecycle-command rewriting (#3611).
3
3
  *
4
- * This is payload classification, not authentication. Host hook JSON is locally
5
- * forgeable under the occupancy model's cooperating-process assumption.
4
+ * This is payload/environment classification, not authentication. Host hook JSON
5
+ * is locally forgeable under the occupancy model's cooperating-process
6
+ * assumption. The provider table and the canonical owner form live in
7
+ * `session/host-session-owner.ts` because the CLI claim path resolves the same
8
+ * owner from the same host (#3873).
6
9
  */
10
+ import { ambientHostSessionOwner, canonicalHostSessionId, HOST_IDENTITY_PROVIDERS, hookHostIdentitySource, isUsableHostSessionId, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, readHostEnvIdentity, } from "../../session/host-session-owner.js";
7
11
  import { isShellTool } from "../tools.js";
8
12
  import { record, toolInputRecord } from "./payload.js";
9
13
  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
- }
14
+ export { ambientHostSessionOwner, HOST_IDENTITY_PROVIDERS, hookHostIdentitySource, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, };
35
15
  function identityField(input, fieldName) {
36
16
  if (!(fieldName in input))
37
17
  return { status: "missing", value: null };
38
18
  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) {
19
+ if (typeof raw !== "string" || !isUsableHostSessionId(raw)) {
45
20
  return { status: "invalid", value: null };
46
21
  }
47
22
  return { status: "ok", value: raw };
@@ -49,10 +24,6 @@ function identityField(input, fieldName) {
49
24
  function unresolvedHostIdentity(status, provider, message) {
50
25
  return { status, provider, rawSessionId: null, sessionId: null, message };
51
26
  }
52
- function canonicalHostSessionId(provider, rawSessionId) {
53
- const encoded = Buffer.from(rawSessionId, "utf8").toString("base64url");
54
- return `host:${provider}:v1:${encoded}`;
55
- }
56
27
  function resolvedHostIdentity(provider, rawSessionId) {
57
28
  return {
58
29
  status: "ok",
@@ -65,28 +36,39 @@ function resolvedHostIdentity(provider, rawSessionId) {
65
36
  /**
66
37
  * Resolve the stable conversation/session-family key documented by each host.
67
38
  *
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.
39
+ * - Codex: payload `session_id` (parent and subagents share it).
40
+ * - Claude Code: payload `session_id` (`agent_id` is not the owner key).
41
+ * - Cursor: payload `conversation_id`; simultaneous `session_id` must agree.
42
+ * - Grok: hook process `GROK_SESSION_ID` (#3873). The payload `session_id` is
43
+ * deliberately not read -- that contract is unverified.
44
+ * - Unknown hosts: no guessed identity; callers keep the explicit/env flow.
72
45
  */
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.`);
46
+ export function resolveHookHostIdentity(host, payload, environ = process.env) {
47
+ const source = hookHostIdentitySource(host);
48
+ if (source === null) {
49
+ return unresolvedHostIdentity("unsupported", host, `Host ${host || "<empty>"} has no verified session identity contract.`);
76
50
  }
77
51
  const provider = host;
52
+ if (source.kind === "host-env") {
53
+ const value = readHostEnvIdentity(environ, source.variable);
54
+ if (value.status !== "ok") {
55
+ return unresolvedHostIdentity(value.status, provider, `${provider} hook process environment ` +
56
+ `${value.status === "missing" ? "omits" : "has invalid"} ${source.variable}.`);
57
+ }
58
+ return resolvedHostIdentity(provider, value.rawSessionId);
59
+ }
78
60
  const input = record(payload);
79
61
  if (input === null) {
80
62
  return unresolvedHostIdentity("invalid", provider, `${provider} hook payload is not an object.`);
81
63
  }
82
- if (provider === "codex" || provider === "claude") {
83
- const session = identityField(input, "session_id");
64
+ if (provider !== "cursor") {
65
+ const session = identityField(input, source.field);
84
66
  if (session.status !== "ok") {
85
- return unresolvedHostIdentity(session.status, provider, `${provider} hook payload ${session.status === "missing" ? "omits" : "has invalid"} session_id.`);
67
+ return unresolvedHostIdentity(session.status, provider, `${provider} hook payload ${session.status === "missing" ? "omits" : "has invalid"} ${source.field}.`);
86
68
  }
87
69
  return resolvedHostIdentity(provider, session.value);
88
70
  }
89
- const conversation = identityField(input, "conversation_id");
71
+ const conversation = identityField(input, source.field);
90
72
  if (conversation.status !== "ok") {
91
73
  return unresolvedHostIdentity(conversation.status, provider, `Cursor hook payload ${conversation.status === "missing" ? "omits" : "has invalid"} conversation_id.`);
92
74
  }
@@ -99,6 +81,18 @@ export function resolveHookHostIdentity(host, payload) {
99
81
  }
100
82
  return resolvedHostIdentity(provider, conversation.value);
101
83
  }
84
+ /**
85
+ * True when a `host-env` provider simply did not publish its variable (#3873).
86
+ *
87
+ * Absence is the pre-#3873 state rather than a broken contract, so callers keep
88
+ * the documented explicit `--session-id` / `DEFT_SESSION_ID` flow instead of
89
+ * failing a host that never had a hook identity to begin with. It is never an
90
+ * admission: with no explicit owner the actor stays unset and the write gate
91
+ * still denies. A present-but-malformed variable is `invalid` and fails closed.
92
+ */
93
+ export function hostIdentityFallsBackToExplicitOwner(host, resolution) {
94
+ return resolution.status === "missing" && hookHostIdentitySource(host)?.kind === "host-env";
95
+ }
102
96
  export const EXACT_LIFECYCLE_VERBS = [
103
97
  "session:start",
104
98
  "session:ready",
@@ -117,7 +111,10 @@ const DIRECT_LIFECYCLE_VERBS = {
117
111
  "occupancy:heartbeat": "occupancy:heartbeat",
118
112
  "swarm-launch": "swarm:launch",
119
113
  };
120
- const CANONICAL_OWNER_PATTERN = /^host:(?:codex|claude|cursor):v1:[A-Za-z0-9_-]+$/;
114
+ // Derived from the provider list so the rewrite surface cannot drift from the
115
+ // identity surface: a provider added to one is added to both (#3873). Provider
116
+ // ids are lowercase ASCII words, so the alternation needs no escaping.
117
+ const CANONICAL_OWNER_PATTERN = new RegExp(`^host:(?:${HOST_IDENTITY_PROVIDERS.join("|")}):v1:[A-Za-z0-9_-]+$`);
121
118
  // Shell expansion markers are deliberately absent: `$`/backticks for POSIX,
122
119
  // `@` splatting for PowerShell, and `%NAME%` expansion for command shells.
123
120
  // Backslashes are inspectable so Windows path-bearing lifecycle commands fail
@@ -5,8 +5,8 @@
5
5
  * Policy / ritual / scope / permission emission stay in dispatcher + CLI adapter.
6
6
  */
7
7
  export { classifyHookPayload } from "./classify.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";
8
+ export { EXACT_LIFECYCLE_VERBS, type ExactLifecycleCommandConflict, type ExactLifecycleCommandInspection, type ExactLifecycleCommandResult, type ExactLifecycleCommandRewrite, type ExactLifecycleVerb, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, type HookHostIdentityProvider, type HookHostIdentityResolution, type HookHostIdentitySource, type HookHostIdentityStatus, hookHostIdentitySource, hostIdentityFallsBackToExplicitOwner, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
9
+ export { hookApplyPatchBodyPaths, hookApplyPatchBodyText, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
10
10
  export { fieldPresent, fieldString, firstString, hookPayloadTopLevelKeys, record, toolInputRecord, } from "./payload.js";
11
11
  export { applyPatchMutationPaths, parseHookStdin, stripUtf8Bom } from "./stdin.js";
12
12
  export { hookToolName, inferCursorDirectWriteToolName, type MissingToolNameInput, missingToolNameMessage, } from "./tool-name.js";
@@ -5,8 +5,8 @@
5
5
  * Policy / ritual / scope / permission emission stay in dispatcher + CLI adapter.
6
6
  */
7
7
  export { classifyHookPayload } from "./classify.js";
8
- export { EXACT_LIFECYCLE_VERBS, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, 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";
8
+ export { EXACT_LIFECYCLE_VERBS, exactLifecycleCommandVerb, HOST_IDENTITY_PROVIDERS, hookHostIdentitySource, hostIdentityFallsBackToExplicitOwner, inspectExactLifecycleCommand, MAX_HOOK_HOST_IDENTITY_UTF8_BYTES, resolveHookHostIdentity, rewriteExactLifecycleCommand, } from "./host-session-identity.js";
9
+ export { hookApplyPatchBodyPaths, hookApplyPatchBodyText, hookMcpArgsText, hookMutationTargetPaths, hookPathSet, hookShellCommand, hookWriteTargetPath, } from "./paths.js";
10
10
  export { fieldPresent, fieldString, firstString, hookPayloadTopLevelKeys, record, toolInputRecord, } from "./payload.js";
11
11
  export { applyPatchMutationPaths, parseHookStdin, stripUtf8Bom } from "./stdin.js";
12
12
  export { hookToolName, inferCursorDirectWriteToolName, missingToolNameMessage, } from "./tool-name.js";
@@ -7,6 +7,8 @@
7
7
  * Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
8
8
  */
9
9
  export declare function hookWriteTargetPath(payload: unknown): string | null;
10
+ /** Raw ApplyPatch body text from patch / unified_diff / diff fields. */
11
+ export declare function hookApplyPatchBodyText(payload: unknown): string | null;
10
12
  /** ApplyPatch body paths from patch / unified_diff / diff fields. */
11
13
  export declare function hookApplyPatchBodyPaths(payload: unknown): string[];
12
14
  /**
@@ -22,13 +22,13 @@ export function hookWriteTargetPath(payload) {
22
22
  input.path,
23
23
  ]);
24
24
  }
25
- /** ApplyPatch body paths from patch / unified_diff / diff fields. */
26
- export function hookApplyPatchBodyPaths(payload) {
25
+ /** Raw ApplyPatch body text from patch / unified_diff / diff fields. */
26
+ export function hookApplyPatchBodyText(payload) {
27
27
  const input = record(payload);
28
28
  if (input === null)
29
- return [];
29
+ return null;
30
30
  const toolInput = toolInputRecord(input);
31
- const patch = firstString([
31
+ return firstString([
32
32
  toolInput?.patch,
33
33
  toolInput?.unified_diff,
34
34
  toolInput?.diff,
@@ -36,6 +36,10 @@ export function hookApplyPatchBodyPaths(payload) {
36
36
  input.unified_diff,
37
37
  input.diff,
38
38
  ]);
39
+ }
40
+ /** ApplyPatch body paths from patch / unified_diff / diff fields. */
41
+ export function hookApplyPatchBodyPaths(payload) {
42
+ const patch = hookApplyPatchBodyText(payload);
39
43
  return patch === null ? [] : applyPatchMutationPaths(patch);
40
44
  }
41
45
  /**
@@ -2,6 +2,7 @@
2
2
  * Pure stdin → payload parse for hook-dispatch (#2734 / #2738 / #2950).
3
3
  * No process I/O — operates on an already-read string.
4
4
  */
5
+ import { firstString, record, toolInputRecord } from "./payload.js";
5
6
  const UTF8_BOM = "\uFEFF";
6
7
  const APPLY_PATCH_BEGIN_MARKER = "*** Begin Patch";
7
8
  /** Single-file Add/Update only — other *** … File: ops must fail closed (#2738 Greptile). */
@@ -9,8 +10,9 @@ const APPLY_PATCH_MUTATION_LINE_RE = /^\*\*\* (Add File|Update File|Delete File|
9
10
  /**
10
11
  * Canonical apply_patch spells a rename as `*** Update File:` followed by
11
12
  * `*** Move to:`. The destination is a mutation target in its own right, so it
12
- * must reach root admission; kept separate from the header regex above so the
13
- * single-mutation #2738 synthesis contract is unchanged (#3794).
13
+ * must reach root admission. Synthesis refuses when applyPatchMutationPaths
14
+ * reports more than one unique path, so a Move-to destination is not dropped
15
+ * from the single-target contract (#3614 / #3794).
14
16
  */
15
17
  const APPLY_PATCH_MOVE_DESTINATION_RE = /^\*\*\* Move to: (.+)$/gm;
16
18
  export function stripUtf8Bom(raw) {
@@ -49,6 +51,10 @@ function trySynthesizeFreeFormApplyPatch(normalized) {
49
51
  }
50
52
  if (mutations.length !== 1)
51
53
  return null;
54
+ // Count headers plus Move-to destinations so a rename cannot collapse to one
55
+ // checked path (#3614 / #3794). Reuses applyPatchMutationPaths; no second parser.
56
+ if (applyPatchMutationPaths(normalized).length !== 1)
57
+ return null;
52
58
  const sole = mutations[0];
53
59
  if (sole === undefined || (sole.op !== "Add File" && sole.op !== "Update File"))
54
60
  return null;
@@ -63,6 +69,65 @@ function trySynthesizeFreeFormApplyPatch(normalized) {
63
69
  context: {},
64
70
  };
65
71
  }
72
+ function declaredWritePathFromParsed(payload) {
73
+ const input = record(payload);
74
+ if (input === null)
75
+ return null;
76
+ const toolInput = toolInputRecord(input);
77
+ return firstString([
78
+ toolInput?.file_path,
79
+ toolInput?.filePath,
80
+ toolInput?.path,
81
+ input.file_path,
82
+ input.filePath,
83
+ input.path,
84
+ ]);
85
+ }
86
+ function applyPatchBodyTextFromParsed(payload) {
87
+ const input = record(payload);
88
+ if (input === null)
89
+ return null;
90
+ const toolInput = toolInputRecord(input);
91
+ return firstString([
92
+ toolInput?.patch,
93
+ toolInput?.unified_diff,
94
+ toolInput?.diff,
95
+ input.patch,
96
+ input.unified_diff,
97
+ input.diff,
98
+ ]);
99
+ }
100
+ function withToolInputPath(parsed, path) {
101
+ const input = record(parsed);
102
+ if (input === null)
103
+ return parsed;
104
+ for (const key of ["tool_input", "toolInput", "input", "arguments"]) {
105
+ const nested = record(input[key]);
106
+ if (nested !== null) {
107
+ return { ...input, [key]: { ...nested, path } };
108
+ }
109
+ }
110
+ return { ...input, tool_input: { path } };
111
+ }
112
+ /**
113
+ * Valid-JSON hosts (Codex) never hit the JSON.parse catch arm, so the free-form
114
+ * extractor was unreached. Call it on the patch body when no path was declared.
115
+ * Does not replace the parsed payload; only fills tool_input.path.
116
+ */
117
+ function attachSynthesizedApplyPatchPath(parsed) {
118
+ if (declaredWritePathFromParsed(parsed) !== null)
119
+ return parsed;
120
+ const patch = applyPatchBodyTextFromParsed(parsed);
121
+ if (patch === null)
122
+ return parsed;
123
+ const synthesized = trySynthesizeFreeFormApplyPatch(patch);
124
+ if (synthesized === null)
125
+ return parsed;
126
+ const synthesizedPath = declaredWritePathFromParsed(synthesized.payload);
127
+ if (synthesizedPath === null)
128
+ return parsed;
129
+ return withToolInputPath(parsed, synthesizedPath);
130
+ }
66
131
  /**
67
132
  * Parse host hook stdin text into a payload + parse context.
68
133
  * Empty → stdinEmpty; invalid JSON without free-form ApplyPatch → parseFailed.
@@ -76,7 +141,8 @@ export function parseHookStdin(raw) {
76
141
  return { payload: {}, context: { stdinEmpty: true } };
77
142
  }
78
143
  try {
79
- return { payload: JSON.parse(normalized), context: {} };
144
+ const parsed = JSON.parse(normalized);
145
+ return { payload: attachSynthesizedApplyPatchPath(parsed), context: {} };
80
146
  }
81
147
  catch {
82
148
  const synthesized = trySynthesizeFreeFormApplyPatch(normalized);
@@ -12,13 +12,27 @@
12
12
  * *forms* instead, and see `content/contracts/path-write-fence.md`.
13
13
  *
14
14
  * Residual known-open (recognition, not resolution): `python -c`,
15
- * `cmd /c copy`, obfuscated `bash -c`.
15
+ * `cmd /c copy`, obfuscated `bash -c`. Tree-wide destructive git
16
+ * (`reset --hard`, `clean -f`, `checkout -f`/`switch -f`, `stash drop`/`clear`)
17
+ * is classified separately by {@link classifyGitDestructive} and is always
18
+ * fail-closed at the dispatcher, independent of `shellDestForms`.
16
19
  *
17
20
  * Commit-time active-scope is out of this slice (#3438), and ⊗ do not weaken
18
21
  * Edit to match Shell. Keep this sentence on ONE line: acceptance clause 4 is
19
22
  * verified by a literal token search, so wrapping it silently fails the gate.
20
23
  */
21
24
  export type ProductDestFormKind = "git-checkout" | "git-restore" | "rm" | "rmdir";
25
+ export type GitDestructiveKind = "git-reset-hard" | "git-clean" | "git-checkout-force" | "git-stash-drop";
26
+ export interface GitDestructiveForm {
27
+ readonly kind: GitDestructiveKind;
28
+ /** Relocating context paths from `-C` / `--git-dir` / `--work-tree` / `GIT_DIR` / `GIT_WORK_TREE`. */
29
+ readonly relocators: readonly string[];
30
+ /**
31
+ * True when the command is compound or the relocator is opaque (`GIT_CONFIG*`,
32
+ * `-c core.workTree`). The dispatcher denies these; it cannot prove a fixture.
33
+ */
34
+ readonly unprovable: boolean;
35
+ }
22
36
  export interface ProductDestForm {
23
37
  readonly kind: ProductDestFormKind;
24
38
  readonly path: string;
@@ -43,4 +57,9 @@ export declare function payloadWithInjectedWriteTarget(payload: unknown, destPat
43
57
  */
44
58
  export declare function destFormHasExpansion(path: string): boolean;
45
59
  export declare function classifyProductDestForms(command: string): ProductDestForm[];
60
+ /**
61
+ * Tree-wide destructive git — always-on at the dispatcher, not behind
62
+ * `shellDestForms`. Empty when the command is not one of those forms.
63
+ */
64
+ export declare function classifyGitDestructive(command: string): GitDestructiveForm[];
46
65
  //# sourceMappingURL=dest-form.d.ts.map