@deftai/directive-core 0.107.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/architecture/sor-preflight.js +2 -0
  2. package/dist/check/cached-orchestrator.js +28 -3
  3. package/dist/check/named-cause.js +18 -3
  4. package/dist/codebase/provider.js +7 -1
  5. package/dist/delivery-attempt/handoff.js +1 -1
  6. package/dist/design-critique/citation-grammar.d.ts +53 -0
  7. package/dist/design-critique/citation-grammar.js +303 -0
  8. package/dist/design-critique/completed-arc-record.d.ts +62 -0
  9. package/dist/design-critique/completed-arc-record.js +232 -0
  10. package/dist/freshness/bind.d.ts +32 -3
  11. package/dist/freshness/bind.js +58 -12
  12. package/dist/hooks/classify/host-session-identity.d.ts +76 -0
  13. package/dist/hooks/classify/host-session-identity.js +470 -0
  14. package/dist/hooks/classify/index.d.ts +3 -2
  15. package/dist/hooks/classify/index.js +3 -2
  16. package/dist/hooks/classify/paths.d.ts +7 -0
  17. package/dist/hooks/classify/paths.js +35 -0
  18. package/dist/hooks/classify/stdin.d.ts +5 -0
  19. package/dist/hooks/classify/stdin.js +27 -0
  20. package/dist/hooks/dispatcher.d.ts +52 -1
  21. package/dist/hooks/dispatcher.js +538 -45
  22. package/dist/hooks/fixtures/cases.d.ts +16 -2
  23. package/dist/hooks/fixtures/cases.js +175 -0
  24. package/dist/hooks/scope.js +3 -1
  25. package/dist/init-deposit/agent-hooks.d.ts +2 -1
  26. package/dist/init-deposit/agent-hooks.js +8 -1
  27. package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
  28. package/dist/init-deposit/hook-runtime-travel.js +269 -0
  29. package/dist/intake/clause-derivation.js +9 -3
  30. package/dist/intake/issue-ingest.js +62 -2
  31. package/dist/intake/platform-capabilities.d.ts +9 -2
  32. package/dist/intake/platform-capabilities.js +38 -9
  33. package/dist/orphan-active/evaluate.d.ts +25 -0
  34. package/dist/orphan-active/evaluate.js +193 -128
  35. package/dist/orphan-active/index.d.ts +1 -0
  36. package/dist/orphan-active/index.js +1 -0
  37. package/dist/orphan-active/issue-state.d.ts +121 -0
  38. package/dist/orphan-active/issue-state.js +278 -0
  39. package/dist/platform/cursor-managed-runtime.d.ts +95 -0
  40. package/dist/platform/cursor-managed-runtime.js +241 -0
  41. package/dist/platform/index.d.ts +1 -0
  42. package/dist/platform/index.js +1 -0
  43. package/dist/platform/platform-capabilities.d.ts +23 -1
  44. package/dist/platform/platform-capabilities.js +55 -15
  45. package/dist/policy/ceremony-dial.js +5 -9
  46. package/dist/policy/host-hooks.js +4 -9
  47. package/dist/policy/org-force-on-migration.js +5 -10
  48. package/dist/policy/plan-extensions.d.ts +14 -2
  49. package/dist/policy/plan-extensions.js +24 -3
  50. package/dist/policy/product-signal.js +5 -10
  51. package/dist/policy/require-human-merge.js +5 -9
  52. package/dist/policy/resolve.js +77 -16
  53. package/dist/policy/value-feedback.js +9 -18
  54. package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
  55. package/dist/pr-closeout-attestable/evaluate.js +306 -0
  56. package/dist/pr-closeout-attestable/index.d.ts +2 -0
  57. package/dist/pr-closeout-attestable/index.js +2 -0
  58. package/dist/pr-protected-issues/gh.d.ts +6 -2
  59. package/dist/pr-protected-issues/gh.js +7 -3
  60. package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
  61. package/dist/pr-wait-mergeable/cascade.js +21 -1
  62. package/dist/pr-wait-mergeable/types.d.ts +2 -0
  63. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  64. package/dist/pr-wait-mergeable/wrappers.js +11 -0
  65. package/dist/preflight/evaluate.d.ts +15 -0
  66. package/dist/preflight/evaluate.js +29 -3
  67. package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
  68. package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
  69. package/dist/product-first-done-gate/evaluate.js +17 -7
  70. package/dist/release/cli-drift-report.d.ts +68 -0
  71. package/dist/release/cli-drift-report.js +189 -0
  72. package/dist/release/index.d.ts +1 -0
  73. package/dist/release/index.js +1 -0
  74. package/dist/release/issue-state-fetch.d.ts +5 -3
  75. package/dist/release/issue-state-fetch.js +86 -21
  76. package/dist/release/native-steps.js +1 -0
  77. package/dist/release/pipeline.js +19 -0
  78. package/dist/release/types.d.ts +14 -0
  79. package/dist/render/constants.d.ts +4 -0
  80. package/dist/render/constants.js +11 -8
  81. package/dist/render/export-spec.js +31 -5
  82. package/dist/render/index.d.ts +1 -1
  83. package/dist/render/index.js +1 -1
  84. package/dist/render/prd-render.d.ts +4 -1
  85. package/dist/render/prd-render.js +82 -23
  86. package/dist/render/project-render.js +14 -7
  87. package/dist/render/scope-outlook.d.ts +2 -0
  88. package/dist/render/scope-outlook.js +3 -0
  89. package/dist/render/spec-render.js +2 -2
  90. package/dist/resolution/package-manager.d.ts +33 -4
  91. package/dist/resolution/package-manager.js +210 -9
  92. package/dist/review-monitor/constants.d.ts +7 -0
  93. package/dist/review-monitor/constants.js +7 -0
  94. package/dist/review-monitor/github-lease.d.ts +98 -1
  95. package/dist/review-monitor/github-lease.js +182 -1
  96. package/dist/review-monitor/lease-comment.d.ts +36 -0
  97. package/dist/review-monitor/lease-comment.js +103 -2
  98. package/dist/scm/call.js +12 -3
  99. package/dist/scm/design-critique-chip.d.ts +1 -0
  100. package/dist/scm/design-critique-chip.js +22 -6
  101. package/dist/scm/gh-rest.d.ts +9 -1
  102. package/dist/scm/gh-rest.js +103 -0
  103. package/dist/scm/readiness.d.ts +2 -0
  104. package/dist/scm/readiness.js +28 -4
  105. package/dist/scope/project-definition-sync.js +14 -7
  106. package/dist/session/git.d.ts +24 -0
  107. package/dist/session/git.js +138 -2
  108. package/dist/session/occupancy.d.ts +238 -12
  109. package/dist/session/occupancy.js +815 -29
  110. package/dist/session/ritual-sentinel.d.ts +5 -0
  111. package/dist/session/ritual-sentinel.js +5 -0
  112. package/dist/session/session-ready.d.ts +5 -1
  113. package/dist/session/session-ready.js +98 -8
  114. package/dist/session/session-start.d.ts +5 -1
  115. package/dist/session/session-start.js +32 -17
  116. package/dist/session/toolchain-preflight.d.ts +13 -3
  117. package/dist/session/toolchain-preflight.js +93 -18
  118. package/dist/session/verify-session-ritual.d.ts +11 -0
  119. package/dist/session/verify-session-ritual.js +60 -15
  120. package/dist/slice/constants.d.ts +1 -1
  121. package/dist/spec-authority/constants.d.ts +10 -2
  122. package/dist/spec-authority/constants.js +53 -8
  123. package/dist/spec-authority/resolver.d.ts +3 -0
  124. package/dist/spec-authority/resolver.js +55 -7
  125. package/dist/subprocess/max-buffer.d.ts +14 -0
  126. package/dist/subprocess/max-buffer.js +15 -0
  127. package/dist/swarm/complete-cohort.d.ts +2 -0
  128. package/dist/swarm/complete-cohort.js +21 -13
  129. package/dist/swarm/index.d.ts +1 -0
  130. package/dist/swarm/index.js +1 -0
  131. package/dist/swarm/launch-cli.js +53 -33
  132. package/dist/swarm/launch.d.ts +8 -0
  133. package/dist/swarm/launch.js +63 -43
  134. package/dist/swarm/pre-dispatch-cli.js +2 -1
  135. package/dist/swarm/pre-dispatch.js +10 -1
  136. package/dist/swarm/subagent-status-dir.d.ts +27 -0
  137. package/dist/swarm/subagent-status-dir.js +42 -0
  138. package/dist/swarm/worktrees.d.ts +17 -3
  139. package/dist/swarm/worktrees.js +48 -4
  140. package/dist/triage/scope/mutations-core.d.ts +2 -6
  141. package/dist/triage/scope/mutations-core.js +23 -55
  142. package/dist/triage/scope-drift/add-ignore.js +5 -39
  143. package/dist/triage/subscribe/index.d.ts +2 -6
  144. package/dist/triage/subscribe/index.js +15 -65
  145. package/dist/triage/welcome/writers.js +15 -74
  146. package/dist/vbrief-build/index.d.ts +2 -1
  147. package/dist/vbrief-build/index.js +2 -1
  148. package/dist/vbrief-build/parity-scenarios.js +5 -5
  149. package/dist/vbrief-build/project-definition-io.d.ts +63 -3
  150. package/dist/vbrief-build/project-definition-io.js +357 -31
  151. package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
  152. package/dist/vbrief-build/project-definition-mutation.js +43 -0
  153. package/dist/vbrief-validate/conformance.d.ts +1 -0
  154. package/dist/vbrief-validate/conformance.js +85 -8
  155. package/dist/vbrief-validate/main.js +2 -0
  156. package/dist/vbrief-validate/precutover.js +5 -10
  157. package/dist/verify-ac/clauses.d.ts +62 -2
  158. package/dist/verify-ac/clauses.js +183 -111
  159. package/dist/verify-ac/index.d.ts +1 -1
  160. package/dist/verify-ac/index.js +1 -1
  161. package/dist/verify-env/node-runtime.d.ts +8 -4
  162. package/dist/verify-env/node-runtime.js +9 -6
  163. package/dist/verify-env/toolchain-check.d.ts +35 -3
  164. package/dist/verify-env/toolchain-check.js +155 -36
  165. package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
  166. package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
  167. package/package.json +7 -3
