@deftai/directive-runtime 0.129.1 → 0.129.2

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 (54) hide show
  1. package/.githooks/_directive-run.sh +9 -12
  2. package/.githooks/pre-push +1 -5
  3. package/dist/doctor-diagnose.js +6 -3
  4. package/dist/pre-pr-run.js +9 -2
  5. package/dist/setup-wiring.js +64 -41
  6. package/package.json +1 -1
  7. package/tasks/engine-invoke.cjs +37 -2
  8. package/tasks/engine-invoke.test.cjs +14 -0
  9. package/tasks/engine.yml +5 -0
  10. package/vendor/directive-content/.githooks/_directive-run.sh +9 -12
  11. package/vendor/directive-content/.githooks/pre-push +1 -5
  12. package/vendor/directive-content/tasks/engine-invoke.cjs +37 -2
  13. package/vendor/directive-content/tasks/engine-invoke.test.cjs +14 -0
  14. package/vendor/directive-content/tasks/engine.yml +5 -0
  15. package/vendor/directive-core/dist/.tsbuildinfo +1 -1
  16. package/vendor/directive-core/dist/class-checks/evaluate.d.ts.map +1 -1
  17. package/vendor/directive-core/dist/class-checks/evaluate.js +21 -4
  18. package/vendor/directive-core/dist/class-checks/evaluate.js.map +1 -1
  19. package/vendor/directive-core/dist/coverage-applicability/evaluate.d.ts.map +1 -1
  20. package/vendor/directive-core/dist/coverage-applicability/evaluate.js +134 -6
  21. package/vendor/directive-core/dist/coverage-applicability/evaluate.js.map +1 -1
  22. package/vendor/directive-core/dist/coverage-hotspots/evaluate.d.ts +2 -0
  23. package/vendor/directive-core/dist/coverage-hotspots/evaluate.d.ts.map +1 -1
  24. package/vendor/directive-core/dist/coverage-hotspots/evaluate.js +19 -6
  25. package/vendor/directive-core/dist/coverage-hotspots/evaluate.js.map +1 -1
  26. package/vendor/directive-core/dist/fs/contained-write.d.ts +12 -0
  27. package/vendor/directive-core/dist/fs/contained-write.d.ts.map +1 -1
  28. package/vendor/directive-core/dist/fs/contained-write.js +35 -1
  29. package/vendor/directive-core/dist/fs/contained-write.js.map +1 -1
  30. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts +1 -0
  31. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts.map +1 -1
  32. package/vendor/directive-core/dist/init-deposit/agent-hooks.js +75 -72
  33. package/vendor/directive-core/dist/init-deposit/agent-hooks.js.map +1 -1
  34. package/vendor/directive-core/dist/init-deposit/host-hook-outer.d.ts +26 -0
  35. package/vendor/directive-core/dist/init-deposit/host-hook-outer.d.ts.map +1 -0
  36. package/vendor/directive-core/dist/init-deposit/host-hook-outer.js +158 -0
  37. package/vendor/directive-core/dist/init-deposit/host-hook-outer.js.map +1 -0
  38. package/vendor/directive-core/dist/init-deposit/hygiene.d.ts.map +1 -1
  39. package/vendor/directive-core/dist/init-deposit/hygiene.js +29 -1
  40. package/vendor/directive-core/dist/init-deposit/hygiene.js.map +1 -1
  41. package/vendor/directive-core/dist/install-health/wiring.d.ts.map +1 -1
  42. package/vendor/directive-core/dist/install-health/wiring.js +30 -1
  43. package/vendor/directive-core/dist/install-health/wiring.js.map +1 -1
  44. package/vendor/directive-core/dist/pre-pr-controller/controller.d.ts +1 -1
  45. package/vendor/directive-core/dist/pre-pr-controller/controller.d.ts.map +1 -1
  46. package/vendor/directive-core/dist/pre-pr-controller/controller.js +43 -15
  47. package/vendor/directive-core/dist/pre-pr-controller/controller.js.map +1 -1
  48. package/vendor/directive-core/dist/pre-pr-controller/coverage-report.d.ts +22 -0
  49. package/vendor/directive-core/dist/pre-pr-controller/coverage-report.d.ts.map +1 -0
  50. package/vendor/directive-core/dist/pre-pr-controller/coverage-report.js +48 -0
  51. package/vendor/directive-core/dist/pre-pr-controller/coverage-report.js.map +1 -0
  52. package/vendor/directive-core/dist/pre-pr-controller/types.d.ts +7 -0
  53. package/vendor/directive-core/dist/pre-pr-controller/types.d.ts.map +1 -1
  54. package/vendor/directive-core/dist/pre-pr-controller/types.js.map +1 -1
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env sh
2
- # Package-relative Directive git-hook helper (AC9.9).
2
+ # Package-relative Directive git-hook helper (AC9.9 / AC9.12 / #5994).
3
3
  # Sourced by pre-commit/pre-push; defines run_directive.
