@deftai/directive-runtime 0.128.0 → 0.129.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 (74) hide show
  1. package/UPGRADING.md +2 -2
  2. package/dist/contract-entry.js +2 -0
  3. package/dist/doctor-diagnose.d.ts +8 -3
  4. package/dist/doctor-diagnose.js +23 -5
  5. package/dist/hook-dispatch.js +2 -1
  6. package/dist/self-complete.d.ts +4 -0
  7. package/dist/self-complete.js +18 -0
  8. package/dist/setup-wiring.d.ts +19 -1
  9. package/dist/setup-wiring.js +113 -24
  10. package/dist/setup.d.ts +4 -0
  11. package/dist/setup.js +80 -2
  12. package/dist/verify-ac.js +29 -2
  13. package/package.json +1 -1
  14. package/packs/skills/skills-pack-0.1.json +2 -2
  15. package/skills/deft-directive-refinement/SKILL.md +2 -2
  16. package/skills/deft-directive-setup/SKILL.md +2 -2
  17. package/skills/deft-directive-swarm/references/core-phase-0.md +1 -1
  18. package/tasks/docs.yml +1 -1
  19. package/vendor/directive-content/UPGRADING.md +2 -2
  20. package/vendor/directive-content/packs/skills/skills-pack-0.1.json +2 -2
  21. package/vendor/directive-content/skills/deft-directive-refinement/SKILL.md +2 -2
  22. package/vendor/directive-content/skills/deft-directive-setup/SKILL.md +2 -2
  23. package/vendor/directive-content/skills/deft-directive-swarm/references/core-phase-0.md +1 -1
  24. package/vendor/directive-content/tasks/docs.yml +1 -1
  25. package/vendor/directive-core/dist/.tsbuildinfo +1 -1
  26. package/vendor/directive-core/dist/class-checks/evaluate.d.ts.map +1 -1
  27. package/vendor/directive-core/dist/class-checks/evaluate.js +8 -0
  28. package/vendor/directive-core/dist/class-checks/evaluate.js.map +1 -1
  29. package/vendor/directive-core/dist/coverage-applicability/evaluate.d.ts.map +1 -1
  30. package/vendor/directive-core/dist/coverage-applicability/evaluate.js +1 -0
  31. package/vendor/directive-core/dist/coverage-applicability/evaluate.js.map +1 -1
  32. package/vendor/directive-core/dist/hooks/scope.d.ts +57 -0
  33. package/vendor/directive-core/dist/hooks/scope.d.ts.map +1 -1
  34. package/vendor/directive-core/dist/hooks/scope.js +171 -0
  35. package/vendor/directive-core/dist/hooks/scope.js.map +1 -1
  36. package/vendor/directive-core/dist/install-health/wiring.d.ts.map +1 -1
  37. package/vendor/directive-core/dist/install-health/wiring.js +111 -0
  38. package/vendor/directive-core/dist/install-health/wiring.js.map +1 -1
  39. package/vendor/directive-core/dist/packs/pack-render.d.ts +6 -1
  40. package/vendor/directive-core/dist/packs/pack-render.d.ts.map +1 -1
  41. package/vendor/directive-core/dist/packs/pack-render.js +103 -78
  42. package/vendor/directive-core/dist/packs/pack-render.js.map +1 -1
  43. package/vendor/directive-core/dist/packs/packs-root.d.ts +53 -0
  44. package/vendor/directive-core/dist/packs/packs-root.d.ts.map +1 -0
  45. package/vendor/directive-core/dist/packs/packs-root.js +83 -0
  46. package/vendor/directive-core/dist/packs/packs-root.js.map +1 -0
  47. package/vendor/directive-core/dist/packs/packs-slice.d.ts.map +1 -1
  48. package/vendor/directive-core/dist/packs/packs-slice.js +6 -6
  49. package/vendor/directive-core/dist/packs/packs-slice.js.map +1 -1
  50. package/vendor/directive-core/dist/preflight-gh/pre-push.d.ts.map +1 -1
  51. package/vendor/directive-core/dist/preflight-gh/pre-push.js +13 -0
  52. package/vendor/directive-core/dist/preflight-gh/pre-push.js.map +1 -1
  53. package/vendor/directive-core/dist/product-first-done-gate/acceptance-resolver.d.ts +1 -1
  54. package/vendor/directive-core/dist/product-first-done-gate/acceptance-resolver.d.ts.map +1 -1
  55. package/vendor/directive-core/dist/product-first-done-gate/acceptance-resolver.js +1 -1
  56. package/vendor/directive-core/dist/product-first-done-gate/acceptance-resolver.js.map +1 -1
  57. package/vendor/directive-core/dist/product-first-done-gate/index.d.ts +1 -1
  58. package/vendor/directive-core/dist/product-first-done-gate/index.d.ts.map +1 -1
  59. package/vendor/directive-core/dist/product-first-done-gate/index.js +1 -1
  60. package/vendor/directive-core/dist/product-first-done-gate/index.js.map +1 -1
  61. package/vendor/directive-core/dist/release/main.d.ts.map +1 -1
  62. package/vendor/directive-core/dist/release/main.js +3 -2
  63. package/vendor/directive-core/dist/release/main.js.map +1 -1
  64. package/vendor/directive-core/dist/release/version.d.ts +14 -0
  65. package/vendor/directive-core/dist/release/version.d.ts.map +1 -1
  66. package/vendor/directive-core/dist/release/version.js +28 -0
  67. package/vendor/directive-core/dist/release/version.js.map +1 -1
  68. package/vendor/directive-core/dist/xbrief-migrate/migrate-project.d.ts +5 -4
  69. package/vendor/directive-core/dist/xbrief-migrate/migrate-project.d.ts.map +1 -1
  70. package/vendor/directive-core/dist/xbrief-migrate/migrate-project.js +65 -46
  71. package/vendor/directive-core/dist/xbrief-migrate/migrate-project.js.map +1 -1
  72. package/vendor/directive-core/dist/xbrief-migrate/transforms.d.ts.map +1 -1
  73. package/vendor/directive-core/dist/xbrief-migrate/transforms.js +13 -6
  74. package/vendor/directive-core/dist/xbrief-migrate/transforms.js.map +1 -1
