@deftai/directive-core 0.108.0 → 0.109.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) 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 +8 -1
  27. package/dist/design-critique/completed-arc-record.js +98 -14
  28. package/dist/hooks/classify/host-session-identity.d.ts +25 -12
  29. package/dist/hooks/classify/host-session-identity.js +64 -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 +22 -2
  38. package/dist/hooks/dispatcher.js +338 -66
  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 +3 -0
  44. package/dist/hooks/index.js +3 -0
  45. package/dist/hooks/owner-liveness.d.ts +92 -0
  46. package/dist/hooks/owner-liveness.js +103 -0
  47. package/dist/hooks/shell-write-targets.d.ts +10 -0
  48. package/dist/hooks/shell-write-targets.js +274 -0
  49. package/dist/hooks/tools.d.ts +49 -2
  50. package/dist/hooks/tools.js +96 -1
  51. package/dist/init-deposit/agent-hooks.d.ts +10 -0
  52. package/dist/init-deposit/agent-hooks.js +39 -0
  53. package/dist/init-deposit/gitignore.d.ts +7 -0
  54. package/dist/init-deposit/gitignore.js +24 -0
  55. package/dist/init-deposit/host-tool-coverage.d.ts +53 -0
  56. package/dist/init-deposit/host-tool-coverage.js +150 -0
  57. package/dist/init-deposit/index.d.ts +1 -0
  58. package/dist/init-deposit/index.js +1 -0
  59. package/dist/init-deposit/init-deposit.js +3 -0
  60. package/dist/init-deposit/refresh.js +7 -0
  61. package/dist/init-deposit/runtime-writers.d.ts +14 -0
  62. package/dist/init-deposit/runtime-writers.js +33 -0
  63. package/dist/intake/issue-ingest.d.ts +6 -1
  64. package/dist/intake/issue-ingest.js +21 -2
  65. package/dist/intake/reconcile-issues.js +13 -13
  66. package/dist/lifecycle/brief-envelope.d.ts +25 -0
  67. package/dist/lifecycle/brief-envelope.js +42 -0
  68. package/dist/lifecycle/index.d.ts +1 -0
  69. package/dist/lifecycle/index.js +1 -0
  70. package/dist/literal-acceptance/evaluate.js +14 -5
  71. package/dist/literal-acceptance/index.d.ts +1 -1
  72. package/dist/literal-acceptance/index.js +1 -1
  73. package/dist/literal-acceptance/run.d.ts +2 -0
  74. package/dist/literal-acceptance/run.js +19 -1
  75. package/dist/orchestration/subagent-monitor.d.ts +6 -0
  76. package/dist/orchestration/subagent-monitor.js +23 -1
  77. package/dist/orphan-active/candidate-scope.d.ts +53 -0
  78. package/dist/orphan-active/candidate-scope.js +157 -0
  79. package/dist/orphan-active/evaluate.d.ts +27 -0
  80. package/dist/orphan-active/evaluate.js +63 -6
  81. package/dist/orphan-active/index.d.ts +1 -0
  82. package/dist/orphan-active/index.js +1 -0
  83. package/dist/policy/merge-approval-head.js +7 -6
  84. package/dist/pr-closing-keywords/gh.js +32 -9
  85. package/dist/pr-closing-keywords/main.d.ts +1 -0
  86. package/dist/pr-closing-keywords/main.js +68 -3
  87. package/dist/pr-closing-keywords/types.d.ts +2 -0
  88. package/dist/pr-merge-readiness/gh.js +32 -9
  89. package/dist/pr-protected-issues/gh.js +36 -9
  90. package/dist/pr-wait-mergeable/wrappers.js +3 -3
  91. package/dist/product-first-done-gate/evaluate.js +6 -2
  92. package/dist/product-first-done-gate/types.js +2 -0
  93. package/dist/release/consumer-hard-stops.d.ts +48 -0
  94. package/dist/release/consumer-hard-stops.js +140 -0
  95. package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
  96. package/dist/release/consumer-readiness-disclosure.js +51 -0
  97. package/dist/release/index.d.ts +2 -0
  98. package/dist/release/index.js +2 -0
  99. package/dist/release/pipeline.js +55 -0
  100. package/dist/release/run-consumer-readiness.d.ts +15 -0
  101. package/dist/release/run-consumer-readiness.js +24 -0
  102. package/dist/release/types.d.ts +6 -0
  103. package/dist/review-monitor/github-lease.js +4 -3
  104. package/dist/run-summary/types.d.ts +2 -2
  105. package/dist/scm/build-command.d.ts +2 -2
  106. package/dist/scm/build-command.js +2 -2
  107. package/dist/scm/call-shape.d.ts +25 -0
  108. package/dist/scm/call-shape.js +59 -0
  109. package/dist/scm/call.d.ts +7 -4
  110. package/dist/scm/call.js +41 -11
  111. package/dist/scm/gh-rest.d.ts +25 -12
  112. package/dist/scm/gh-rest.js +65 -15
  113. package/dist/scm/index.d.ts +2 -0
  114. package/dist/scm/index.js +2 -0
  115. package/dist/scm/spawn-status.d.ts +33 -0
  116. package/dist/scm/spawn-status.js +53 -0
  117. package/dist/scope/acceptance-evidence.d.ts +1 -1
  118. package/dist/scope/transition.js +4 -14
  119. package/dist/session/ac-pass-banking.d.ts +2 -2
  120. package/dist/session/ac-pass-banking.js +2 -2
  121. package/dist/session/child-occupancy.d.ts +72 -0
  122. package/dist/session/child-occupancy.js +209 -0
  123. package/dist/session/host-session-owner.d.ts +93 -0
  124. package/dist/session/host-session-owner.js +148 -0
  125. package/dist/session/index.d.ts +2 -0
  126. package/dist/session/index.js +2 -0
  127. package/dist/session/occupancy.d.ts +107 -2
  128. package/dist/session/occupancy.js +272 -34
  129. package/dist/session/verify-ac-session-cache.d.ts +2 -2
  130. package/dist/session/verify-ac-session-cache.js +2 -2
  131. package/dist/swarm/complete-cohort.js +2 -0
  132. package/dist/swarm/pre-dispatch.js +2 -0
  133. package/dist/swarm/subagent-status-dir.d.ts +2 -1
  134. package/dist/swarm/subagent-status-dir.js +11 -2
  135. package/dist/swarm/worktrees.js +2 -0
  136. package/dist/triage/evaluate/worktrees.js +153 -6
  137. package/dist/umbrella-current-shape/index.d.ts +51 -3
  138. package/dist/umbrella-current-shape/index.js +106 -18
  139. package/dist/validate-content/deposit-required.d.ts +39 -0
  140. package/dist/validate-content/deposit-required.js +147 -0
  141. package/dist/validate-content/index.d.ts +1 -0
  142. package/dist/validate-content/index.js +1 -0
  143. package/dist/validate-content/validate-links.d.ts +2 -3
  144. package/dist/validate-content/validate-links.js +29 -2
  145. package/dist/vbrief-activate/activate.d.ts +7 -2
  146. package/dist/vbrief-activate/activate.js +26 -13
  147. package/dist/verify-ac/evaluate.d.ts +9 -0
  148. package/dist/verify-ac/evaluate.js +32 -9
  149. package/dist/verify-env/agent-hooks.d.ts +6 -1
  150. package/dist/verify-env/agent-hooks.js +28 -2
  151. package/dist/verify-source/deposit-closure.d.ts +23 -0
  152. package/dist/verify-source/deposit-closure.js +162 -0
  153. package/dist/verify-source/index.d.ts +2 -0
  154. package/dist/verify-source/index.js +2 -0
  155. package/dist/verify-source/semantic-single-source.d.ts +36 -0
  156. package/dist/verify-source/semantic-single-source.js +349 -0
  157. package/package.json +3 -3