@@ -4,15 +4,16 @@
4
4
  * Cases are pure data: raw stdin or structured payload → expected classification.
5
5
  * Core tests and CLI hook-dispatch tests import this module (shared corpus collapse).
6
6
  */
7
+ import type { ExactLifecycleVerb, HookHostIdentityStatus } from "../classify/host-session-identity.js";
7
8
  import type { ClassifyHookHost, HookWriteIntent } from "../classify/types.js";
8
9
  export type FixtureOs = "win32" | "posix";
9
- export type FixtureToolFamily = "Write" | "ApplyPatch" | "Task" | "StrReplace" | "other";
10
+ export type FixtureToolFamily = "Write" | "ApplyPatch" | "Task" | "StrReplace" | "Shell" | "other";
10
11
  export interface HookFixtureCase {
11
12
  readonly id: string;
12
13
  readonly host: ClassifyHookHost;
13
14
  readonly os: FixtureOs;
14
15
  readonly tool: FixtureToolFamily;
15
- /** Closed-issue tags for the regression class this case freezes. */
16
+ /** Issue/regression tags for the behavior class this case freezes. */
16
17
  readonly regression: readonly string[];
17
18
  /** Structured payload when the host sends JSON. */
18
19
  readonly payload?: unknown;
@@ -25,6 +26,19 @@ export interface HookFixtureCase {
25
26
  /** When raw is set: after parseHookStdin. */
26
27
  readonly stdinEmpty?: boolean;
27
28
  readonly parseFailed?: boolean;
29
+ /** Cooperative payload owner resolution expected for #3611 cases. */
30
+ readonly hostIdentity?: {
31
+ readonly status: HookHostIdentityStatus;
32
+ readonly sessionId: string | null;
33
+ };
34
+ /** Exact lifecycle classification/rewrite expected for #3611 shell cases. */
35
+ readonly lifecycle?: {
36
+ readonly verb: ExactLifecycleVerb | null;
37
+ readonly requestedSessionId: string;
38
+ readonly resultKind: "rewrite" | "conflict" | null;
39
+ readonly rewrittenCommand?: string;
40
+ readonly updatedInput?: Readonly<Record<string, unknown>>;
41
+ };
28
42
  };
29
43
  readonly notes?: string;
30
44
  }