4
+ # F1 order: disable → missing pin (project PM) → pin bin.js.
4
5
  if [ -z "${REPO_ROOT:-}" ]; then
5
6
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
6
7
  fi
@@ -8,23 +9,19 @@ if [ -z "$REPO_ROOT" ]; then
8
9
  echo "directive hooks: unable to resolve repo root." >&2
9
10
  exit 1
10
11
  fi
11
- RUNTIME_BIN="$REPO_ROOT/node_modules/@deftai/directive-runtime/dist/bin.js"
12
- if [ ! -f "$RUNTIME_BIN" ]; then
13
- echo "directive hooks: missing $RUNTIME_BIN" >&2
14
- echo "Recovery: npm i -g @deftai/directive, then directive install --repair" >&2
15
- exit 1
16
- fi
17
12
  run_directive() {
18
- # #3039 / #5543: root local kill-switch — skip CLI spawn when flag is present,
19
- # not a symlink, and not tracked by git (tracked flag must not disable clones).
13
+ # #3039 / #5543 / #5994 F1: disable BEFORE requiring pin bin.js; quiet skip.
20
14
  _kill="$REPO_ROOT/.deft-directive-disable"
21
15
  if [ -f "$_kill" ] && [ ! -L "$_kill" ]; then
22
16
  if ! git -C "$REPO_ROOT" ls-files --error-unmatch -- .deft-directive-disable >/dev/null 2>&1; then
23
- echo "Directive is DISABLED for this project via root .deft-directive-disable (test/local kill-switch)." >&2
24
- echo "Git hooks skipping Directive CLI spawn (#5543 / #3039). Deposit may remain." >&2
25
- echo "Recovery: rm .deft-directive-disable && start a new agent session." >&2
26
17
  return 0
27
18
  fi
28
19
  fi
20
+ RUNTIME_BIN="$REPO_ROOT/node_modules/@deftai/directive-runtime/dist/bin.js"
21
+ if [ ! -f "$RUNTIME_BIN" ]; then
22
+ echo "directive hooks: missing $RUNTIME_BIN" >&2
23
+ echo "Directive hooks: missing node_modules/@deftai/directive-runtime. Run npm install or pnpm install (or yarn) in this project." >&2
24
+ exit 1
25
+ fi
29
26
  node "$RUNTIME_BIN" "$@"
30
27
  }
@@ -5,8 +5,4 @@ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 1
5
5
  . "$REPO_ROOT/.githooks/_directive-run.sh"
6
6
  run_directive verify:branch --project-root "$REPO_ROOT" || exit $?
7
7
  # Enrolled bound pre-PR arm: sole-stdin (do not redirect shared run_directive to /dev/null)
8
- # Pins older than #5804 lack --push-admit: keep the pre-#5804 hook there (verify:branch only).
9
- _pre_pr_help="$(run_directive pre-pr:run --help </dev/null 2>&1)" || exit $?
10
- case "$_pre_pr_help" in
11
- *--push-admit*) run_directive pre-pr:run --push-admit --project-root "$REPO_ROOT" || exit $? ;;
12
- esac
8
+ run_directive pre-pr:run --push-admit --project-root "$REPO_ROOT" || exit $?
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { join } from "node:path";
9
+ import { isHostHookOuterCommand } from "../vendor/directive-core/dist/init-deposit/host-hook-outer.js";
9
10
  import { checkFullInSync, DEFT_DOCTOR_HOOK_DIAGNOSTIC, INSTALLATION_RECOVERY_GUIDANCE, RUNTIME_PACKAGE_PATH_PREFIX, } from "../vendor/directive-core/dist/install-health/index.js";
10
11
  import { readProjectDataStatus } from "./data-status.js";
11
12
  export { DEFT_DOCTOR_HOOK_DIAGNOSTIC };
@@ -48,7 +49,7 @@ function schemaSurface(dataStatus) {
48
49
  return { ok: false, detail: "project data schema diagnosis failed" };
49
50
  }
50
51
  /**
51
- * Cheap read-only check that committed hook/skill wiring uses package-relative form.
52
+ * Cheap read-only check that committed hook/skill wiring uses host `.mjs` outer or package-relative form.
52
53
  * Does not spawn hooks; structural only (AC12.3 hook command form / skill pointers).
53
54
  */