package/UPGRADING.md CHANGED
@@ -79,7 +79,7 @@ npm install -g --allow-scripts=<pkg,...> <package>
79
79
 
80
80
  ### Framework monorepo contributors
81
81
 
82
- Install source of truth for this repo is **pnpm** (`packageManager` field; CI uses `pnpm install --frozen-lockfile`). Install-script builds are gated by `pnpm-workspace.yaml` **`allowBuilds`** (currently `esbuild: true`). Do **not** treat `npm approve-scripts` under the pnpm virtual store as monorepo SoT.
82
+ Install source of truth for this repo is **pnpm** (`packageManager` field; CI uses `pnpm install --frozen-lockfile`). Install-script builds are gated by `pnpm-workspace.yaml` **`allowBuilds`** (currently `esbuild: true` where needed). **`allowBuilds` is PM-owned**: Directive install/upgrade uses the package manager for `package.json` / lockfile pin ops and does **not** hand-edit `allowBuilds`. If pnpm fails with `ERR_PNPM_IGNORED_BUILDS`, recover with `pnpm approve-builds` or set a real `true`/`false` under `allowBuilds` — do **not** commit pnpm's interactive placeholder text. Pin/add paths already pass `--ignore-scripts` so ordinary consumer upgrades should not need a runtime `allowBuilds` entry. Do **not** treat `npm approve-scripts` under the pnpm virtual store as monorepo SoT.
83
83
 
84
84
  ### Git / remote package sources and publish
85
85
 
@@ -456,7 +456,7 @@ import schema from "@deftai/directive-types/schemas/xbrief-core-0.8.schema.json"
456
456
 
457
457
  **Supported public API:** `@deftai/directive-types` and its published schema subpaths only. **`@deftai/directive-core` is published for npm dependency resolution but is not a supported library surface** — use the `deft` / `directive` CLI for behavior.
458
458
 
459
- **Schema source of truth:** `content/vbrief/schemas/vbrief-core.schema.json` in the directive repo. The types package mirrors it to `packages/types/schemas/` at build time; `deft` install also deposits schemas under project-root `xbrief/schemas/`. When in doubt, treat the directive repo canonical file as authoritative and refresh mirrors with `deft update` (install) or `@deftai/directive-types` (npm).
459
+ **Schema source of truth (OVPR):** the pinned runtime ships schemas under `node_modules/@deftai/directive-runtime/vbrief/schemas/` (current write schema: `xbrief-core-0.8.schema.json`). The types package publishes the same artifacts as `@deftai/directive-types/schemas/…`. Framework source authors at `content/vbrief/schemas/`. Install does **not** deposit project-root `xbrief/schemas/`. Refresh via `directive upgrade` (pin) or `@deftai/directive-types` (npm).
460
460
 
461
461
  ### `deft migrate` vs pre-v0.20 document-model migration
462
462
 
@@ -61,6 +61,8 @@ export async function handleContractEntrypoint(argv) {
61
61
  operation: operation,
62
62
  from,
63
63
  root,
64
+ // Explicit pre-write operator force only (#5837); self-complete never forces.
65
+ forceGitHooks: hasFlag(argv, "--force-githooks"),
64
66
  });
65
67
  process.stdout.write(`${JSON.stringify(result)}\n`);
66
68
  if (result.conflicts.length > 0)
@@ -8,6 +8,11 @@ export { DEFT_DOCTOR_HOOK_DIAGNOSTIC };
8
8
  export declare const GIT_PRECONDITION_RECOVERY_GUIDANCE = "git init, then directive install --repair";
9
9
  /** Readiness detail when wiring-ready is withheld solely because there is no .git. */
10
10
  export declare const GIT_PRECONDITION_READINESS_DETAIL = "incomplete install \u2014 wiring-ready withheld until git init (hooksPath not activatable)";
11
+ /**
12
+ * #5798 — wiring-only residual (pin/install/readiness already ok).
13
+ * Names Taskfile/probe surface; owned wiring damage → repair, never npm i -g hammer.
14
+ */
15
+ export declare const WIRING_RECOVERY_GUIDANCE = "inspect Taskfile.yml / Directive wiring targets (or probe); for owned wiring damage: directive install --repair";
11
16
  /** Patterns that must never appear as doctor remedies (AC12.5–12.7 / AC9.11). */
12
17
  export declare const FORBIDDEN_DOCTOR_REMEDY_PATTERNS: readonly RegExp[];