@@ -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
@@ -12,7 +12,10 @@
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
@@ -113,13 +116,14 @@ export function classifyProductDestForms(command) {
113
116
  // bypass.
114
117
  //
115
118
  // ⊗ Recognition is NOT total either, and this is not a security boundary.
116
- // Only four verbs are recognized, and a non-literal verb (`\rm`, `rm${IFS}x`)
117
- // defeats the tokenizer, so the fail-closed branch below only fires when the
118
- // verb is still legible. Unrecognized mutators (`git reset --hard`,
119
- // `git clean`, `git checkout` without `--`, `mv`, `sed -i`, `>` redirection)
120
- // and interpreters (`bash -c`, `python -c`) are fail-OPEN. See the threat
121
- // model in content/contracts/path-write-fence.md: this is a guardrail for
122
- // careless agents, not a boundary against adversarial ones.
119
+ // Only four dest-form verbs are recognized, and a non-literal verb (`\rm`,
120
+ // `rm${IFS}x`) defeats the tokenizer, so the fail-closed branch below only
121
+ // fires when the verb is still legible. Tree-wide destructive git is handled
122
+ // by classifyGitDestructive (always-on). Remaining fail-OPEN mutators:
123
+ // `git checkout` without `--` or `-f`, `mv`, `sed -i`, `>` redirection, and
124
+ // interpreters (`bash -c`, `python -c`). See the threat model in
125
+ // content/contracts/path-write-fence.md: this is a guardrail for careless
126
+ // agents, not a boundary against adversarial ones.
123
127
  if (segments.length === 1 && !hasUnsupportedSyntax(cmd)) {
124
128
  return classifySimpleDestForms(segments[0] ?? "");
125
129
  }
@@ -136,6 +140,29 @@ export function classifyProductDestForms(command) {
136
140
  expansion: true,
137
141
  }));