@@ -527,6 +527,181 @@ export const HOOK_FIXTURE_CASES = [
527
527
  writeTargetPath: "C:\\Repos\\proj\\src\\edit-me.ts",
528
528
  },
529
529
  },
530
+ // --- Cooperative host-session identity and exact lifecycle rewrite (#3611) ---
531
+ {
532
+ id: "codex-posix-shell-lifecycle-rewrite",
533
+ host: "codex",
534
+ os: "posix",
535
+ tool: "Shell",
536
+ regression: ["#3611"],
537
+ payload: {
538
+ tool_name: "Bash",
539
+ session_id: "session-a",
540
+ tool_input: {
541
+ command: "deft session:start --rearm",
542
+ description: "Re-arm the mutation session",
543
+ },
544
+ },
545
+ expected: {
546
+ toolName: "Bash",
547
+ writeIntent: "shell",
548
+ writeTargetPath: null,
549
+ hostIdentity: {
550
+ status: "ok",
551
+ sessionId: "host:codex:v1:c2Vzc2lvbi1h",
552
+ },
553
+ lifecycle: {
554
+ verb: "session:start",
555
+ requestedSessionId: "host:codex:v1:c2Vzc2lvbi1h",
556
+ resultKind: "rewrite",
557
+ rewrittenCommand: "deft session:start --rearm --session-id=host:codex:v1:c2Vzc2lvbi1h",
558
+ updatedInput: {
559
+ command: "deft session:start --rearm --session-id=host:codex:v1:c2Vzc2lvbi1h",
560
+ description: "Re-arm the mutation session",
561
+ },
562
+ },
563
+ },
564
+ },
565
+ {
566
+ id: "codex-posix-shell-lifecycle-owner-conflict",
567
+ host: "codex",
568
+ os: "posix",
569
+ tool: "Shell",
570
+ regression: ["#3611"],
571
+ payload: {
572
+ tool_name: "Bash",
573
+ session_id: "session-a",
574
+ tool_input: {
575
+ command: "deft session:start --session-id=host:codex:v1:b3RoZXI",
576
+ },
577
+ },
578
+ expected: {
579
+ toolName: "Bash",
580
+ writeIntent: "shell",
581
+ writeTargetPath: null,
582
+ hostIdentity: {
583
+ status: "ok",
584
+ sessionId: "host:codex:v1:c2Vzc2lvbi1h",
585
+ },
586
+ lifecycle: {
587
+ verb: "session:start",
588
+ requestedSessionId: "host:codex:v1:c2Vzc2lvbi1h",
589
+ resultKind: "conflict",
590
+ },
591
+ },
592
+ },
593
+ {
594
+ id: "codex-posix-shell-lifecycle-ambiguous",
595
+ host: "codex",
596
+ os: "posix",
597
+ tool: "Shell",
598
+ regression: ["#3611"],
599
+ payload: {
600
+ tool_name: "Bash",
601
+ session_id: "session-a",
602
+ tool_input: { command: "deft session:start && echo unsafe" },
603
+ },
604
+ expected: {
605
+ toolName: "Bash",
606
+ writeIntent: "shell",
607
+ writeTargetPath: null,
608
+ hostIdentity: {
609
+ status: "ok",
610
+ sessionId: "host:codex:v1:c2Vzc2lvbi1h",
611
+ },
612
+ lifecycle: {
613
+ verb: null,
614
+ requestedSessionId: "host:codex:v1:c2Vzc2lvbi1h",
615
+ resultKind: null,
616
+ },
617
+ },
618
+ },
619
+ {
620
+ id: "cursor-posix-shell-identity-conflict",
621
+ host: "cursor",
622
+ os: "posix",
623
+ tool: "Shell",
624
+ regression: ["#3611"],
625
+ payload: {
626
+ tool_name: "Shell",
627
+ conversation_id: "conversation-a",
628
+ session_id: "conversation-b",
629
+ tool_input: { command: "task session:ready" },
630
+ },
631
+ expected: {
632
+ toolName: "Shell",
633
+ writeIntent: "shell",
634
+ writeTargetPath: null,
635
+ hostIdentity: { status: "conflict", sessionId: null },
636
+ },
637
+ },
638
+ {
639
+ // #3599 item 5: the refresh verb is only useful if the id it renews is the
640
+ // one later hook processes compare against. #3611's bridge supplies that.
641
+ id: "cursor-posix-shell-occupancy-heartbeat-rewrite",
642
+ host: "cursor",
643
+ os: "posix",
644
+ tool: "Shell",
645
+ regression: ["#3599", "#3611"],
646
+ payload: {
647
+ tool_name: "Bash",
648
+ conversation_id: "session-a",
649
+ tool_input: { command: "task occupancy:heartbeat" },
650
+ },
651
+ expected: {
652
+ toolName: "Bash",
653
+ writeIntent: "shell",
654
+ writeTargetPath: null,
655
+ hostIdentity: {
656
+ status: "ok",
657
+ sessionId: "host:cursor:v1:c2Vzc2lvbi1h",
658
+ },
659
+ lifecycle: {
660
+ verb: "occupancy:heartbeat",
661
+ requestedSessionId: "host:cursor:v1:c2Vzc2lvbi1h",
662
+ resultKind: "rewrite",
663
+ rewrittenCommand: "task occupancy:heartbeat -- --session-id=host:cursor:v1:c2Vzc2lvbi1h",
664
+ updatedInput: {
665
+ command: "task occupancy:heartbeat -- --session-id=host:cursor:v1:c2Vzc2lvbi1h",
666
+ },
667
+ },
668
+ },
669
+ },
670
+ {
671
+ id: "claude-posix-shell-identity-missing",
672
+ host: "claude",
673
+ os: "posix",
674
+ tool: "Shell",
675
+ regression: ["#3611"],
676
+ payload: {
677
+ tool_name: "Bash",
678
+ tool_input: { command: "deft occupancy:release" },
679
+ },
680
+ expected: {
681
+ toolName: "Bash",
682
+ writeIntent: "shell",
683
+ writeTargetPath: null,
684
+ hostIdentity: { status: "missing", sessionId: null },
685
+ },
686
+ },
687
+ {
688
+ id: "claude-posix-shell-identity-malformed",
689
+ host: "claude",
690
+ os: "posix",
691
+ tool: "Shell",
692
+ regression: ["#3611"],
693
+ payload: {
694
+ tool_name: "Bash",
695
+ session_id: " malformed ",
696
+ tool_input: { command: "deft occupancy:release" },
697
+ },
698
+ expected: {
699
+ toolName: "Bash",
700
+ writeIntent: "shell",
701
+ writeTargetPath: null,
702
+ hostIdentity: { status: "invalid", sessionId: null },
703
+ },
704
+ },
530
705
  // --- Regression: empty / missing tool name ---