13
18
  export interface DoctorSurface {
@@ -34,11 +39,11 @@ export interface DoctorDiagnosis {
34
39
  readonly failClosed: boolean;
35
40
  readonly dataStatus: DataStatusValue;
36
41
  /**
37
- * Install/wiring recovery guidance, or F-010 git-precondition recovery.
38
- * Null when healthy or schema-only.
42
+ * Install-class (AC9.7), wiring-only (#5798 WIRING_RECOVERY_GUIDANCE), or
43
+ * F-010 git-precondition recovery. Null when healthy or schema-only.
39
44
  */
40
45
  readonly remedy: string | null;
41
- /** Schema/data migration remedy; never `install --repair`. */
46
+ /** Schema/data migration remedy; never the AC9.7 global-install hammer. */
42
47
  readonly schemaRemedy: "directive upgrade" | null;
43
48
  /**
44
49
  * F-010: set when pin/lock/installed/GENERATION/wiring are ok but readiness is
@@ -16,6 +16,11 @@ export { DEFT_DOCTOR_HOOK_DIAGNOSTIC };
16
16
  export const GIT_PRECONDITION_RECOVERY_GUIDANCE = "git init, then directive install --repair";
17
17
  /** Readiness detail when wiring-ready is withheld solely because there is no .git. */
18
18
  export const GIT_PRECONDITION_READINESS_DETAIL = "incomplete install — wiring-ready withheld until git init (hooksPath not activatable)";
19
+ /**
20
+ * #5798 — wiring-only residual (pin/install/readiness already ok).
21
+ * Names Taskfile/probe surface; owned wiring damage → repair, never npm i -g hammer.
22
+ */
23
+ export const WIRING_RECOVERY_GUIDANCE = "inspect Taskfile.yml / Directive wiring targets (or probe); for owned wiring damage: directive install --repair";
19
24
  /** Patterns that must never appear as doctor remedies (AC12.5–12.7 / AC9.11). */
20
25
  export const FORBIDDEN_DOCTOR_REMEDY_PATTERNS = [
21
26
  /\balign-pin\b/i,
@@ -208,13 +213,26 @@ function chooseRemedy(sync, dataStatus, projectRoot) {
208
213
  if (isNoGitReadinessGap(sync, projectRoot)) {
209
214
  return { remedy: GIT_PRECONDITION_RECOVERY_GUIDANCE, schemaRemedy: null };
210
215
  }
211
- const installBroken = sync.failClosed || !sync.inSync || !sync.predicates.installed.ok || !sync.predicates.pin.ok;
212
- // AC12.7: never dual-prescribe upgrade + repair. Install/wiring recovery wins
213
- // when both faults are present; schema-only behind → upgrade alone.
214
- if (installBroken) {
216
+ const p = sync.predicates;
217
+ // Install-class faults only — do NOT fold wiring-only into AC9.7 via !inSync (#5798).
218
+ const pinInstallBroken = sync.failClosed || !p.pin.ok || !p.installed.ok || !p.lockfile.ok || !p.generation.ok;
219
+ // Missing readiness (with .git) stays install/repair guidance (acceptance / F-010 peer).
220
+ const readinessBroken = !p.readiness.ok;
221
+ const wiringBroken = !p.wiring.ok;
222
+ const schemaBehind = dataStatus === "migration_needed" || dataStatus === "diagnosis_failed";
223
+ // AC12.7: AC9.7 global install + schema upgrade must not co-prescribe for install-class red.
224
+ if (pinInstallBroken || readinessBroken) {
215
225
  return { remedy: INSTALLATION_RECOVERY_GUIDANCE, schemaRemedy: null };
216
226
  }
217
- if (dataStatus === "migration_needed" || dataStatus === "diagnosis_failed") {
227
+ // Wiring-only (good pin/install/readiness): failure-shaped map — Taskfile/probe, not npm i -g.
228
+ if (wiringBroken) {
229
+ return {
230
+ remedy: WIRING_RECOVERY_GUIDANCE,
231
+ // Distinct surfaces may list both (#5798 multi-surface); not the AC9.7+upgrade dual ban.
232
+ schemaRemedy: schemaBehind ? "directive upgrade" : null,
233
+ };
234
+ }
235
+ if (schemaBehind) {
218
236
  return { remedy: null, schemaRemedy: "directive upgrade" };
219
237
  }
220
238
  return { remedy: null, schemaRemedy: null };
@@ -239,7 +239,8 @@ export function run(argv, seams = {}) {
239
239
  }
240
240
  // Wave 2d / AC8.6: registered hooks self-complete when readiness is missing/stale.
241
241
  // Suppressed when DEFT_DOCTOR_HOOK_DIAGNOSTIC is set (AC9.10).
242
- maybeSelfCompleteSetup(projectRoot, { fromHook: true });
242
+ // #5885: thread seams.writeErr so preserved tracked hooks disclose on stderr.
243
+ maybeSelfCompleteSetup(projectRoot, { fromHook: true, writeErr });
243
244
  const environ = mergeHookDispatchEnviron(payload);
244
245
  let decision = decideHook({
245
246
  host: args.host,
@@ -5,6 +5,8 @@ import { type SetupResult } from "./setup.js";
5
5
  * Doctor sets this before spawning committed hook probes.
6
6
  */
7
7
  export declare const DEFT_DOCTOR_HOOK_DIAGNOSTIC = "DEFT_DOCTOR_HOOK_DIAGNOSTIC";
8
+ /** Test-only: clear the process once-guard for preserved disclosure. */
9
+ export declare function resetSelfCompletePreservedDisclosureForTests(): void;
8
10
  export type SelfCompleteSkipReason = "fast-path" | "doctor-diagnostic" | "ineligible" | "not-configured" | "fail-closed" | "no-version";
9
11
  export interface SelfCompleteResult {
10
12
  readonly ran: boolean;
@@ -24,5 +26,7 @@ export declare function maybeSelfCompleteSetup(projectRoot: string, options?: {
24
26
  readonly environ?: NodeJS.ProcessEnv;
25
27
  /** Hooks are always eligible unless doctor-diagnostic. */
26
28
  readonly fromHook?: boolean;
29
+ /** Stderr sink for preserved tracked-hook disclosure (#5885); never stdout. */
30
+ readonly writeErr?: (text: string) => void;
27
31
  }): SelfCompleteResult;
28
32
  //# sourceMappingURL=self-complete.d.ts.map
@@ -23,6 +23,22 @@ import { runSetup } from "./setup.js";
23
23
  * Doctor sets this before spawning committed hook probes.
24
24
  */
25
25
  export const DEFT_DOCTOR_HOOK_DIAGNOSTIC = DOCTOR_DIAGNOSTIC_ENV;
26
+ /** Process once-guard for preserved-hook stderr lines (#5885). */
27
+ const emittedPreservedLines = new Set();
28
+ /** Test-only: clear the process once-guard for preserved disclosure. */
29
+ export function resetSelfCompletePreservedDisclosureForTests() {
30
+ emittedPreservedLines.clear();
31
+ }
32
+ function disclosePreservedHooks(preserved, writeErr) {
33
+ if (preserved === undefined || preserved.length === 0)
34
+ return;
35
+ for (const line of preserved) {
36
+ if (emittedPreservedLines.has(line))
37
+ continue;
38
+ emittedPreservedLines.add(line);
39
+ writeErr(`${line}\n`);
40
+ }
41
+ }
26
42
  /** Installation-management and help heads that never trigger self-complete. */
27
43
  const INELIGIBLE_HEADS = new Set([
28
44
  "install",
@@ -76,6 +92,7 @@ function readOwnRuntimeVersion() {
76
92
  */
77
93
  export function maybeSelfCompleteSetup(projectRoot, options = {}) {
78
94
  const environ = options.environ ?? process.env;
95
+ const writeErr = options.writeErr ?? ((text) => process.stderr.write(text));
79
96
  if (isDoctorDiagnosticEnv(environ)) {
80
97
  return { ran: false, skipReason: "doctor-diagnostic", setup: null };
81
98
  }
@@ -113,6 +130,7 @@ export function maybeSelfCompleteSetup(projectRoot, options = {}) {
113
130
  from: version,
114
131
  root: projectRoot,
115
132
  });
133
+ disclosePreservedHooks(setup.preserved, writeErr);
116
134
  return { ran: true, skipReason: null, setup };
117
135
  }
118
136
  //# sourceMappingURL=self-complete.js.map
@@ -84,10 +84,28 @@ export declare function ensureHostHooks(projectRoot: string, hostHooksPolicy?: H
84
84
  export declare const PRE_PUSH_BOUND_PRE_PR_MARKER = "deft:pre-pr-bound-push-v1";
85
85
  /** AC8.6 / AC2.5 / F-008 — disclosed when hooks files exist but core.hooksPath cannot activate. */
86
86
  export declare const GIT_HOOKS_INACTIVE_NO_GIT = "Git hooks inactive until git init + eligible repair/self-complete (core.hooksPath not set)";
87
- export declare function ensureGitHooks(projectRoot: string): {
87
+ /**
88
+ * True when `rel` is tracked by git in `projectRoot` (#5837). Any git failure
89
+ * (no repo, no git binary) reads as untracked, which keeps the replace path lawful.
90
+ */
91
+ export declare function isGitTrackedPath(projectRoot: string, rel: string): boolean;
92
+ /** Disclosure for a tracked divergent managed hook kept in place (#5837). */
93
+ export declare function formatTrackedHookPreserved(rel: string): string;
94
+ export interface EnsureGitHooksOptions {
95
+ /** Explicit pre-write operator force: replace tracked divergent managed hooks (#5837). */
96
+ readonly forceTrackedReplace?: boolean;
97
+ /**
98
+ * Test-only: override the next body for named relative hook paths so fixtures
99
+ * can reach refuse-to-downgrade under forceTrackedReplace (#5885 / #5804).
100
+ */
101
+ readonly testOnlyNextBodyByRel?: Readonly<Record<string, string>>;
102
+ }
103
+ export declare function ensureGitHooks(projectRoot: string, options?: EnsureGitHooksOptions): {
88
104
  created: string[];
89
105
  modified: string[];
90
106
  conflicts: string[];
107
+ /** Tracked divergent managed hooks kept in place; a non-failure outcome (#5837). */
108
+ preserved: string[];
91
109
  /** True only when core.hooksPath is relative .githooks (AC9.9). */
92
110
  hooksPathActive: boolean;
93
111
  /** Human disclosure when hooks are not live; null when active. */
@@ -8,6 +8,7 @@ import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { containedChmod, containedRemove, containedWrite, } from "../vendor/directive-core/dist/fs/contained-write.js";
10
10
  import { DIRECT_WRITE_HOOK_MATCHER, KILL_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../vendor/directive-core/dist/hooks/tools.js";
11
+ import { GROK_PRETOOLUSE_WRAPPER_COMMAND, GROK_PRETOOLUSE_WRAPPER_REL_PATH, grokPretooluseWrapperScriptSource, isGrokWrapperCommand, } from "../vendor/directive-core/dist/init-deposit/agent-hooks.js";
11
12
  import { INSTALLATION_RECOVERY_GUIDANCE, RUNTIME_PACKAGE_PATH_PREFIX, } from "../vendor/directive-core/dist/install-health/constants.js";
12
13
  import { MANAGED_SECTION_CLOSE, MANAGED_SECTION_OPEN_PREFIX, } from "../vendor/directive-core/dist/install-health/index.js";
13
14
  import { applyAgentsRefresh } from "../vendor/directive-core/dist/platform/agents-md.js";
@@ -428,6 +429,7 @@ function rewriteCommandsInValue(value) {
428
429
  }
429
430
  function isManagedHookCommand(command) {
430
431
  return (command.includes(HOOK_BIN_COMMAND) ||
432
+ isGrokWrapperCommand(command) ||
431
433
  /(^|[\s"`'])deft-hook([\s"`']|$)/.test(command) ||
432
434
  command.includes("deft hook:dispatch"));
433
435
  }
@@ -436,26 +438,49 @@ function object(value) {
436
438
  ? value
437
439
  : null;
438
440
  }
439
- function nestedManagedGroup(value) {
441
+ function isManagedNestedHookCommand(command, host) {
442
+ if (host === "grok" && isGrokWrapperCommand(command))
443
+ return true;
444
+ return isManagedHookCommand(command) && command.includes(`--host ${host}`);
445
+ }
446
+ /**
447
+ * Drop managed commands inside a nested group; keep custom hooks and group fields.
448
+ * Returns null when the group is empty after filtering (or was fully managed).
449
+ * Non-group entries pass through unchanged.
450
+ */
451
+ function filterManagedCommandsInNestedGroup(value, host, options = {}) {
440
452
  const group = object(value);
441
453
  if (group === null || !Array.isArray(group.hooks))
442
- return false;
443
- return group.hooks.some((h) => {
454
+ return value;
455
+ const keptHooks = group.hooks.filter((h) => {
444
456
  const hook = object(h);
445
- return typeof hook?.command === "string" && isManagedHookCommand(hook.command);
457
+ if (typeof hook?.command !== "string")
458
+ return true;
459
+ if (options.hostAgnostic)
460
+ return !isManagedHookCommand(hook.command);
461
+ return !isManagedNestedHookCommand(hook.command, host);
446
462
  });
463
+ if (keptHooks.length === 0)
464
+ return null;
465
+ return { ...group, hooks: keptHooks };
447
466
  }
448
467
  function cursorManagedEntry(value) {
449
468
  const entry = object(value);
450
469
  return typeof entry?.command === "string" && isManagedHookCommand(entry.command);
451
470
  }
471
+ function nestedToolCommand(host, event) {
472
+ // Grok PreToolUse deposits the outer-deny wrapper (#5807 / #5656 / #5715).
473
+ if (host === "grok" && event === "tool.before")
474
+ return GROK_PRETOOLUSE_WRAPPER_COMMAND;
475
+ return hookCommand(host, event);
476
+ }
452
477
  function nestedGroup(host, event, matcher) {
453
478
  return {
454
479
  ...(event === "tool.before" && matcher !== undefined ? { matcher } : {}),
455
480
  hooks: [
456
481
  {
457
482
  type: "command",
458
- command: hookCommand(host, event),
483
+ command: nestedToolCommand(host, event),
459
484
  timeout: event === "tool.before" ? 30 : 5,
460
485
  },
461
486
  ],
@@ -478,17 +503,9 @@ function stripNestedManaged(existing, host) {
478
503
  if (!(key in hooks))
479
504
  continue;
480
505
  const filtered = Array.isArray(hooks[key])
481
- ? hooks[key].filter((e) => {
482
- const group = object(e);
483
- if (group === null || !Array.isArray(group.hooks))
484
- return true;
485
- return !group.hooks.some((h) => {
486
- const hook = object(h);
487
- return (typeof hook?.command === "string" &&
488
- isManagedHookCommand(hook.command) &&
489
- hook.command.includes(`--host ${host}`));
490
- });
491
- })
506
+ ? hooks[key]
507
+ .map((e) => filterManagedCommandsInNestedGroup(e, host))
508
+ .filter((e) => e !== null)
492
509
  : [];
493
510
  if (filtered.length > 0)
494
511
  nextHooks[key] = filtered;
@@ -525,13 +542,21 @@ function stripCursorManaged(existing) {
525
542
  }
526
543
  return { ...existing, version: 1, hooks: nextHooks };
527
544
  }
545
+ function keepCustomNestedGroups(entries, host) {
546
+ return entries
547
+ .map((e) => filterManagedCommandsInNestedGroup(e, host, {
548
+ // Merge clears any managed identity (incl. Grok wrapper) before redeposit.
549
+ hostAgnostic: true,
550
+ }))
551
+ .filter((e) => e !== null);
552
+ }
528
553
  function mergeNestedHost(existing, host, options) {
529
554
  const hooks = object(existing.hooks) ?? {};
530
555
  const session = Array.isArray(hooks.SessionStart)
531
- ? hooks.SessionStart.filter((e) => !nestedManagedGroup(e))
556
+ ? keepCustomNestedGroups(hooks.SessionStart, host)
532
557
  : [];
533
558
  const preTool = Array.isArray(hooks.PreToolUse)
534
- ? hooks.PreToolUse.filter((e) => !nestedManagedGroup(e))
559
+ ? keepCustomNestedGroups(hooks.PreToolUse, host)
535
560
  : [];
536
561
  const nextHooks = {
537
562
  ...hooks,
@@ -543,10 +568,10 @@ function mergeNestedHost(existing, host, options) {
543
568
  };
544
569
  if (options.compact) {
545
570
  const preCompact = Array.isArray(hooks.PreCompact)
546
- ? hooks.PreCompact.filter((e) => !nestedManagedGroup(e))
571
+ ? keepCustomNestedGroups(hooks.PreCompact, host)
547
572
  : [];
548
573
  const postCompact = Array.isArray(hooks.PostCompact)
549
- ? hooks.PostCompact.filter((e) => !nestedManagedGroup(e))
574
+ ? keepCustomNestedGroups(hooks.PostCompact, host)
550
575
  : [];
551
576
  nextHooks.PreCompact = [...preCompact, nestedGroup(host, "session.compact")];
552
577
  nextHooks.PostCompact = [...postCompact, nestedGroup(host, "session.compact")];
@@ -665,6 +690,38 @@ export function ensureHostHooks(projectRoot, hostHooksPolicy = loadHostHooksPoli
665
690
  else
666
691
  modified.push(spec.rel);
667
692
  }
693
+ // #5807: rewrite/redeposit Grok outer-deny wrapper so leftover deft-hook deny
694
+ // bodies cannot sit beside a healthy PreToolUse registration.
695
+ const grokEnabled = isHostHookDepositEnabled("grok", hostHooksPolicy);
696
+ const wrapperRel = GROK_PRETOOLUSE_WRAPPER_REL_PATH;
697
+ if (grokEnabled) {
698
+ const wrapperBody = grokPretooluseWrapperScriptSource();
699
+ const wrapperPrior = readText(join(projectRoot, wrapperRel));
700
+ if (wrapperPrior !== wrapperBody) {
701
+ writeContained(projectRoot, wrapperRel, wrapperBody);
702
+ if (wrapperPrior === null)
703
+ created.push(wrapperRel);
704
+ else
705
+ modified.push(wrapperRel);
706
+ }
707
+ if (existsSync(join(projectRoot, wrapperRel))) {
708
+ try {
709
+ containedChmod({ root: projectRoot, target: wrapperRel, mode: 0o755 });
710
+ }
711
+ catch {
712
+ /* best-effort on platforms without chmod */
713
+ }
714
+ }
715
+ }
716
+ else if (existsSync(join(projectRoot, wrapperRel))) {
717
+ try {
718
+ containedRemove({ root: projectRoot, target: wrapperRel });
719
+ modified.push(wrapperRel);
720
+ }
721
+ catch {
722
+ /* best-effort strip */
723
+ }
724
+ }
668
725
  return { created, modified };
669
726
  }
670
727
  const GITHOOK_HELPER = `#!/usr/bin/env sh
@@ -740,22 +797,51 @@ function isOurGitHookBody(rel, body) {
740
797
  const EXECUTABLE_GITHOOKS = new Set([".githooks/pre-commit", ".githooks/pre-push"]);
741
798
  /** AC8.6 / AC2.5 / F-008 — disclosed when hooks files exist but core.hooksPath cannot activate. */
742
799
  export const GIT_HOOKS_INACTIVE_NO_GIT = "Git hooks inactive until git init + eligible repair/self-complete (core.hooksPath not set)";
743
- export function ensureGitHooks(projectRoot) {
800
+ /**
801
+ * True when `rel` is tracked by git in `projectRoot` (#5837). Any git failure
802
+ * (no repo, no git binary) reads as untracked, which keeps the replace path lawful.
803
+ */
804
+ export function isGitTrackedPath(projectRoot, rel) {
805
+ try {
806
+ execFileSync("git", ["-C", projectRoot, "ls-files", "--error-unmatch", "--", rel], {
807
+ stdio: ["ignore", "ignore", "ignore"],
808
+ });
809
+ return true;
810
+ }
811
+ catch {
812
+ return false;
813
+ }
814
+ }
815
+ /** Disclosure for a tracked divergent managed hook kept in place (#5837). */
816
+ export function formatTrackedHookPreserved(rel) {
817
+ return `${rel}: tracked managed hook differs from the template; kept (not a failure). Review and pass --force-githooks to setup to replace it (#5837)`;
818
+ }
819
+ export function ensureGitHooks(projectRoot, options = {}) {
744
820
  const created = [];
745
821
  const modified = [];
746
822
  const conflicts = [];
823
+ const preserved = [];
824
+ const force = options.forceTrackedReplace === true;
747
825
  const files = [
748
826
  { rel: ".githooks/_directive-run.sh", body: GITHOOK_HELPER },
749
827
  { rel: ".githooks/pre-commit", body: PRE_COMMIT },
750
828
  { rel: ".githooks/pre-push", body: PRE_PUSH },
751
829
  ];
752
830
  for (const file of files) {
753
- const next = file.body.endsWith("\n") ? file.body : `${file.body}\n`;
831
+ const override = options.testOnlyNextBodyByRel?.[file.rel];
832
+ const rawNext = override ?? file.body;
833
+ const next = rawNext.endsWith("\n") ? rawNext : `${rawNext}\n`;
754
834
  const prior = readText(join(projectRoot, file.rel));
755
835
  if (prior !== null && prior !== next && !isOurGitHookBody(file.rel, prior)) {
756
836
  conflicts.push(`${file.rel}: existing hook is not Directive-managed; refusing to overwrite (chain or remove manually)`);
757
837
  continue;
758
838
  }
839
+ // Tracked divergent preserve (#5837): never overwrite a committed hook body,
840
+ // even a Directive-managed one, unless the operator forced the replace.
841
+ if (prior !== null && prior !== next && !force && isGitTrackedPath(projectRoot, file.rel)) {
842
+ preserved.push(formatTrackedHookPreserved(file.rel));
843
+ continue;
844
+ }
759
845
  // Refuse-to-downgrade: marker-aware installer will not replace a newer enrolled
760
846
  // pre-push arm with an older body that lacks the monotonic marker (#5804).
761
847
  if (file.rel === ".githooks/pre-push" &&
@@ -802,7 +888,10 @@ export function ensureGitHooks(projectRoot) {
802
888
  catch {
803
889
  stillReferenced = true;
804
890
  }
805
- if (!stillReferenced) {
891
+ if (!stillReferenced && !force && isGitTrackedPath(projectRoot, legacyHelperRel)) {
892
+ preserved.push(formatTrackedHookPreserved(legacyHelperRel));
893
+ }
894
+ else if (!stillReferenced) {
806
895
  try {
807
896
  const rem = containedRemove({ root: projectRoot, target: legacyHelperRel });
808
897
  if (rem.removed)
@@ -847,7 +936,7 @@ export function ensureGitHooks(projectRoot) {
847
936
  inactiveDisclosure = `Git hooks inactive: core.hooksPath is '${current}' (not .githooks)`;
848
937
  }
849
938
  }
850
- return { created, modified, conflicts, hooksPathActive, inactiveDisclosure };
939
+ return { created, modified, conflicts, preserved, hooksPathActive, inactiveDisclosure };
851
940
  }
852
941
  const MINIMAL_TASKFILE = `version: '3'
853
942
 
package/dist/setup.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface SetupResult {
11
11
  readonly modified: string[];
12
12
  readonly removed: string[];
13
13
  readonly conflicts: string[];
14
+ /** Tracked divergent managed hooks kept in place; non-failure, present only when non-empty (#5837). */
15
+ readonly preserved?: string[];
14
16
  readonly message: string;
15
17
  }
16
18
  /**
@@ -22,6 +24,8 @@ export declare function runSetup(options: {
22
24
  readonly operation: SetupOperation;
23
25
  readonly from: string;
24
26
  readonly root: string;
27
+ /** Explicit pre-write operator force to replace tracked divergent managed hooks (#5837). */
28
+ readonly forceGitHooks?: boolean;
25
29
  }): SetupResult;
26
30
  export declare function setupChangeCount(result: SetupResult): number;
27
31
  //# sourceMappingURL=setup.d.ts.map
package/dist/setup.js CHANGED
@@ -9,14 +9,20 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { ContainedWriteError, containedRemove, containedWrite, } from "../vendor/directive-core/dist/fs/contained-write.js";
12
+ import { defaultTrackedNames, isTrackedDeletePath, stageFrameworkPaths, } from "../vendor/directive-core/dist/init-deposit/hygiene.js";
13
+ import { seedMinimalProjectDefinition } from "../vendor/directive-core/dist/init-deposit/init-deposit.js";
12
14
  import { buildOwnershipRecord, fingerprintContent, ownershipRecordsEqual, readEvidenceGenerationNumber, readOwnershipRecord, writeOwnershipRecord, } from "../vendor/directive-core/dist/install-health/index.js";
13
15
  import { assertInventoryCompleteness } from "./installer-output-inventory.js";
14
16
  import { CURRENT_DATA_SCHEMA_VERSION, DATA_SCHEMA_MARKER_REL } from "./migrate.js";
15
17
  import { isConvertStrategyFrom, runStrategy } from "./removal-steps/index.js";
16
18
  import { ensureAgentsManagedSection, ensureGitHooks, ensureHostHooks, ensureOpenClawIntegrations, ensureSkillPointers, ensureSlashCommands, ensureTaskfileInclude, listSkillPointerPaths, listVerifiedManagedSlashCommandPaths, RUNTIME_PREFIX, } from "./setup-wiring.js";
17
19
  const SEED_PLACEHOLDER_REL = "xbrief/.directive-seed-placeholder";
20
+ const PD_REL = "xbrief/PROJECT-DEFINITION.xbrief.json";
18
21
  const GEN_REL = ".deft/GENERATION.json";
19
22
  const WIRING_READY_DIR_REL = ".deft/directive";
23
+ const XBRIEF_LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
24
+ const XBRIEF_LIFECYCLE_GITKEEP = "# Keeps this xbrief/ lifecycle directory present in version control so the\n" +
25
+ "# deft-directive-setup pre-cutover guard does not fire on a fresh install (#4666).\n";
20
26
  function readOwnRuntimeVersion() {
21
27
  try {
22
28
  const here = dirname(fileURLToPath(import.meta.url));
@@ -144,6 +150,59 @@ function createSeedPlaceholder(projectRoot) {
144
150
  writeContained(projectRoot, SEED_PLACEHOLDER_REL, "# Directive initial seed placeholder (install only; repair must not create this).\n");
145
151
  return SEED_PLACEHOLDER_REL;
146
152
  }
153
+ /** Install-only: five xbrief lifecycle dirs + .gitkeeps (#4666 / AC8.4). */
154
+ function ensureXbriefLifecycleGitkeeps(projectRoot) {
155
+ const created = [];
156
+ for (const sub of XBRIEF_LIFECYCLE_DIRS) {
157
+ const rel = relPosix("xbrief", sub, ".gitkeep");
158
+ if (existsSync(join(projectRoot, rel)))
159
+ continue;
160
+ writeContained(projectRoot, rel, XBRIEF_LIFECYCLE_GITKEEP);
161
+ created.push(rel);
162
+ }
163
+ return created;
164
+ }
165
+ /**
166
+ * Stage this-run setup writes after install/upgrade (#4666).
167
+ * Always includes PD + lifecycle gitkeeps when present (fix the 0.126 hole).
168
+ * Does not `git add` every existing managed matcher path — that would stage
169
+ * unrelated dirty peers (package.json / custom .githooks/*). Keeps exact
170
+ * this-run paths (no skill/command parent compaction) so sibling untracked
171
+ * skills stay unstaged. This-run removals may be missing on disk so tracked
172
+ * deletions can reach `git add`; untracked missing paths are skipped so one
173
+ * unmatched pathspec cannot fail the whole staging batch (#5883 P1 / #3394).
174
+ */
175
+ function stageSetupManagedPaths(projectRoot, thisRunPaths) {
176
+ const present = [];
177
+ const missingAllow = [];
178
+ const seen = new Set();
179
+ const add = (rel, opts = {}) => {
180
+ const normalized = rel.replace(/\\/g, "/");
181
+ if (!normalized || seen.has(normalized))
182
+ return;
183
+ const onDisk = existsSync(join(projectRoot, normalized));
184
+ if (!onDisk && !opts.allowMissing)
185
+ return;
186
+ seen.add(normalized);
187
+ if (onDisk)
188
+ present.push(normalized);
189
+ else
190
+ missingAllow.push(normalized);
191
+ };
192
+ for (const rel of thisRunPaths) {
193
+ add(rel, { allowMissing: true });
194
+ }
195
+ add(PD_REL);
196
+ for (const sub of XBRIEF_LIFECYCLE_DIRS) {
197
+ add(relPosix("xbrief", sub, ".gitkeep"));
198
+ }
199
+ const tracked = new Set(defaultTrackedNames(projectRoot, missingAllow));
200
+ const stagePaths = [
201
+ ...present,
202
+ ...missingAllow.filter((path) => isTrackedDeletePath(path, tracked)),
203
+ ];
204
+ stageFrameworkPaths(projectRoot, stagePaths);
205
+ }
147
206
  /** Shared host settings / Taskfile: only Directive entries are owned (structured-entry). */
148
207
  const STRUCTURED_ENTRY_RELS = [
149
208
  { path: ".claude/settings.json", entryKey: "hooks" },
@@ -207,7 +266,9 @@ function buildOwnershipArtifacts(projectRoot) {
207
266
  });
208
267
  }
209
268
  const seedAbs = join(projectRoot, SEED_PLACEHOLDER_REL);
210
- if (existsSync(seedAbs)) {
269
+ const pdAbs = join(projectRoot, PD_REL);
270
+ const hasLifecycle = XBRIEF_LIFECYCLE_DIRS.some((sub) => existsSync(join(projectRoot, "xbrief", sub, ".gitkeep")));
271
+ if (existsSync(seedAbs) || existsSync(pdAbs) || hasLifecycle) {
211
272
  artifacts.push({
212
273
  path: "xbrief",
213
274
  kind: "retained",
@@ -223,6 +284,8 @@ function buildOwnershipArtifacts(projectRoot) {
223
284
  */
224
285
  export function runSetup(options) {
225
286
  const { operation, from, root } = options;
287
+ let preserved = [];
288
+ const withPreserved = () => (preserved.length > 0 ? { preserved } : {});
226
289
  const created = [];
227
290
  const modified = [];
228
291
  const removed = [];
@@ -297,7 +360,8 @@ export function runSetup(options) {
297
360
  const hooks = ensureHostHooks(root);
298
361
  created.push(...hooks.created);
299
362
  modified.push(...hooks.modified);
300
- const githooks = ensureGitHooks(root);
363
+ const githooks = ensureGitHooks(root, { forceTrackedReplace: options.forceGitHooks === true });
364
+ preserved = githooks.preserved;
301
365
  created.push(...githooks.created);
302
366
  modified.push(...githooks.modified);
303
367
  conflicts.push(...githooks.conflicts);
@@ -309,10 +373,15 @@ export function runSetup(options) {
309
373
  // Seeds only on first-time install (AC8.4); repair/upgrade never create them.
310
374
  // AC6.2: leave data-schema current for empty/seeded install. Repair may write a
311
375
  // missing marker (not a migration); upgrade still migrates when data is behind.
376
+ // #4666: greenfield retain — PD + lifecycle dirs (not placeholder-only); no schemas.
312
377
  if (operation === "install" && conflicts.length === 0) {
313
378
  const seed = createSeedPlaceholder(root);
314
379
  if (seed !== null)
315
380
  created.push(seed);
381
+ created.push(...ensureXbriefLifecycleGitkeeps(root));
382
+ if (seedMinimalProjectDefinition(root, { printf: () => undefined })) {
383
+ created.push(PD_REL);
384
+ }
316
385
  }
317
386
  if ((operation === "install" || operation === "repair") &&
318
387
  conflicts.length === 0 &&
@@ -335,6 +404,12 @@ export function runSetup(options) {
335
404
  created.push(GEN_REL);
336
405
  else if (!unchanged || legacyConversion || convertViaRemovals)
337
406
  modified.push(GEN_REL);
407
+ // #4666: stage this-run managed peers including PD + lifecycle after install/upgrade.
408
+ // Still no auto-commit. Repair does not stage (seeds are install-only; upgrade stages
409
+ // existing PD/lifecycle with refreshed wiring).
410
+ if (conflicts.length === 0 && (operation === "install" || operation === "upgrade")) {
411
+ stageSetupManagedPaths(root, [...created, ...modified, ...removed]);
412
+ }
338
413
  if (conflicts.length > 0) {
339
414
  // Never publish readiness after conflict; invalidate any existing marker (AC8.6).
340
415
  invalidateReadinessMarkers(root, removed);
@@ -350,6 +425,7 @@ export function runSetup(options) {
350
425
  modified,
351
426
  removed,
352
427
  conflicts,
428
+ ...withPreserved(),
353
429
  message: "setup completed with conflicts",
354
430
  };
355
431
  }
@@ -371,6 +447,7 @@ export function runSetup(options) {
371
447
  modified,
372
448
  removed,
373
449
  conflicts,
450
+ ...withPreserved(),
374
451
  message: `setup ${operation} completed for ${runtimeVersion} (${RUNTIME_PREFIX}); ${disclosure}`,
375
452
  };
376
453
  }
@@ -388,6 +465,7 @@ export function runSetup(options) {
388
465
  modified,
389
466
  removed,
390
467
  conflicts,
468
+ ...withPreserved(),
391
469
  message: `setup ${operation} completed for ${runtimeVersion} (${RUNTIME_PREFIX})`,
392
470
  };
393
471
  }