138
142
  }
143
+ /**
144
+ * Tree-wide destructive git — always-on at the dispatcher, not behind
145
+ * `shellDestForms`. Empty when the command is not one of those forms.
146
+ */
147
+ export function classifyGitDestructive(command) {
148
+ const cmd = command.trim();
149
+ if (cmd.length === 0)
150
+ return [];
151
+ const segments = splitCommandSegments(cmd);
152
+ const compound = segments.length > 1 || hasUnsupportedSyntax(cmd);
153
+ const found = [];
154
+ const seen = new Set();
155
+ for (const seg of segments) {
156
+ const form = classifyGitDestructiveSegment(seg);
157
+ if (form === null)
158
+ continue;
159
+ if (seen.has(form.kind))
160
+ continue;
161
+ seen.add(form.kind);
162
+ found.push(compound ? { ...form, unprovable: true } : form);
163
+ }
164
+ return found;
165
+ }
139
166
  /** Classify one command with no operators, no grouping, and no substitution. */
140
167
  function classifySimpleDestForms(segment) {
141
168
  const found = [];
@@ -334,9 +361,33 @@ function isGitWorkTreeAssign(token) {
334
361
  // A config FILE override can set core.worktree out of sight.
335
362
  return key === "GIT_CONFIG" || key === "GIT_CONFIG_GLOBAL" || key === "GIT_CONFIG_SYSTEM";
336
363
  }
364
+ function gitDirOrWorkTreeValue(token) {
365
+ const eq = token.indexOf("=");
366
+ if (eq <= 0)
367
+ return null;
368
+ const key = token.slice(0, eq);
369
+ const value = token.slice(eq + 1);
370
+ if ((key === "GIT_WORK_TREE" || key === "GIT_DIR") && value.length > 0)
371
+ return value;
372
+ return null;
373
+ }
337
374
  function skipPrefix(tokens) {
338
375
  let i = 0;
339
376
  let gitContext = false;
377
+ let opaqueContext = false;
378
+ const relocators = [];
379
+ const noteAssign = (tok) => {
380
+ const reloc = gitDirOrWorkTreeValue(tok);
381
+ if (reloc !== null) {
382
+ gitContext = true;
383
+ relocators.push(reloc);
384
+ return;
385
+ }
386
+ if (isGitWorkTreeAssign(tok)) {
387
+ gitContext = true;
388
+ opaqueContext = true;
389
+ }
390
+ };
340
391
  const consumeAssigns = () => {
341
392
  while (i < tokens.length) {
342
393
  const tok = tokens[i];
@@ -351,8 +402,7 @@ function skipPrefix(tokens) {
351
402
  }
352
403
  if (!isEnvAssign(tok))
353
404
  break;
354
- if (isGitWorkTreeAssign(tok))
355
- gitContext = true;
405
+ noteAssign(tok);
356
406
  i++;
357
407
  }
358
408
  };
@@ -380,15 +430,14 @@ function skipPrefix(tokens) {
380
430
  continue;
381
431
  }
382
432
  if (isEnvAssign(tok)) {
383
- if (isGitWorkTreeAssign(tok))
384
- gitContext = true;
433
+ noteAssign(tok);
385
434
  i++;
386
435
  continue;
387
436
  }
388
437
  break;
389
438
  }
390
439
  }