531
706
  {
532
707
  id: "cursor-posix-stdin-empty",
@@ -27,7 +27,9 @@ export function inspectActiveScope(projectRoot) {
27
27
  candidates.sort();
28
28
  let firstRejection = null;
29
29
  for (const candidate of candidates) {
30
- const result = evaluate(candidate);
30
+ // #3736: origin freshness remains fail-closed at explicit xbrief:preflight.
31
+ // The host mutation path must stay local and render before its effective timeout.
32
+ const result = evaluate(candidate, { skipOriginFreshness: true });
31
33
  if (result.exitCode === 0) {
32
34
  return { ready: true, path: candidate, message: result.message };
33
35
  }
@@ -1,6 +1,7 @@
1
1
  import type { HookHost } from "../hooks/dispatcher.js";
2
2
  import { type HostHooksPolicy } from "../policy/host-hooks.js";
3
3
  import type { InitDepositIo } from "./constants.js";
4
+ import { type HookRuntimeTravelSeams } from "./hook-runtime-travel.js";
4
5
  export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
5
6
  export declare const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
6
7
  export declare const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
@@ -41,7 +42,7 @@ export interface AgentHookDepositResult {
41
42
  readonly changedPaths: AgentHookPath[];
42
43
  }
43
44
  /** Merge Directive-owned project hook entries without replacing user configuration. */
44
- export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo, hostHooksPolicy?: HostHooksPolicy): AgentHookDepositResult;
45
+ export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo, hostHooksPolicy?: HostHooksPolicy, travelSeams?: HookRuntimeTravelSeams): AgentHookDepositResult;
45
46
  /** Read-only registration probe shared by verify and doctor. */
46
47
  export declare function inspectAgentHookDeposit(projectRoot: string, hostHooksPolicy?: HostHooksPolicy): AgentHookInspection[];
47
48
  //# sourceMappingURL=agent-hooks.d.ts.map
@@ -4,6 +4,7 @@ import { assertDepositContained } from "../deposit/contain.js";
4
4
  import { containedRemove, containedRename, containedWrite } from "../fs/contained-write.js";
5
5
  import { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
6
6
  import { isHostHookDepositEnabled, loadHostHooksPolicyFromProject, } from "../policy/host-hooks.js";
7
+ import { inspectHookRuntimeTravel } from "./hook-runtime-travel.js";
7
8
  export { DIRECT_WRITE_HOOK_MATCHER, MCP_HOOK_MATCHER, SHELL_HOOK_MATCHER, SPAWN_HOOK_MATCHER, } from "../hooks/tools.js";
8
9
  export const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
9
10
  export const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
@@ -252,7 +253,7 @@ function writeJsonIfChanged(projectRoot, path, payload, kind) {
252
253
  return true;
253
254
  }
254
255
  /** Merge Directive-owned project hook entries without replacing user configuration. */
255
- export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefined }, hostHooksPolicy = loadHostHooksPolicyFromProject(projectRoot)) {
256
+ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefined }, hostHooksPolicy = loadHostHooksPolicyFromProject(projectRoot), travelSeams = {}) {
256
257
  const changedPaths = [];
257
258
  const strippedPaths = [];
258
259
  const definitions = [
@@ -332,6 +333,12 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
332
333
  if (changedPaths.length === 0 && strippedPaths.length === 0 && adaptersRemoved === 0) {
333
334
  io.printf("Directive agent hooks already current.\n");
334
335
  }
336
+ // #3785: the registration is trackable and the deposit that implements it is
337
+ // born-ignored, so warn when a clone would inherit the fence without a way to
338
+ // obtain the runtime it names.
339
+ const travel = inspectHookRuntimeTravel(projectRoot, definitions.map((definition) => ({ host: definition.host, path: definition.path })), hostHooksPolicy, travelSeams);
340
+ if (travel.warning !== null)
341
+ io.printf(`${travel.warning}\n`);
335
342
  return {
336
343
  changed: changedPaths.length + strippedPaths.length + adaptersRemoved > 0,
337
344
  changedPaths: [...changedPaths, ...strippedPaths],
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Hook-runtime travel warning (#3785).
3
+ *
4
+ * Directive keeps the agent-hook registration files trackable by design and
5
+ * born-ignores the deposit that implements them. When a registration travels
6
+ * with the tree and nothing in that tree lets a clone obtain `deft-hook`, the
7
+ * registration arrives and the runtime does not. A fresh clone, CI runner, or
8
+ * container without a global install then fail-closes every mutation on an
9
+ * opaque exit 127, and no Directive code runs to explain it.
10
+ *
11
+ * "Travels" means tracked OR trackable-and-not-ignored. A first `deft init`
12
+ * writes the registration unstaged, so a tracked-only probe would go quiet on
13
+ * exactly the run that precedes the commit carrying the fence into every clone.
14
+ *
15
+ * The committed `package.json` dependency on `@deftai/directive` (#2264) is the
16
+ * existing reconstitution anchor. With it a clone can install the runtime the
17
+ * registration names; without it the registration names a command no clone can
18
+ * host. The anchor is read from the commit, not from the working tree or the
19
+ * index: a consumer can commit the generated registration and leave the
20
+ * manifest merely present, staged, or edited, and that is the case that strands
21
+ * the clone.
22
+ *
23
+ * Warn-only. Refusing `deft init` / `deft update` on this condition would be
24
+ * the same lockout from the other side, so the deposit still writes and
25
+ * reports. Absence of the runtime stays a deny — fail-open-on-absence was
26
+ * refuted as unimplementable and as a bypass primitive (#3156).
27
+ *
28
+ * Refs #3785, #3736, #3571, #2752, #2264, #3156.
29
+ */
30
+ import { type HookHost, type HostHooksPolicy } from "../policy/host-hooks.js";
31
+ import { type PinReadResult } from "../resolution/pin.js";
32
+ /**
33
+ * Hosts whose registration is deposited `failClosed: true`. Only these turn a
34
+ * missing runtime into a denial; the rest fail open and merely lose the gate.
35
+ */
36
+ export declare const FAIL_CLOSED_HOOK_HOSTS: readonly HookHost[];
37
+ /** Manifest carrying the committed runtime anchor. */
38
+ export declare const RUNTIME_ANCHOR_MANIFEST = "package.json";
39
+ /**
40
+ * Spec prefixes that name a location instead of a release. Each resolves
41
+ * against something outside the tree -- a path, a symlink target, a workspace
42
+ * member -- so the clone that receives the registration cannot install from it.
43
+ */
44
+ export declare const LOCATION_SPEC_PREFIXES: readonly ["file:", "link:", "portal:", "workspace:", "git+file:"];
45
+ export type GitLsFilesProbe = (projectDir: string, paths: readonly string[]) => string | null;
46
+ /** Contents of a path as committed at `HEAD`, or null when there is none. */
47
+ export type CommittedFileProbe = (projectDir: string, path: string) => string | null;
48
+ export interface HookRuntimeTravelSeams {
49
+ readonly gitLsFiles?: GitLsFilesProbe;
50
+ readonly readCommittedFile?: CommittedFileProbe;
51
+ readonly readPin?: (projectRoot: string) => PinReadResult;
52
+ }
53
+ /** One deposited registration considered by the probe. */
54
+ export interface HookRegistrationRef {
55
+ readonly host: HookHost;
56
+ readonly path: string;
57
+ }
58
+ export interface HookRuntimeTravelResult {
59
+ /** Enabled registrations that travel with this repository. */
60
+ readonly travelingRegistrations: readonly string[];
61
+ /** Subset of {@link travelingRegistrations} deposited fail-closed. */
62
+ readonly failClosedRegistrations: readonly string[];
63
+ /** A committed manifest declares `@deftai/directive`, so a clone can install it. */
64
+ readonly runtimeTravels: boolean;
65
+ /** Null when the pairing is safe or git could not answer. */
66
+ readonly warning: string | null;
67
+ }
68
+ /**
69
+ * Report whether any enabled agent-hook registration travels with this tree
70
+ * while no manifest travelling alongside it lets a clone install the runtime it
71
+ * names.
72
+ *
73
+ * Disabled hosts are excluded: their registration is stripped, so a leftover
74
+ * copy cannot deny anything.
75
+ */
76
+ export declare function inspectHookRuntimeTravel(projectRoot: string, registrations: readonly HookRegistrationRef[], hostHooksPolicy: HostHooksPolicy, seams?: HookRuntimeTravelSeams): HookRuntimeTravelResult;
77
+ //# sourceMappingURL=hook-runtime-travel.d.ts.map
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Hook-runtime travel warning (#3785).
3
+ *
4
+ * Directive keeps the agent-hook registration files trackable by design and
5
+ * born-ignores the deposit that implements them. When a registration travels
6
+ * with the tree and nothing in that tree lets a clone obtain `deft-hook`, the
7
+ * registration arrives and the runtime does not. A fresh clone, CI runner, or
8
+ * container without a global install then fail-closes every mutation on an
9
+ * opaque exit 127, and no Directive code runs to explain it.
10
+ *
11
+ * "Travels" means tracked OR trackable-and-not-ignored. A first `deft init`
12
+ * writes the registration unstaged, so a tracked-only probe would go quiet on
13
+ * exactly the run that precedes the commit carrying the fence into every clone.
14
+ *
15
+ * The committed `package.json` dependency on `@deftai/directive` (#2264) is the
16
+ * existing reconstitution anchor. With it a clone can install the runtime the
17
+ * registration names; without it the registration names a command no clone can
18
+ * host. The anchor is read from the commit, not from the working tree or the
19
+ * index: a consumer can commit the generated registration and leave the
20
+ * manifest merely present, staged, or edited, and that is the case that strands
21
+ * the clone.
22
+ *
23
+ * Warn-only. Refusing `deft init` / `deft update` on this condition would be
24
+ * the same lockout from the other side, so the deposit still writes and
25
+ * reports. Absence of the runtime stays a deny — fail-open-on-absence was
26
+ * refuted as unimplementable and as a bypass primitive (#3156).
27
+ *
28
+ * Refs #3785, #3736, #3571, #2752, #2264, #3156.
29
+ */
30
+ import { execFileSync } from "node:child_process";
31
+ import { disableHostHooksInvocation, isHostHookDepositEnabled, } from "../policy/host-hooks.js";
32
+ import { PIN_DEPENDENCY_NAME, readPin } from "../resolution/pin.js";
33
+ /**
34
+ * Hosts whose registration is deposited `failClosed: true`. Only these turn a
35
+ * missing runtime into a denial; the rest fail open and merely lose the gate.
36
+ */
37
+ export const FAIL_CLOSED_HOOK_HOSTS = ["cursor"];
38
+ /** Manifest carrying the committed runtime anchor. */
39
+ export const RUNTIME_ANCHOR_MANIFEST = "package.json";
40
+ /**
41
+ * Spec prefixes that name a location instead of a release. Each resolves
42
+ * against something outside the tree -- a path, a symlink target, a workspace
43
+ * member -- so the clone that receives the registration cannot install from it.
44
+ */
45
+ export const LOCATION_SPEC_PREFIXES = [
46
+ "file:",
47
+ "link:",
48
+ "portal:",
49
+ "workspace:",
50
+ "git+file:",
51
+ ];
52
+ /** Prefixes any clone can fetch: a registry alias or a remote repository. */
53
+ const REMOTE_SPEC_PREFIXES = [
54
+ "npm:",
55
+ "http:",
56
+ "https:",
57
+ "git:",
58
+ "git+http:",
59
+ "git+https:",
60
+ "git+ssh:",
61
+ "github:",
62
+ "gitlab:",
63
+ "bitbucket:",
64
+ ];
65
+ /**
66
+ * Whether a clone could install `deft-hook` from this spec.
67
+ *
68
+ * Allowlist, not denylist. Location forms are open-ended -- `file:`,
69
+ * `git+file:`, `../directive`, `/opt/directive`, `C:\src\directive`, `~/dev` --
70
+ * so enumerating them leaves the next shape uncovered, and an uncovered shape
71
+ * silences the warning. Anything that is not recognisably a registry release or
72
+ * a fetchable remote is therefore treated as not travelling. A bare
73
+ * `owner/repo` shorthand is refused on the same rule: it warns where the
74
+ * runtime might in fact arrive, which is the harmless direction for a
75
+ * warn-only probe.
76
+ */
77
+ function reconstitutesForAClone(rawSpec) {
78
+ const spec = rawSpec.trim();
79
+ if (spec.length === 0)
80
+ return false;
81
+ const lower = spec.toLowerCase();
82
+ if (LOCATION_SPEC_PREFIXES.some((prefix) => lower.startsWith(prefix)))
83
+ return false;
84
+ if (REMOTE_SPEC_PREFIXES.some((prefix) => lower.startsWith(prefix)))
85
+ return true;
86
+ // Bare paths: relative, home-anchored, POSIX-absolute, or a Windows drive.
87
+ if (/^[.~/\\]/.test(spec) || /^[a-z]:[\\/]/i.test(spec))
88
+ return false;
89
+ // Anything else with a separator is a path or a repository shorthand, not a
90
+ // version, a range, or a dist-tag.
91
+ return !(spec.includes("/") || spec.includes("\\"));
92
+ }
93
+ function defaultGitLsFiles(projectDir, paths) {
94
+ try {
95
+ // Ask git rather than looking for `.git`: a project deposited into a
96
+ // subdirectory of a repository has no `.git` of its own, yet its
97
+ // registration is trackable by the parent. `ls-files` answers from any
98
+ // depth, relative to this directory, and declines outside a repository.
99
+ //
100
+ // `--cached` reports what is in the index, `--others --exclude-standard`
101
+ // what a first `deft init` just wrote and the next `git add` would sweep
102
+ // in; ignored paths are omitted, since they cannot reach a clone.
103
+ return execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "--", ...paths], {
104
+ cwd: projectDir,
105
+ encoding: "utf8",
106
+ stdio: ["ignore", "pipe", "ignore"],
107
+ windowsHide: true,
108
+ });
109
+ }
110
+ catch {
111
+ // git absent, or not a repository: whether these paths travel has no answer
112
+ // here, so the caller stays silent rather than guessing.
113
+ return null;
114
+ }
115
+ }
116
+ /** Paths that travel, as reported by git, or null when git could not answer. */
117
+ function travelingPaths(output) {
118
+ if (output === null)
119
+ return null;
120
+ return new Set(output
121
+ .split("\n")
122
+ .map((line) => line.trim())
123
+ .filter((line) => line.length > 0));
124
+ }
125
+ function defaultReadCommittedFile(projectDir, path) {
126
+ try {
127
+ // `HEAD:./<path>` resolves relative to this directory, so a project nested
128
+ // in a repository reads its own manifest rather than the parent's.
129
+ return execFileSync("git", ["show", `HEAD:./${path}`], {
130
+ cwd: projectDir,
131
+ encoding: "utf8",
132
+ stdio: ["ignore", "pipe", "ignore"],
133
+ windowsHide: true,
134
+ });
135
+ }
136
+ catch {
137
+ // No commit, no such path in it, or no repository: nothing is committed
138
+ // here that a clone would receive.
139
+ return null;
140
+ }
141
+ }
142
+ /** The `@deftai/directive` spec as committed, or null when none is. */
143
+ function committedRuntimeSpec(manifest) {
144
+ if (manifest === null)
145
+ return null;
146
+ let parsed;
147
+ try {
148
+ parsed = JSON.parse(manifest);
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ if (typeof parsed !== "object" || parsed === null)
154
+ return null;
155
+ const pkg = parsed;
156
+ for (const block of ["devDependencies", "dependencies"]) {
157
+ const deps = pkg[block];
158
+ if (typeof deps !== "object" || deps === null || Array.isArray(deps))
159
+ continue;
160
+ const spec = deps[PIN_DEPENDENCY_NAME];
161
+ if (typeof spec === "string" && spec.trim().length > 0)
162
+ return spec.trim();
163
+ }
164
+ return null;
165
+ }
166
+ function isFailClosed(host) {
167
+ return FAIL_CLOSED_HOOK_HOSTS.includes(host);
168
+ }
169
+ function recoveryHosts(failClosed) {
170
+ const hosts = [...new Set(failClosed.map((entry) => entry.host))];
171
+ return hosts
172
+ .map((host) => `\`${disableHostHooksInvocation(` --host ${host} --confirm`)}\``)
173
+ .join(" / ");
174
+ }
175
+ function causeLine(anchor) {
176
+ if (anchor.kind === "uncommitted") {
177
+ return ` ${RUNTIME_ANCHOR_MANIFEST} declares ${PIN_DEPENDENCY_NAME} but is not committed, so a fresh`;
178
+ }
179
+ if (anchor.kind === "non-portable") {
180
+ return ` ${RUNTIME_ANCHOR_MANIFEST} pins ${PIN_DEPENDENCY_NAME} to \`${anchor.spec}\`, which no clone can install, so a fresh`;
181
+ }
182
+ return ` No ${RUNTIME_ANCHOR_MANIFEST} dependency on ${PIN_DEPENDENCY_NAME} travels with this tree, so a fresh`;
183
+ }
184
+ function fixLine(anchor) {
185
+ if (anchor.kind === "uncommitted") {
186
+ return ` Fix: commit ${RUNTIME_ANCHOR_MANIFEST} so the runtime travels with the registration.`;
187
+ }
188
+ if (anchor.kind === "non-portable") {
189
+ return ` Fix: pin ${PIN_DEPENDENCY_NAME} to a published version so a clone can install it.`;
190
+ }
191
+ return ` Fix: commit a ${RUNTIME_ANCHOR_MANIFEST} dependency on ${PIN_DEPENDENCY_NAME} so the runtime travels too.`;
192
+ }
193
+ /**
194
+ * Judge the anchor from the commit, not the working tree or the index.
195
+ *
196
+ * A clone receives commits. A manifest that is merely present, merely staged,
197
+ * or edited-but-uncommitted declares the runtime to this machine only, and the
198
+ * consumer is free to commit the generated registration without it.
199
+ */
200
+ function resolveAnchorState(committedSpec, workingSpec) {
201
+ if (committedSpec === null) {
202
+ return workingSpec === null ? { kind: "absent" } : { kind: "uncommitted" };
203
+ }
204
+ if (!reconstitutesForAClone(committedSpec)) {
205
+ return { kind: "non-portable", spec: committedSpec };
206
+ }
207
+ return { kind: "travels" };
208
+ }
209
+ function buildWarning(traveling, failClosed, anchor) {
210
+ const listed = traveling
211
+ .map((entry) => `${entry.path}${isFailClosed(entry.host) ? " (fail-closed)" : ""}`)
212
+ .join(", ");
213
+ const cause = causeLine(anchor);
214
+ const lines = [
215
+ `\u26a0 Hook registration travels without its runtime (#3785): ${listed}`,
216
+ cause,
217
+ " clone, CI runner, or container cannot obtain the `deft-hook` command these files name.",
218
+ ];
219
+ if (failClosed.length > 0) {
220
+ lines.push(" Fail-closed hosts then deny every mutation on an opaque exit 127, and no Directive", " code runs to say why. There is no in-session recovery on a host without Node.");
221
+ }
222
+ lines.push(fixLine(anchor));
223
+ if (failClosed.length > 0) {
224
+ lines.push(` Or, accepting the capability cost: ${recoveryHosts(failClosed)}.`);
225
+ }
226
+ lines.push(" \u2297 Do not hand-edit `failClosed` in the deposited file -- the next `deft update` restores it.", " Detail: .deft/core/docs/hook-runtime-unavailable.md");
227
+ return lines.join("\n");
228
+ }
229
+ /**
230
+ * Report whether any enabled agent-hook registration travels with this tree
231
+ * while no manifest travelling alongside it lets a clone install the runtime it
232
+ * names.
233
+ *
234
+ * Disabled hosts are excluded: their registration is stripped, so a leftover
235
+ * copy cannot deny anything.
236
+ */
237
+ export function inspectHookRuntimeTravel(projectRoot, registrations, hostHooksPolicy, seams = {}) {
238
+ const enabled = registrations.filter((entry) => isHostHookDepositEnabled(entry.host, hostHooksPolicy));
239
+ const probePaths = [...enabled.map((entry) => entry.path), RUNTIME_ANCHOR_MANIFEST];
240
+ const gitLsFiles = seams.gitLsFiles ?? defaultGitLsFiles;
241
+ const traveling = travelingPaths(gitLsFiles(projectRoot, probePaths));
242
+ if (traveling === null) {
243
+ return {
244
+ travelingRegistrations: [],
245
+ failClosedRegistrations: [],
246
+ runtimeTravels: false,
247
+ warning: null,
248
+ };
249
+ }
250
+ // The two sides are judged asymmetrically, deliberately. A registration is
251
+ // dangerous as soon as it is trackable: one `git add` sends it to every
252
+ // clone. An anchor counts only once a commit carries it, because a clone
253
+ // receives commits -- present, staged, and edited-but-uncommitted manifests
254
+ // all declare the runtime to this machine alone.
255
+ const readCommittedFile = seams.readCommittedFile ?? defaultReadCommittedFile;
256
+ const committedSpec = committedRuntimeSpec(readCommittedFile(projectRoot, RUNTIME_ANCHOR_MANIFEST));
257
+ const anchor = resolveAnchorState(committedSpec, (seams.readPin ?? readPin)(projectRoot).rawSpec);
258
+ const runtimeTravels = anchor.kind === "travels";
259
+ const travelingRegistrations = enabled.filter((entry) => traveling.has(entry.path));
260
+ const failClosedRegistrations = travelingRegistrations.filter((entry) => isFailClosed(entry.host));
261
+ const unsafe = travelingRegistrations.length > 0 && !runtimeTravels;
262
+ return {
263
+ travelingRegistrations: travelingRegistrations.map((entry) => entry.path),
264
+ failClosedRegistrations: failClosedRegistrations.map((entry) => entry.path),
265
+ runtimeTravels,
266
+ warning: unsafe ? buildWarning(travelingRegistrations, failClosedRegistrations, anchor) : null,
267
+ };
268
+ }
269
+ //# sourceMappingURL=hook-runtime-travel.js.map
@@ -6,7 +6,7 @@
6
6
  * the missing derive step.
7
7
  */
8
8
  import { ENV_RUN_SUMMARY_PATH, RunSummaryEmitter, } from "../run-summary/index.js";
9
- import { deriveAcceptanceClauses, readAcceptanceClauses, serializeAcceptanceClauses, } from "../verify-ac/clauses.js";
9
+ import { collectPlanItemAcceptanceSurface, deriveAcceptanceClauses, readAcceptanceClauses, serializeAcceptanceClauses, } from "../verify-ac/clauses.js";
10
10
  /** One-line remediation when a stamp has no statement-traceable clause (#3398). */
11
11
  export const CLAUSE_STAMP_IMPLEMENTATION_ONLY_REMEDIATION = "derive clauses from the statement's testable constraints";
12
12
  export const MISSING_AMBIGUITY_ATTESTATION_CAUSE = "missing_ambiguity_attestation";
@@ -72,7 +72,11 @@ export function collectTaskStatementFromPlan(plan) {
72
72
  continue;
73
73
  }
74
74
  const narrative = asRecord(rec.narrative);
75
- const text = narrative?.Acceptance;
75
+ const declared = narrative?.Acceptance;
76
+ // #3826: criteria commonly live in item.title with an empty narrative object.
77
+ // Reading only narrative.Acceptance left them out of the statement entirely,
78
+ // so a clause derived from a title could not trace back to it.
79
+ const text = isNonEmptyString(declared) ? declared : rec.title;
76
80
  if (isNonEmptyString(text)) {
77
81
  parts.push(text.trim());
78
82
  }
@@ -296,7 +300,9 @@ export function applyClauseDerivationToPlan(plan, options = {}) {
296
300
  notice: "",
297
301
  };
298
302
  }
299
- const clauses = deriveAcceptanceClauses(collectTaskStatementFromPlan(plan));
303
+ const clauses = deriveAcceptanceClauses(collectTaskStatementFromPlan(plan), {
304
+ itemSurface: collectPlanItemAcceptanceSurface(plan),
305
+ });
300
306
  if (clauses.length === 0) {
301
307
  return { applied: false, clauses: [], notice: "" };
302
308
  }