54
55
  function inspectHookAndSkillForm(projectRoot) {
@@ -70,11 +71,13 @@ function inspectHookAndSkillForm(projectRoot) {
70
71
  sawHook = true;
71
72
  try {
72
73
  const text = readFileSync(abs, "utf8");
74
+ // #5994 / AC9.1: v261027 host `.mjs` outer is the registered form; pin hook-bin is spawned inside.
75
+ const hasOuter = isHostHookOuterCommand(text);
73
76
  const hasPkg = text.includes(prefix) && text.includes("hook-bin");
74
77
  const hasLegacy = text.includes(".deft/core/") || /(^|[\s"`'])deft-hook([\s"`']|$)/.test(text);
75
- if (hasLegacy || !hasPkg) {
78
+ if (hasLegacy || !(hasOuter || hasPkg)) {
76
79
  hookOk = false;
77
- hookDetail = `${rel} does not use package-relative hook-bin form`;
80
+ hookDetail = `${rel} does not use host .mjs outer or package-relative hook-bin form`;
78
81
  break;
79
82
  }
80
83
  }
@@ -422,13 +422,15 @@ export function run(argv) {
422
422
  return 2;
423
423
  }
424
424
  const spec = phaseSpec(phase);
425
+ // Same root rule as push-admit: one report path for observe and satisfy.
426
+ const projectRoot = args.projectRoot?.trim() || process.cwd();
425
427
  const d = observeCommandPhase(store, args.runId, {
426
428
  phaseId: phase,
427
429
  command: args.command.length > 0 ? args.command : (spec.command ?? ""),
428
430
  exitCode: args.exitCode,
429
431
  inputHash: args.inputHash ?? rec?.inputHash ?? "",
430
432
  skipReason: args.skipReason,
431
- });
433
+ }, { projectRoot });
432
434
  emit(args.json, d, d.message, !d.ok);
433
435
  return d.ok ? 0 : 1;
434
436
  }
@@ -508,7 +510,12 @@ export function run(argv) {
508
510
  process.stderr.write("pre_pr_run: --complete requires --run-id\n");
509
511
  return 2;
510
512
  }
511
- const d = completeRun(store, controllerPublisher(), args.runId);
513
+ // #5804 / #5994: projectRoot is required so completeRun can stamp
514
+ // pushBinding.reviewedDeletedPaths from base..head.
515
+ const projectRoot = args.projectRoot?.trim() || process.cwd();
516
+ const d = completeRun(store, controllerPublisher(), args.runId, undefined, {
517
+ projectRoot,
518
+ });
512
519
  emit(args.json, d, d.message, !d.ok);
513
520
  return d.ok ? 0 : 1;
514
521
  }
@@ -8,7 +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
+ import { GROK_PRETOOLUSE_WRAPPER_REL_PATH, HOST_HOOK_MISSING_PIN_PROJECT_PM_GUIDANCE, hostHookOuterCommand, hostHookOuterRelPath, hostHookOuterSha8, hostHookOuterSource, isGrokWrapperCommand, isHostHookOuterCommand, } from "../vendor/directive-core/dist/init-deposit/agent-hooks.js";
12
12
  import { INSTALLATION_RECOVERY_GUIDANCE, RUNTIME_PACKAGE_PATH_PREFIX, } from "../vendor/directive-core/dist/install-health/constants.js";
13
13
  import { MANAGED_SECTION_CLOSE, MANAGED_SECTION_OPEN_PREFIX, } from "../vendor/directive-core/dist/install-health/index.js";
14
14
  import { applyAgentsRefresh } from "../vendor/directive-core/dist/platform/agents-md.js";
@@ -376,25 +376,45 @@ export function ensureOpenClawIntegrations(projectRoot) {
376
376
  }
377
377
  }
378
378
  function hookCommand(host, event) {
379
- return `${HOOK_BIN_COMMAND} --host ${host} --event ${event}`;
379
+ const sha8 = hostHookOuterSha8();
380
+ return `${hostHookOuterCommand(host, sha8)} --host ${host} --event ${event}`;
380
381
  }
381
382
  /** Rewrite bare `deft-hook` / legacy `deft hook:dispatch` to package-relative (AC9.2). */
382
383
  export function rewriteBareDeftHookCommand(command) {
383
384
  const trimmed = command.trim();
384
- if (trimmed.startsWith(HOOK_BIN_COMMAND))
385
+ if (isHostHookOuterCommand(trimmed))
385
386
  return command;
387
+ const hostMatch = trimmed.match(/--host\s+(\w+)/);
388
+ const eventMatch = trimmed.match(/--event\s+([\w.]+)/);
389
+ const host = hostMatch?.[1];
390
+ const event = eventMatch?.[1];
391
+ const sha8 = hostHookOuterSha8();
392
+ const toOuter = (h, e) => `${hostHookOuterCommand(h, sha8)} --host ${h} --event ${e}`;
393
+ // Legacy Grok .sh wrapper → outer
394
+ if (isGrokWrapperCommand(trimmed) && host && event) {
395
+ return toOuter(host, event);
396
+ }
397
+ if (isGrokWrapperCommand(trimmed)) {
398
+ return toOuter("grok", "tool.before");
399
+ }
400
+ // Direct hook-bin with host/event → outer
401
+ if (trimmed.includes(HOOK_BIN_COMMAND) && host && event) {
402
+ return toOuter(host, event);
403
+ }
386
404
  const bare = /^(?:deft-hook|deft\s+hook:dispatch)(\s+.*)?$/;
387
405
  const m = trimmed.match(bare);
388
406
  if (m) {
389
407
  const rest = (m[1] ?? "").trim();
408
+ if (host && event)
409
+ return toOuter(host, event);
410
+ // bare without host/event: leave as package-relative interim only if no host
390
411
  return rest.length > 0 ? `${HOOK_BIN_COMMAND} ${rest}` : HOOK_BIN_COMMAND;
391
412
  }
392
- // Inline occurrences inside longer strings (rare)
393
- if (/\bdeft-hook\b/.test(trimmed) && !trimmed.includes(RUNTIME_PREFIX)) {
394
- return trimmed.replace(/\bdeft-hook\b/g, HOOK_BIN_COMMAND);
413
+ if (/\bdeft-hook\b/.test(trimmed) && !isHostHookOuterCommand(trimmed) && host && event) {
414
+ return toOuter(host, event);
395
415
  }
396
- if (/\bdeft\s+hook:dispatch\b/.test(trimmed) && !trimmed.includes(RUNTIME_PREFIX)) {
397
- return trimmed.replace(/\bdeft\s+hook:dispatch\b/g, HOOK_BIN_COMMAND);
416
+ if (/\bdeft\s+hook:dispatch\b/.test(trimmed) && host && event) {
417
+ return toOuter(host, event);
398
418
  }
399
419
  return command;
400
420
  }
@@ -428,7 +448,8 @@ function rewriteCommandsInValue(value) {
428
448
  return { value, changed: false };
429
449
  }
430
450
  function isManagedHookCommand(command) {
431
- return (command.includes(HOOK_BIN_COMMAND) ||
451
+ return (isHostHookOuterCommand(command) ||
452
+ command.includes(HOOK_BIN_COMMAND) ||
432
453
  isGrokWrapperCommand(command) ||
433
454
  /(^|[\s"`'])deft-hook([\s"`']|$)/.test(command) ||
434
455
  command.includes("deft hook:dispatch"));
@@ -469,9 +490,7 @@ function cursorManagedEntry(value) {
469
490
  return typeof entry?.command === "string" && isManagedHookCommand(entry.command);
470
491
  }
471
492
  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;
493
+ // #5994 / AC9.1: every host/event uses the content-hashed `.mjs` outer.
475
494
  return hookCommand(host, event);
476
495
  }
477
496
  function nestedGroup(host, event, matcher) {
@@ -690,30 +709,34 @@ export function ensureHostHooks(projectRoot, hostHooksPolicy = loadHostHooksPoli
690
709
  else
691
710
  modified.push(spec.rel);
692
711
  }
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);
712
+ // #5994: deposit content-hashed `.mjs` outers; strip legacy Grok `.sh`.
713
+ const outerSource = hostHookOuterSource();
714
+ const outerSha8 = hostHookOuterSha8(outerSource);
715
+ for (const host of ["claude", "grok", "cursor", "codex"]) {
716
+ const rel = hostHookOuterRelPath(host, outerSha8);
717
+ const enabled = isHostHookDepositEnabled(host, hostHooksPolicy);
718
+ if (enabled) {
719
+ const prior = readText(join(projectRoot, rel));
720
+ if (prior !== outerSource) {
721
+ writeContained(projectRoot, rel, outerSource);
722
+ if (prior === null)
723
+ created.push(rel);
724
+ else
725
+ modified.push(rel);
726
+ }
706
727
  }
707
- if (existsSync(join(projectRoot, wrapperRel))) {
728
+ else if (existsSync(join(projectRoot, rel))) {
708
729
  try {
709
- containedChmod({ root: projectRoot, target: wrapperRel, mode: 0o755 });
730
+ containedRemove({ root: projectRoot, target: rel });
731
+ modified.push(rel);
710
732
  }
711
733
  catch {
712
- /* best-effort on platforms without chmod */
734
+ /* best-effort strip */
713
735
  }
714
736
  }
715
737
  }
716
- else if (existsSync(join(projectRoot, wrapperRel))) {
738
+ const wrapperRel = GROK_PRETOOLUSE_WRAPPER_REL_PATH;
739
+ if (existsSync(join(projectRoot, wrapperRel))) {
717
740
  try {
718
741
  containedRemove({ root: projectRoot, target: wrapperRel });
719
742
  modified.push(wrapperRel);
@@ -724,9 +747,13 @@ export function ensureHostHooks(projectRoot, hostHooksPolicy = loadHostHooksPoli
724
747
  }
725
748
  return { created, modified };
726
749
  }
750
+ // Taskfile F1 disable-first + project-PM pin miss live in tasks/engine.yml +
751
+ // tasks/engine-invoke.cjs (#5994) — disable-active runs BEFORE pin-bin / is-buildable-source;
752
+ // pin-bin also quiet-exits when disable is active. This GITHOOK_HELPER is the .githooks twin.
727
753
  const GITHOOK_HELPER = `#!/usr/bin/env sh
728
- # Package-relative Directive git-hook helper (AC9.9).
754
+ # Package-relative Directive git-hook helper (AC9.9 / AC9.12 / #5994).
729
755
  # Sourced by pre-commit/pre-push; defines run_directive.
756
+ # F1 order: disable → missing pin (project PM) → pin bin.js.
730
757
  if [ -z "\${REPO_ROOT:-}" ]; then
731
758
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
732
759
  fi
@@ -734,24 +761,20 @@ if [ -z "$REPO_ROOT" ]; then
734
761
  echo "directive hooks: unable to resolve repo root." >&2
735
762
  exit 1
736
763
  fi
737
- RUNTIME_BIN="$REPO_ROOT/${RUNTIME_BIN_REL}"
738
- if [ ! -f "$RUNTIME_BIN" ]; then
739
- echo "directive hooks: missing $RUNTIME_BIN" >&2
740
- echo "Recovery: ${INSTALLATION_RECOVERY_GUIDANCE}" >&2
741
- exit 1
742
- fi
743
764
  run_directive() {
744
- # #3039 / #5543: root local kill-switch — skip CLI spawn when flag is present,
745
- # not a symlink, and not tracked by git (tracked flag must not disable clones).
765
+ # #3039 / #5543 / #5994 F1: disable BEFORE requiring pin bin.js; quiet skip.
746
766
  _kill="$REPO_ROOT/.deft-directive-disable"
747
767
  if [ -f "$_kill" ] && [ ! -L "$_kill" ]; then
748
768
  if ! git -C "$REPO_ROOT" ls-files --error-unmatch -- .deft-directive-disable >/dev/null 2>&1; then
749
- echo "Directive is DISABLED for this project via root .deft-directive-disable (test/local kill-switch)." >&2
750
- echo "Git hooks skipping Directive CLI spawn (#5543 / #3039). Deposit may remain." >&2
751
- echo "Recovery: rm .deft-directive-disable && start a new agent session." >&2
752
769
  return 0
753
770
  fi
754
771
  fi
772
+ RUNTIME_BIN="$REPO_ROOT/${RUNTIME_BIN_REL}"
773
+ if [ ! -f "$RUNTIME_BIN" ]; then
774
+ echo "directive hooks: missing $RUNTIME_BIN" >&2
775
+ echo "${HOST_HOOK_MISSING_PIN_PROJECT_PM_GUIDANCE}" >&2
776
+ exit 1
777
+ fi
755
778
  node "$RUNTIME_BIN" "$@"
756
779
  }
757
780
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-runtime",
3
- "version": "0.129.1",
3
+ "version": "0.129.2",
4
4
  "description": "Directive runtime — pinned product commands, hooks, content, and engine.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://deftai.github.io/directive/",
@@ -23,11 +23,15 @@ const CONTENT_PACKAGE_NAME = "@deftai/directive-content";
23
23
  /** Single remediation when a deposit has no global CLI (#3324 / #3265). */
24
24
  const DEPOSIT_REMEDIATION = "npm i -g @deftai/directive";
25
25
  const SOURCE_BUILD_REMEDIATION = "task build";
26
- /** Missing checkout pin remediation (#5805 Prefer-A Bound P1). */
26
+ /** Missing checkout pin remediation (#5994 / AC9.12): project PM first.
27
+ * pin-bin path never recommends npm i -g (that is DEPOSIT_REMEDIATION only).
28
+ */
27
29
  const PIN_INSTALL_REMEDIATION =
28
- "npm i -g @deftai/directive, then directive install --repair (or package-manager install of @deftai/directive-runtime)";
30
+ "Run npm install or pnpm install (or yarn) in this project (missing node_modules/@deftai/directive-runtime)";
31
+ /** Tip dogfood secondary when project install is not enough (#5805). */
29
32
  const PIN_MATERIALIZE_REMEDIATION = "task materialize:runtime";
30
33
  const PIN_BIN_REL = path.join("node_modules", "@deftai", "directive-runtime", "dist", "bin.js");
34
+ const DISABLE_FLAG = ".deft-directive-disable";
31
35
 
32
36
  /**
33
37
  * cmd.exe command separators / metacharacters. Free-text DEFT_ENGINE_CMD_JSON
@@ -178,6 +182,29 @@ function resolveFrameworkPinBin(projectRoot, opts = {}) {
178
182
  * @param {string} projectRoot
179
183
  * @param {{ fs?: typeof fs, path?: typeof path }} [opts]
180
184
  */
185
+ /**
186
+ * AC9.12 / #5994: untracked root .deft-directive-disable → quiet skip before pin require.
187
+ * @param {string} projectRoot
188
+ * @param {{ fs?: typeof fs, path?: typeof path, spawnSync?: typeof spawnSync }} [opts]
189
+ */
190
+ function isDirectiveDisableActive(projectRoot, opts = {}) {
191
+ const io = opts.fs || fs;
192
+ const pathMod = opts.path || path;
193
+ const spawn = opts.spawnSync || spawnSync;
194
+ if (!projectRoot) return false;
195
+ const killPath = pathMod.join(projectRoot, DISABLE_FLAG);
196
+ try {
197
+ if (!io.existsSync(killPath)) return false;
198
+ if (io.lstatSync(killPath).isSymbolicLink()) return false;
199
+ } catch {
200
+ return false;
201
+ }
202
+ const r = spawn("git", ["-C", projectRoot, "ls-files", "--error-unmatch", "--", DISABLE_FLAG], {
203
+ stdio: ["ignore", "ignore", "ignore"],
204
+ });
205
+ return r.status !== 0;
206
+ }
207
+
181
208
  function hasFrameworkPinBin(projectRoot, opts = {}) {
182
209
  const io = opts.fs || fs;
183
210
  const pin = resolveFrameworkPinBin(projectRoot, opts);
@@ -399,8 +426,14 @@ if (require.main === module) {
399
426
  if (mode === "is-buildable-source") {
400
427
  process.exit(isBuildableSource(process.argv[3] || "") ? 0 : 1);
401
428
  }
429
+ if (mode === "disable-active") {
430
+ process.exit(isDirectiveDisableActive(process.argv[3] || "") ? 0 : 1);
431
+ }
402
432
  if (mode === "pin-bin") {
403
433
  const root = process.argv[3] || "";
434
+ if (isDirectiveDisableActive(root)) {
435
+ process.exit(0);
436
+ }
404
437
  const pin = resolveFrameworkPinBin(root);
405
438
  if (hasFrameworkPinBin(root)) {
406
439
  process.stdout.write(pin);
@@ -433,4 +466,6 @@ module.exports = {
433
466
  SOURCE_BUILD_REMEDIATION,
434
467
  PIN_INSTALL_REMEDIATION,
435
468
  PIN_MATERIALIZE_REMEDIATION,
469
+ isDirectiveDisableActive,
470
+ DISABLE_FLAG,
436
471
  };
@@ -615,5 +615,19 @@ describe("framework pin exclusive invoke (#5805)", () => {
615
615
  const r = spawnSync(process.execPath, [script, "pin-bin", root], { encoding: "utf8" });
616
616
  assert.equal(r.status, 2);
617
617
  assert.match(r.stderr, /missing checkout pin/);
618
+ assert.match(r.stderr, /npm install or pnpm install/);
619
+ });
620
+
621
+ it("disable-active / pin-bin quiet-skip when untracked disable flag present (#5994)", () => {
622
+ const root = pinTempRoot();
623
+ const script = join(__dirname, "engine-invoke.cjs");
624
+ writeFileSync(join(root, ".deft-directive-disable"), "1\n");
625
+ // Ensure git repo so ls-files --error-unmatch can run (untracked → active).
626
+ spawnSync("git", ["init"], { cwd: root, stdio: "ignore" });
627
+ const d = spawnSync(process.execPath, [script, "disable-active", root], { encoding: "utf8" });
628
+ assert.equal(d.status, 0);
629
+ const r = spawnSync(process.execPath, [script, "pin-bin", root], { encoding: "utf8" });
630
+ assert.equal(r.status, 0);
631
+ assert.equal((r.stdout || "").trim(), "");
618
632
  });
619
633
  });
package/tasks/engine.yml CHANGED
@@ -73,6 +73,11 @@ tasks:
73
73
  cmds:
74
74
  - |
75
75
  set -eu
76
+ # #5994 / AC9.12 / F1: disable BEFORE requiring pin bin.js; quiet skip.
77
+ # Untracked root .deft-directive-disable → exit 0 (no pin require, no global install).
78
+ if node "{{.TASKFILE_DIR}}/engine-invoke.cjs" disable-active "{{.DEFT_ROOT}}"; then
79
+ exit 0
80
+ fi
76
81
  # #3324: deposit marker forces is_buildable_source=0 (consumer / OVPR path).
77
82
  is_buildable_source=0
78
83
  if node "{{.TASKFILE_DIR}}/engine-invoke.cjs" is-buildable-source "{{.DEFT_ROOT}}"; then
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env sh
2
- # Package-relative Directive git-hook helper (AC9.9).
2
+ # Package-relative Directive git-hook helper (AC9.9 / AC9.12 / #5994).
3
3
  # Sourced by pre-commit/pre-push; defines run_directive.
4
+ # F1 order: disable → missing pin (project PM) → pin bin.js.
4
5
  if [ -z "${REPO_ROOT:-}" ]; then
5
6
  REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
6
7
  fi
@@ -8,23 +9,19 @@ if [ -z "$REPO_ROOT" ]; then
8
9
  echo "directive hooks: unable to resolve repo root." >&2
9
10
  exit 1
10
11
  fi
11
- RUNTIME_BIN="$REPO_ROOT/node_modules/@deftai/directive-runtime/dist/bin.js"
12
- if [ ! -f "$RUNTIME_BIN" ]; then
13
- echo "directive hooks: missing $RUNTIME_BIN" >&2
14
- echo "Recovery: npm i -g @deftai/directive, then directive install --repair" >&2
15
- exit 1
16
- fi
17
12
  run_directive() {
18
- # #3039 / #5543: root local kill-switch — skip CLI spawn when flag is present,
19
- # not a symlink, and not tracked by git (tracked flag must not disable clones).
13
+ # #3039 / #5543 / #5994 F1: disable BEFORE requiring pin bin.js; quiet skip.
20
14
  _kill="$REPO_ROOT/.deft-directive-disable"
21
15
  if [ -f "$_kill" ] && [ ! -L "$_kill" ]; then
22
16
  if ! git -C "$REPO_ROOT" ls-files --error-unmatch -- .deft-directive-disable >/dev/null 2>&1; then
23
- echo "Directive is DISABLED for this project via root .deft-directive-disable (test/local kill-switch)." >&2
24
- echo "Git hooks skipping Directive CLI spawn (#5543 / #3039). Deposit may remain." >&2
25
- echo "Recovery: rm .deft-directive-disable && start a new agent session." >&2
26
17
  return 0
27
18
  fi
28
19
  fi
20
+ RUNTIME_BIN="$REPO_ROOT/node_modules/@deftai/directive-runtime/dist/bin.js"
21
+ if [ ! -f "$RUNTIME_BIN" ]; then
22
+ echo "directive hooks: missing $RUNTIME_BIN" >&2
23
+ echo "Directive hooks: missing node_modules/@deftai/directive-runtime. Run npm install or pnpm install (or yarn) in this project." >&2
24
+ exit 1
25
+ fi
29
26
  node "$RUNTIME_BIN" "$@"
30
27
  }
@@ -5,8 +5,4 @@ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 1
5
5
  . "$REPO_ROOT/.githooks/_directive-run.sh"
6
6
  run_directive verify:branch --project-root "$REPO_ROOT" || exit $?
7
7
  # Enrolled bound pre-PR arm: sole-stdin (do not redirect shared run_directive to /dev/null)
8
- # Pins older than #5804 lack --push-admit: keep the pre-#5804 hook there (verify:branch only).
9
- _pre_pr_help="$(run_directive pre-pr:run --help </dev/null 2>&1)" || exit $?
10
- case "$_pre_pr_help" in
11
- *--push-admit*) run_directive pre-pr:run --push-admit --project-root "$REPO_ROOT" || exit $? ;;
12
- esac
8
+ run_directive pre-pr:run --push-admit --project-root "$REPO_ROOT" || exit $?
@@ -23,11 +23,15 @@ const CONTENT_PACKAGE_NAME = "@deftai/directive-content";
23
23
  /** Single remediation when a deposit has no global CLI (#3324 / #3265). */
24
24
  const DEPOSIT_REMEDIATION = "npm i -g @deftai/directive";
25
25
  const SOURCE_BUILD_REMEDIATION = "task build";
26
- /** Missing checkout pin remediation (#5805 Prefer-A Bound P1). */
26
+ /** Missing checkout pin remediation (#5994 / AC9.12): project PM first.
27
+ * pin-bin path never recommends npm i -g (that is DEPOSIT_REMEDIATION only).
28
+ */
27
29
  const PIN_INSTALL_REMEDIATION =
28
- "npm i -g @deftai/directive, then directive install --repair (or package-manager install of @deftai/directive-runtime)";
30
+ "Run npm install or pnpm install (or yarn) in this project (missing node_modules/@deftai/directive-runtime)";
31
+ /** Tip dogfood secondary when project install is not enough (#5805). */
29
32
  const PIN_MATERIALIZE_REMEDIATION = "task materialize:runtime";
30
33
  const PIN_BIN_REL = path.join("node_modules", "@deftai", "directive-runtime", "dist", "bin.js");
34
+ const DISABLE_FLAG = ".deft-directive-disable";
31
35
 
32
36
  /**
33
37
  * cmd.exe command separators / metacharacters. Free-text DEFT_ENGINE_CMD_JSON
@@ -178,6 +182,29 @@ function resolveFrameworkPinBin(projectRoot, opts = {}) {
178
182
  * @param {string} projectRoot
179
183
  * @param {{ fs?: typeof fs, path?: typeof path }} [opts]
180
184
  */
185
+ /**
186
+ * AC9.12 / #5994: untracked root .deft-directive-disable → quiet skip before pin require.
187
+ * @param {string} projectRoot
188
+ * @param {{ fs?: typeof fs, path?: typeof path, spawnSync?: typeof spawnSync }} [opts]
189
+ */
190
+ function isDirectiveDisableActive(projectRoot, opts = {}) {
191
+ const io = opts.fs || fs;
192
+ const pathMod = opts.path || path;
193
+ const spawn = opts.spawnSync || spawnSync;
194
+ if (!projectRoot) return false;
195
+ const killPath = pathMod.join(projectRoot, DISABLE_FLAG);
196
+ try {
197
+ if (!io.existsSync(killPath)) return false;
198
+ if (io.lstatSync(killPath).isSymbolicLink()) return false;
199
+ } catch {
200
+ return false;
201
+ }
202
+ const r = spawn("git", ["-C", projectRoot, "ls-files", "--error-unmatch", "--", DISABLE_FLAG], {
203
+ stdio: ["ignore", "ignore", "ignore"],
204
+ });
205
+ return r.status !== 0;
206
+ }
207
+
181
208
  function hasFrameworkPinBin(projectRoot, opts = {}) {
182
209
  const io = opts.fs || fs;
183
210
  const pin = resolveFrameworkPinBin(projectRoot, opts);
@@ -399,8 +426,14 @@ if (require.main === module) {
399
426
  if (mode === "is-buildable-source") {
400
427
  process.exit(isBuildableSource(process.argv[3] || "") ? 0 : 1);
401
428
  }
429
+ if (mode === "disable-active") {
430
+ process.exit(isDirectiveDisableActive(process.argv[3] || "") ? 0 : 1);
431
+ }
402
432
  if (mode === "pin-bin") {
403
433
  const root = process.argv[3] || "";
434
+ if (isDirectiveDisableActive(root)) {
435
+ process.exit(0);
436
+ }
404
437
  const pin = resolveFrameworkPinBin(root);
405
438
  if (hasFrameworkPinBin(root)) {
406
439
  process.stdout.write(pin);
@@ -433,4 +466,6 @@ module.exports = {
433
466
  SOURCE_BUILD_REMEDIATION,
434
467
  PIN_INSTALL_REMEDIATION,
435
468
  PIN_MATERIALIZE_REMEDIATION,
469
+ isDirectiveDisableActive,
470
+ DISABLE_FLAG,
436
471
  };
@@ -615,5 +615,19 @@ describe("framework pin exclusive invoke (#5805)", () => {
615
615
  const r = spawnSync(process.execPath, [script, "pin-bin", root], { encoding: "utf8" });
616
616
  assert.equal(r.status, 2);
617
617
  assert.match(r.stderr, /missing checkout pin/);
618
+ assert.match(r.stderr, /npm install or pnpm install/);
619
+ });
620
+
621
+ it("disable-active / pin-bin quiet-skip when untracked disable flag present (#5994)", () => {
622
+ const root = pinTempRoot();
623
+ const script = join(__dirname, "engine-invoke.cjs");
624
+ writeFileSync(join(root, ".deft-directive-disable"), "1\n");
625
+ // Ensure git repo so ls-files --error-unmatch can run (untracked → active).
626
+ spawnSync("git", ["init"], { cwd: root, stdio: "ignore" });
627
+ const d = spawnSync(process.execPath, [script, "disable-active", root], { encoding: "utf8" });
628
+ assert.equal(d.status, 0);
629
+ const r = spawnSync(process.execPath, [script, "pin-bin", root], { encoding: "utf8" });
630
+ assert.equal(r.status, 0);
631
+ assert.equal((r.stdout || "").trim(), "");
618
632
  });
619
633
  });
@@ -73,6 +73,11 @@ tasks:
73
73
  cmds:
74
74
  - |
75
75
  set -eu
76
+ # #5994 / AC9.12 / F1: disable BEFORE requiring pin bin.js; quiet skip.
77
+ # Untracked root .deft-directive-disable → exit 0 (no pin require, no global install).
78
+ if node "{{.TASKFILE_DIR}}/engine-invoke.cjs" disable-active "{{.DEFT_ROOT}}"; then
79
+ exit 0
80
+ fi
76
81
  # #3324: deposit marker forces is_buildable_source=0 (consumer / OVPR path).
77
82
  is_buildable_source=0
78
83
  if node "{{.TASKFILE_DIR}}/engine-invoke.cjs" is-buildable-source "{{.DEFT_ROOT}}"; then