391
- return { i, gitContext };
440
+ return { i, gitContext, relocators, opaqueContext };
392
441
  }
393
442
  /**
394
443
  * Skip git's global options, reporting whether any of them RELOCATES the work
@@ -400,31 +449,51 @@ function skipPrefix(tokens) {
400
449
  function skipGitGlobals(tokens, start) {
401
450
  let i = start;
402
451
  let gitContext = false;
452
+ let opaqueContext = false;
453
+ const relocators = [];
403
454
  while (i < tokens.length) {
404
455
  const t = tokens[i];
405
456
  if (t === undefined || !t.startsWith("-"))
406
- return { i, gitContext };
457
+ return { i, gitContext, relocators, opaqueContext };
407
458
  if (t === "-c" || t === "--config-env") {
408
- if (isCoreWorkTreeConfig(tokens[i + 1]))
459
+ if (isCoreWorkTreeConfig(tokens[i + 1])) {
409
460
  gitContext = true;
461
+ opaqueContext = true;
462
+ }
410
463
  i += 2;
411
464
  continue;
412
465
  }
413
466
  if (t.startsWith("--config-env=")) {
414
- if (isCoreWorkTreeConfig(t.slice("--config-env=".length)))
467
+ if (isCoreWorkTreeConfig(t.slice("--config-env=".length))) {
415
468
  gitContext = true;
469
+ opaqueContext = true;
470
+ }
416
471
  i++;
417
472
  continue;
418
473
  }
419
474
  if (t === "-C" || t === "--work-tree" || t === "--git-dir") {
420
475
  gitContext = true;
476
+ const val = tokens[i + 1];
477
+ if (val !== undefined)
478
+ relocators.push(val);
421
479
  i += 2;
422
480
  continue;
423
481
  }
424
- if (t.startsWith("--work-tree=") ||
425
- t.startsWith("--git-dir=") ||
426
- (t.startsWith("-C") && t.length > 2)) {
482
+ if (t.startsWith("--work-tree=")) {
483
+ gitContext = true;
484
+ relocators.push(t.slice("--work-tree=".length));
485
+ i++;
486
+ continue;
487
+ }
488
+ if (t.startsWith("--git-dir=")) {
427
489
  gitContext = true;
490
+ relocators.push(t.slice("--git-dir=".length));
491
+ i++;
492
+ continue;
493
+ }
494
+ if (t.startsWith("-C") && t.length > 2) {
495
+ gitContext = true;
496
+ relocators.push(t.slice(2));
428
497
  i++;
429
498
  continue;
430
499
  }
@@ -437,14 +506,16 @@ function skipGitGlobals(tokens, start) {
437
506
  continue;
438
507
  }
439
508
  if (t.startsWith("-c")) {
440
- if (isCoreWorkTreeConfig(t.slice(2)))
509
+ if (isCoreWorkTreeConfig(t.slice(2))) {
441
510
  gitContext = true;
511
+ opaqueContext = true;
512
+ }
442
513
  i++;
443
514
  continue;
444
515
  }
445
516
  i++;
446
517
  }
447
- return { i, gitContext };
518
+ return { i, gitContext, relocators, opaqueContext };
448
519
  }
449
520
  function pathsAfterDashDash(tokens, start) {
450
521
  const dash = tokens.indexOf("--", start);
@@ -469,6 +540,72 @@ function dests(kind, paths, unsafe = false) {
469
540
  .filter((path) => path.length > 0)
470
541
  .map((path) => (unsafe ? { kind, path, expansion: true } : { kind, path }));
471
542
  }
543
+ function tokensHaveFlag(tokens, start, names) {
544
+ for (let i = start; i < tokens.length; i++) {
545
+ const t = tokens[i];
546
+ if (t === undefined)
547
+ break;
548
+ if (names.has(t))
549
+ return true;
550
+ for (const name of names) {
551
+ if (name.startsWith("--") && t.startsWith(`${name}=`))
552
+ return true;
553
+ }
554
+ }
555
+ return false;
556
+ }
557
+ function tokensHaveShortForce(tokens, start) {
558
+ for (let i = start; i < tokens.length; i++) {
559
+ const t = tokens[i];
560
+ if (t === undefined || t === "--")
561
+ break;
562
+ if (t === "-f" || t === "-B")
563
+ return true;
564
+ if (t.startsWith("-") && !t.startsWith("--") && t.includes("f"))
565
+ return true;
566
+ }
567
+ return false;
568
+ }
569
+ function classifyGitDestructiveSegment(segment) {
570
+ const tokens = tokenizeSegment(segment.trim());
571
+ const prefixSkip = skipPrefix(tokens);
572
+ let i = prefixSkip.i;
573
+ const binRaw = tokens[i];
574
+ if (binRaw === undefined)
575
+ return null;
576
+ const bin = binRaw.replace(/^[({!]+/, "").toLowerCase();
577
+ if (bin !== "git" && bin !== "git.exe")
578
+ return null;
579
+ const skipped = skipGitGlobals(tokens, i + 1);
580
+ i = skipped.i;
581
+ const sub = tokens[i]?.toLowerCase();
582
+ if (sub === undefined)
583
+ return null;
584
+ let kind = null;
585
+ if (sub === "reset" && tokensHaveFlag(tokens, i + 1, new Set(["--hard"]))) {
586
+ kind = "git-reset-hard";
587
+ }
588
+ else if (sub === "clean" &&
589
+ (tokensHaveFlag(tokens, i + 1, new Set(["--force", "-f"])) ||
590
+ tokensHaveShortForce(tokens, i + 1))) {
591
+ kind = "git-clean";
592
+ }
593
+ else if ((sub === "checkout" || sub === "switch") &&
594
+ (tokensHaveFlag(tokens, i + 1, new Set(["--force", "-f", "-B"])) ||
595
+ tokensHaveShortForce(tokens, i + 1))) {
596
+ kind = "git-checkout-force";
597
+ }
598
+ else if (sub === "stash") {
599
+ const action = tokens[i + 1]?.toLowerCase();
600
+ if (action === "drop" || action === "clear")
601
+ kind = "git-stash-drop";
602
+ }
603
+ if (kind === null)
604
+ return null;
605
+ const relocators = [...prefixSkip.relocators, ...skipped.relocators];
606
+ const opaque = prefixSkip.opaqueContext || skipped.opaqueContext;
607
+ return { kind, relocators, unprovable: opaque };
608
+ }
472
609
  /**
473
610
  * `-c core.workTree=...` / `--config-env` relocates the work tree through git
474
611
  * config. Resolution depends on the git dir, which this classifier does not
@@ -5,9 +5,10 @@ import { type RuntimeAuthorityPolicy } from "../policy/runtime-authority.js";
5
5
  import { type GitRunner } from "../session/git.js";
6
6
  import { type DetectWorkSelection, type RitualRunner, type VerifyResult } from "../session/verify-session-ritual.js";
7
7
  import { type HookPayloadContext } from "./classify/index.js";
8
+ import { type OwnerLivenessInput, type OwnerLivenessOutcome } from "./owner-liveness.js";
8
9
  import { type ActiveScopeInspection } from "./scope.js";
9
10
  export { ASSIST_SESSION_POSTURE_ENV, hookReadOnlyFromPayload, isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
10
- export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
11
+ export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, GROK_MUTATION_TOOL_CATALOG, GROK_NON_MUTATION_TOOLS, HOST_TOOL_SURFACE_AUDIT, type HostMutationToolCatalog, type HostToolSurfaceAudit, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, matcherHasLiteralToken, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
11
12
  export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor", "codex"];
12
13
  export type HookHost = (typeof HOOK_HOSTS)[number];
13
14
  export declare const HOOK_EVENTS: readonly ["session.start", "session.compact", "tool.before"];
@@ -44,7 +45,11 @@ export type HookDecisionCode = "session-start" | "session-start-disabled" | "ses
44
45
  /** UAT lease / human-origin grant denial (#2944). */
45
46
  | "authz-uat-deny" | "authz-grant-missing" | "authz-grant-origin-reject" | "authz-grant-scope-deny" | "authz-grant-expired" | "authz-grant-revoked" | "authz-grant-single-use-spent"
46
47
  /** Slash-command intent ceiling denial (#1193). */
47
- | "intent-ceiling-deny";
48
+ | "intent-ceiling-deny"
49
+ /** Tree-wide destructive git aimed at this checkout (#3917). */
50
+ | "git-destructive-deny"
51
+ /** Tree-wide destructive git aimed at an absolute out-of-root fixture (#3917). */
52
+ | "git-destructive-fixture";
48
53
  export interface HookDecision {
49
54
  readonly verdict: HookVerdict;
50
55
  readonly code: HookDecisionCode;
@@ -122,6 +127,8 @@ export interface HookPolicySeams {
122
127
  readonly realpathLifecycleExecutionRoot?: (path: string) => string;
123
128
  /** Test seam for Windows drive-only execution-root payloads (#2787). */
124
129
  readonly lifecycleExecutionPlatform?: NodeJS.Platform;
130
+ /** Test seam for the #3987 post-decision owner-liveness re-stamp. */
131
+ readonly restampOwnerLiveness?: (input: OwnerLivenessInput) => OwnerLivenessOutcome;
125
132
  }
126
133
  /** POSIX-ish project-relative path for lifecycle matching. */
127
134
  export declare function toProjectRelativePosix(projectRoot: string, targetPath: string): string;
@@ -196,6 +203,19 @@ export interface EffectiveHookRootAdmission {
196
203
  * target that resolved to some other toplevel whose identity cannot be read, is
197
204
  * a question asked and left unanswered — that fails closed rather than
198
205
  * inheriting payloadRoot's occupancy and ritual state (#3794).
206
+ *
207
+ * The `candidateRaw === null` fallback below — a target whose nearest existing
208
+ * ancestor has no Git toplevel at all (OS temp, a home file, anything outside
209
+ * every checkout) — is the third case and is deliberate, not a gap (#4013).
210
+ * `effectiveRoot` selects occupancy, ritual, active scope, the write fence and
211
+ * assist-scratch, so returning no root would drop all of them for an
212
+ * out-of-tree write, not just the lease; occupancy and ritual are cross-checked
213
+ * and the allow path re-stamps the lease, so exempting the lease alone is not a
214
+ * complete state transition; and `admitMutationTargetSet` demands one unique
215
+ * root, so a member contributing none has no defined combining rule and an
216
+ * untrusted patch path would become an authority-selection input. Gate-by-gate
217
+ * disposition, the three-surface matrix and the relative-target
218
+ * canonicalization limitation (#4023): content/docs/hook-root-admission.md.
199
219
  */
200
220
  export declare function admitEffectiveHookRoot(payloadRoot: string, writeTarget: string | null, runGit: GitRunner): EffectiveHookRootAdmission;
201
221
  /**