@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5

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 (101) hide show
  1. package/core/.antigravity-plugin/plugin.json +0 -1
  2. package/core/.claude-plugin/plugin.json +1 -1
  3. package/core/.codex-plugin/plugin.json +1 -1
  4. package/core/.plugin/plugin.json +1 -1
  5. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  6. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  8. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  9. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  10. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  11. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  12. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  13. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  15. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  16. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  17. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  18. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  19. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  20. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  22. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  23. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  24. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  26. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  27. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  28. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  29. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  30. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  31. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  32. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  33. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  34. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  35. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  36. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  37. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  38. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  39. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  40. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  41. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  42. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  43. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  44. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  45. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  46. package/core/registry/assets.json +19 -0
  47. package/core/registry/contracts.json +2 -2
  48. package/core/registry/harnesses.json +196 -10
  49. package/core/registry/lifecycle.json +2626 -7
  50. package/core/registry/storage.json +24 -1
  51. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  52. package/core/shared/skills/do-design/SKILL.md +6 -6
  53. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  54. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  55. package/core/shared/skills/do-implement/SKILL.md +3 -2
  56. package/core/shared/skills/do-plan/SKILL.md +7 -8
  57. package/package.json +1 -1
  58. package/src/adapters/antigravity/index.js +83 -47
  59. package/src/adapters/claude/index.js +15 -1
  60. package/src/adapters/codex/hooks.js +40 -5
  61. package/src/adapters/codex/index.js +52 -9
  62. package/src/adapters/copy-tree.js +4 -4
  63. package/src/adapters/gemini/hooks.js +57 -16
  64. package/src/adapters/gemini/index.js +57 -18
  65. package/src/adapters/kiro/index.js +15 -1
  66. package/src/adapters/pi/hooks.js +119 -0
  67. package/src/adapters/pi/index.js +34 -9
  68. package/src/cli/commands/install.js +1 -1
  69. package/src/cli/commands/update.js +1 -1
  70. package/src/cli/index.js +17 -5
  71. package/src/cli/runtime-commands.js +51 -12
  72. package/src/lifecycle/coverage.js +57 -0
  73. package/src/lifecycle/hook-step.js +61 -0
  74. package/src/lifecycle/index.js +28 -14
  75. package/src/registry/index.js +136 -7
  76. package/src/runtime/candidate.js +505 -0
  77. package/src/runtime/claims.js +14 -5
  78. package/src/runtime/cli-result.js +11 -4
  79. package/src/runtime/cli.js +35 -268
  80. package/src/runtime/context/budget.js +203 -0
  81. package/src/runtime/context/compose.js +1367 -0
  82. package/src/runtime/context/delivery-store.js +102 -0
  83. package/src/runtime/context/mandatory.js +594 -0
  84. package/src/runtime/context/pointer.js +142 -0
  85. package/src/runtime/context-pack.js +1029 -244
  86. package/src/runtime/evidence-fields.js +39 -0
  87. package/src/runtime/evidence-ledger.js +7 -0
  88. package/src/runtime/freshness.js +44 -7
  89. package/src/runtime/outcome.js +1 -1
  90. package/src/runtime/readiness.js +83 -13
  91. package/src/runtime/retrieval-plan.js +422 -73
  92. package/src/runtime/retrieval-record.js +223 -0
  93. package/src/runtime/scaffold/artifacts.js +3 -0
  94. package/src/runtime/source-version.js +289 -0
  95. package/src/runtime/task-attempts.js +1 -1
  96. package/src/runtime/task-scope.js +28 -3
  97. package/src/runtime/task-state.js +72 -7
  98. package/core/.antigravity-plugin/hooks.json +0 -39
  99. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  100. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  101. package/core/harnesses/shared/hooks/hooks.json +0 -47
@@ -92,6 +92,24 @@
92
92
  "src/runtime/task-state.js"
93
93
  ]
94
94
  },
95
+ {
96
+ "id": "task-delivery",
97
+ "root": "project-state",
98
+ "path": "delivery",
99
+ "entry": "record",
100
+ "match": "^[A-Za-z0-9][A-Za-z0-9._-]*\\.json$",
101
+ "rule": "work",
102
+ "bound": {
103
+ "finishedDays": 30
104
+ },
105
+ "protect": null,
106
+ "moments": [
107
+ "verb"
108
+ ],
109
+ "writers": [
110
+ "src/runtime/task-state.js"
111
+ ]
112
+ },
95
113
  {
96
114
  "id": "task-readiness",
97
115
  "root": "project-state",
@@ -162,7 +180,8 @@
162
180
  "verb"
163
181
  ],
164
182
  "writers": [
165
- "src/runtime/retrieval-plan.js"
183
+ "src/runtime/task-state.js",
184
+ "src/runtime/retrieval-record.js"
166
185
  ]
167
186
  },
168
187
  {
@@ -989,6 +1008,10 @@
989
1008
  "file": "core/shared/scripts/doflow/bash/sync-context.sh",
990
1009
  "reason": "agent context file (AGENTS.md or CLAUDE.md) managed block, owned by the install ledger"
991
1010
  },
1011
+ {
1012
+ "file": "core/harnesses/shared/hooks/bridge/bridge.sh",
1013
+ "reason": "caller-named trace file, appended only when the caller sets DOFLOW_BRIDGE_TRACE"
1014
+ },
992
1015
  {
993
1016
  "file": "src/adapters/claude/index.js",
994
1017
  "reason": "harness configuration/projection owned by the install ledger"
@@ -13,8 +13,10 @@
13
13
  # PATH pointing at any of them.
14
14
  #
15
15
  # Usage: doflow-run <verb> [args...]
16
+ # enforcement --harness <id> --policy <id> [--profile <id>] [--json]
16
17
  # Every verb accepts --json. Exit 0 = success, 1 = an actionable finding, 2 = usage or
17
- # resolution error. Arguments, stdin and exit status pass through the dispatcher unmodified —
18
+ # resolution error, 3 = context-pack overflow. Arguments, stdin and exit status pass through the
19
+ # dispatcher unmodified —
18
20
  # `sync-context` reads its block body from stdin, so anything that buffered or consumed it here
19
21
  # would break that verb rather than merely inconvenience it.
20
22
 
@@ -112,13 +114,14 @@ capture_failure() {
112
114
 
113
115
  # After a verb ran: a status outside {0, 1, 2} is a crash worth recording (a signal, a runtime that
114
116
  # died), minus what is not DoFlow's fault (DEC-031): 127 (command not found), 130, 141 and 143
115
- # (interrupted, broken pipe, terminated) and the task-not-found status 3 of `task-brief`. A Node
116
- # crash exits 1 and is recorded by the CLI itself, so it is never counted twice.
117
+ # (interrupted, broken pipe, terminated), task-not-found status 3 of `task-brief`, and overflow
118
+ # status 3 of `context-pack`. A Node crash exits 1 and is recorded by the CLI itself, so it is
119
+ # never counted twice.
117
120
  capture_exit_status() {
118
121
  local verb="${1:-}" status="${2:-0}"
119
122
  case "$status" in
120
123
  0|1|2|127|130|141|143) return 0 ;;
121
- 3) [ "$verb" = "task-brief" ] && return 0 ;;
124
+ 3) case "$verb" in task-brief|context-pack) return 0 ;; esac ;;
122
125
  esac
123
126
  capture_failure "$verb" "exit-$status" "$status"
124
127
  return 0
@@ -358,7 +361,7 @@ Workflow and evidence:
358
361
  claim record or update a claim and its support state
359
362
  readiness evaluate readiness; return state and missing items
360
363
  context-pack emit the compiled context pack for a stage
361
- retrieval-plan declare the needs a stage will resolve, then report every declared item
364
+ retrieval-plan declare the needs a stage will resolve, attempt each need, then report every declared item
362
365
  outcome record a task's terminal state with its basis, or show what was recorded
363
366
  scaffold emit the code scaffold the active feature's artifacts imply
364
367
  decision register, list or compact the active feature's decisions
@@ -379,6 +382,7 @@ Observability:
379
382
  discover missed opportunities in recorded runs
380
383
  inventory both scopes at once: shadowed assets, drift, unmanaged contenders
381
384
  capabilities provider resolution
385
+ enforcement verified lifecycle policy coverage
382
386
  doctor installation and provider health
383
387
  storage report stored data and its retention bounds
384
388
  EOF
@@ -478,7 +482,7 @@ shell_helper_for() {
478
482
  is_node_verb() {
479
483
  case "$1" in
480
484
  classify|workflow|route|evidence|claim|readiness|context-pack|retrieval-plan|outcome|scaffold|decision|followup|lifecycle|goal|failure|verify|recover|leak-scan|\
481
- trace|stats|indicators|discover|inventory|capabilities|doctor|orchestrate|research-request|task-attempt|retrieve|model-role|storage|brief-tools|advise) return 0 ;;
485
+ trace|stats|indicators|discover|inventory|capabilities|enforcement|doctor|orchestrate|research-request|task-attempt|retrieve|model-role|storage|brief-tools|advise) return 0 ;;
482
486
  *) return 1 ;;
483
487
  esac
484
488
  }
@@ -64,12 +64,12 @@ a `design.md` for it invents an artifact its workflow never reads.
64
64
  ```bash
65
65
  "$DOFLOW" context-pack --task-id "<task id>" --json
66
66
  ```
67
- **Exit 1 means the pack came back empty.** This early in the chain that is ordinary rather than a
68
- problem — a feature reaching design for the first time has nothing recorded against it yet, and
69
- `do-brainstorm` may not have batched evidence at all. Treat it the same way step 3's precondition
70
- treats a missing `requirement.md`: note the gap in this stage's report and fall through to
71
- reading `requirement.md` directly, which this step always did. When the pack is non-empty, read
72
- it alongside `requirement.md` for the user stories, FRs, and NFRs the design must serve.
67
+ **Exit 1 means the pack came back empty; exit 3 means overflow: the pack is complete, act on
68
+ `overflowAction.message` and report the gap.** Early in the chain an empty pack is ordinary — a
69
+ new feature has nothing recorded yet, and `do-brainstorm` may not have batched evidence. Treat
70
+ it like step 3's precondition treats missing `requirement.md`: note the gap in this stage's report
71
+ and fall through to reading `requirement.md` directly. When non-empty, read it alongside
72
+ `requirement.md` for the user stories, FRs, and NFRs the design must serve.
73
73
 
74
74
  5. **Design** — per `--type` (architecture/api/component/database), produce the system-shape
75
75
  decisions: a C4 System Context diagram (actors + external systems this feature touches) and,
@@ -163,16 +163,17 @@ Branch on the returned `outcome` field, not the exit code.
163
163
  ```bash
164
164
  "$DOFLOW" context-pack --task-id "<task id>" --json
165
165
  ```
166
- **Exit 1 means the pack came back empty.** Unlike design and plan, this call sits right at the
167
- point a subagent is about to be handed the task and told to act — so proceeding as though
168
- nothing happened is exactly the failure this feature exists to remove. An empty pack here can
169
- still be legitimate: step 4 lets a task reach `READY` on `callerAsserted` inputs alone, which
170
- link no evidence. Do not withhold dispatch on this alone — readiness already governs whether the
171
- task may be worked. Instead, carry the empty result into the group's brief itself: state in the
172
- dispatch, next to the task-brief output, that no prior evidence or claims were compiled for this
173
- task, so the subagent works from the plan/design text `task-brief` already supplies without
174
- assuming a grounding evidence base that isn't there. A non-empty pack is included in the brief
175
- as the subagent's prior context, same handoff.
166
+ **Exit 1 means the pack came back empty; exit 3 means overflow: the pack is complete, act on
167
+ `overflowAction.message` and report the gap.**
168
+ This call sits right at subagent dispatch — proceeding as though nothing happened is the defect
169
+ this feature removes. An empty pack here can still be legitimate: step 4 lets a task reach
170
+ `READY` on `callerAsserted` inputs alone, which link no evidence. Do not withhold dispatch on
171
+ this alone — readiness already governs whether the task may be worked. Instead, carry the empty
172
+ result into the group's brief itself: state in the dispatch, next to the task-brief output, that
173
+ no prior evidence or claims were compiled for this task, so the subagent works from the
174
+ plan/design text `task-brief` already supplies without assuming a grounding evidence base that
175
+ isn't there. A non-empty pack is included in the brief as the subagent's prior context, same
176
+ handoff.
176
177
  - Claim each task before dispatch and finish it with evidence: `references/task_attempts.md`.
177
178
  - Dispatch tasks to appropriate specialist archetypes:
178
179
  - Architecture & Schema $\rightarrow$ `system-architect`
@@ -153,3 +153,5 @@ checklist it is. Do not halt indefinitely on a persistent `NEEDS_EVIDENCE` — r
153
153
  actually established, state which requirements it covers, and say what remains. And do not write
154
154
  `READY` yourself. The gate did not say it, and saying it on the gate's behalf is precisely the
155
155
  failure this page exists to prevent.
156
+
157
+ Record an absence with `--absence --need <intent>`; a positive item never states `--absence`.
@@ -111,8 +111,9 @@ Run every command below from the project root — the walk-up starts at `$PWD`.
111
111
  "$DOFLOW" context-pack --task-id "<task id>" --json
112
112
  ```
113
113
  This verb exits 1 on an empty pack by design — nothing recorded is not the same as nothing
114
- needed — and that non-zero exit is never read as success. Because this call only runs after the
115
- evidence check above already found something recorded for this task id, an exit 1 here means
114
+ needed — and that non-zero exit is never read as success; exit 3 means overflow: the pack is
115
+ complete, act on `overflowAction.message` and report the gap. Because this call only runs after
116
+ the evidence check above already found something recorded for this task id, an exit 1 here means
116
117
  the compiled pack still held nothing usable (evidence with no linked claims, most likely):
117
118
  treat that as a mismatch worth naming in step 6's report, not as license to proceed as though
118
119
  there were no prior context to look for. On a normal, non-empty pack, carry its evidence and
@@ -67,14 +67,13 @@ not have.
67
67
  ```bash
68
68
  "$DOFLOW" context-pack --task-id "<task id>" --json
69
69
  ```
70
- **Exit 1 means the pack came back empty.** By this point `do-design` should already have batched
71
- its own stage-boundary evidence and claims (its step 8), so an empty pack here carries more
72
- weight than it does at design time: either that batch never ran, or this task id doesn't match
73
- the one design used. Treat it as advisory rather than blocking — plan's hard gate is artifact
74
- existence only — but say so plainly in this stage's report, and in `plan.md`'s own §3 "Research &
75
- Decisions": a decision written straight from `requirement.md`/`design.md` prose, with no compiled
76
- pack behind it, must not be recorded as if it traced to prior evidence it doesn't have. When the
77
- pack is non-empty, read it alongside `requirement.md`, `design.md`, and the constitution.
70
+ **Exit 1 means the pack came back empty; exit 3 means overflow: the pack is complete, act on
71
+ `overflowAction.message` and report the gap.** By this point `do-design` should have batched
72
+ evidence and claims (step 8), so an empty pack carries weight: either the batch never ran or the
73
+ task id mismatched. Treat it as advisory rather than blocking — plan's hard gate is artifact
74
+ existence only — but note it in this stage's report and `plan.md` §3 "Research & Decisions": a
75
+ decision written straight from prose without a compiled pack must not claim prior evidence. When
76
+ non-empty, read it alongside `requirement.md`, `design.md`, and the constitution.
78
77
  **Also read `specs.md` when step 1's `has_specs` is true** — use that flag, never a filesystem
79
78
  check of your own, and read the `specs` path the resolver returned. `/do-design` moves the
80
79
  interface and data-model contracts out of `design.md` into `specs.md` §1 as numbered `IC-###`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khoavu882/doflow",
3
- "version": "2.0.0-beta.4",
3
+ "version": "2.0.0-beta.5",
4
4
  "description": "DoFlow config installer — npx CLI for the registry-driven multi-harness install/update/remove lifecycle",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -178,6 +178,8 @@ function createAntigravityAdapter({ declaredPaths = DEFAULT_DECLARED_PATHS }
178
178
  apply,
179
179
  remove,
180
180
  verify: (options , impl = {}) => verify(options, { ...impl, nativePaths: nativePathsFor }),
181
+ planHookDoors: (options , impl = {}) => planHookDoors(options, { ...impl, nativePaths: nativePathsFor }),
182
+ verifyHookDoors: (options , impl = {}) => verifyHookDoors(options, { ...impl, nativePaths: nativePathsFor }),
181
183
  };
182
184
  }
183
185
 
@@ -449,6 +451,29 @@ function plan(options = {}, impl = {}) {
449
451
  };
450
452
  }
451
453
 
454
+ /** The hook front doors (IC-037): the shims and the hooks.json groups that run them, planned and
455
+ * verified alone. Project scope only, and only when the harness selected the hooks asset. */
456
+ function planHookDoors(options = {}, impl = {}) {
457
+ const fsImpl = impl.fsImpl || fs;
458
+ const context = options.context ?? {};
459
+ const scope = options.scope ?? 'project';
460
+ if (!(options.assets ?? []).some((asset ) => asset.id === HOOKS_ASSET_ID)) return { changes: [], conflicts: [], prerequisites: [], notices: [] };
461
+ const paths = (impl.nativePaths ?? singleton.nativePaths)({ ...options, scope, homeDir: context.homeDir });
462
+ const doors = planHooks({ paths, scope, neutralResources: ledgerResources(options), removing: false, repoRoot: context.repoRoot, fsImpl });
463
+ return { changes: doors.changes, conflicts: doors.conflicts, prerequisites: [], notices: [] };
464
+ }
465
+
466
+ function verifyHookDoors(options = {}, impl = {}) {
467
+ const fsImpl = impl.fsImpl || fs;
468
+ const context = options.context ?? {};
469
+ const scope = options.scope ?? 'project';
470
+ if (scope !== 'project' || !(options.assets ?? []).some((asset ) => asset.id === HOOKS_ASSET_ID)) return { ok: true, statuses: [], resources: [], conflicts: [] };
471
+ const paths = (impl.nativePaths ?? singleton.nativePaths)({ ...options, scope, homeDir: context.homeDir });
472
+ const doors = verifyHooks({ options, paths, context, fsImpl });
473
+ const conflicts = doors.statuses.filter((status ) => status.status === 'conflict').map((status ) => status.reason ?? status.identity);
474
+ return { ok: conflicts.length === 0, ...doors, conflicts };
475
+ }
476
+
452
477
  function apply(options = {}, impl = {}) {
453
478
  const fsImpl = impl.fsImpl || fs;
454
479
  // apply()/verify() are reached through the lifecycle with the same scope inputs plan() saw, so
@@ -506,6 +531,60 @@ function remove(options = {}, impl = {}) {
506
531
  return apply(options, impl);
507
532
  }
508
533
 
534
+ /** The hook shims' bytes and the registered groups (project scope), as statuses and resources. */
535
+ function verifyHooks({ options, paths, context, fsImpl } ) {
536
+ const statuses = [];
537
+ const resources = [];
538
+ const hooksTarget = paths.hooksJson;
539
+ const scriptTargets = {
540
+ 'pre-implementation-gate.sh': paths.hookScript,
541
+ 'stop-check.sh': paths.stopHookScript,
542
+ };
543
+ // Source-side residue: reads the shims from THIS checkout (the authored source of the
544
+ // projection, not an install destination — the destination is declared as the paths above).
545
+ const scriptSourceDir = path.join(path.resolve(registryRepoRoot(options)), 'core', 'harnesses', 'antigravity', 'hooks');
546
+ const removingOp = (context.operation ?? options.operation ?? '') === 'remove';
547
+ const fingerprints = {};
548
+ let scriptsOk = true;
549
+ let anyPresent = false;
550
+ for (const identity of HOOK_SCRIPTS) {
551
+ let sourceFp = null;
552
+ try { sourceFp = sha256File(fsImpl, path.join(scriptSourceDir, identity)); } catch { /* checkout without that shim */ }
553
+ fingerprints[identity] = sourceFp;
554
+ const target = scriptTargets[identity];
555
+ const current = fsImpl.existsSync(target) ? sha256File(fsImpl, target) : null;
556
+ if (current !== null) anyPresent = true;
557
+ if (current === null || current !== sourceFp) scriptsOk = false;
558
+ }
559
+ const doc = readJsonObject(hooksTarget, { fsImpl });
560
+ const gate = doc?.[HOOKS_GROUP];
561
+ const stop = doc?.[STOP_HOOKS_GROUP];
562
+ const gateOk = Boolean(gate?.PreToolUse?.[0]?.hooks?.[0]?.command)
563
+ && gate.PreToolUse[0].hooks[0].command === scriptTargets['pre-implementation-gate.sh']
564
+ && gate.PreToolUse[0].matcher === GATE_MATCHER;
565
+ // Stop registration is matcher-free: handlers sit directly under the event key.
566
+ const stopOk = Array.isArray(stop?.Stop) && stop.Stop.length > 0
567
+ && stop.Stop[0]?.command === scriptTargets['stop-check.sh'];
568
+ const groupsOk = gateOk && stopOk;
569
+ statuses.push({ assetId: HOOKS_ASSET_ID, capability: 'hooks', status:
570
+ removingOp ? (!anyPresent && !groupsOk ? 'absent' : 'retained')
571
+ : (scriptsOk && groupsOk ? 'managed' : (!anyPresent && !groupsOk ? 'absent' : 'missing')),
572
+ ownershipIdentity: `${HARNESS}:hooks:registration`, target: hooksTarget });
573
+ if (!removingOp && scriptsOk && groupsOk) {
574
+ resources.push({ assetId: HOOKS_ASSET_ID, target: hooksTarget,
575
+ ownershipIdentity: `${HARNESS}:hooks:registration`, kind: 'hooks-json',
576
+ fingerprint: fingerprint(JSON.stringify(doc)), sourceVersion: 'registry-v1',
577
+ projection: { renderer: 'antigravity-hooks' } });
578
+ for (const identity of HOOK_SCRIPTS) {
579
+ resources.push({ assetId: HOOKS_ASSET_ID, target: scriptTargets[identity],
580
+ ownershipIdentity: `${HARNESS}:copy-tree:${HOOKS_ASSET_ID}:${identity}`,
581
+ kind: 'copy-tree-file', identity, fingerprint: fingerprints[identity],
582
+ sourceVersion: 'registry-v1', projection: { renderer: 'antigravity-hooks' } });
583
+ }
584
+ }
585
+ return { statuses, resources };
586
+ }
587
+
509
588
  function verify(options = {}, impl = {}) {
510
589
  const fsImpl = impl.fsImpl || fs;
511
590
  const context = options.context ?? {};
@@ -564,53 +643,9 @@ function verify(options = {}, impl = {}) {
564
643
 
565
644
  // Hooks projection (project scope): the shims' bytes and the registered groups.
566
645
  if (scope === 'project') {
567
- const hooksTarget = paths.hooksJson;
568
- const scriptTargets = {
569
- 'pre-implementation-gate.sh': paths.hookScript,
570
- 'stop-check.sh': paths.stopHookScript,
571
- };
572
- // Source-side residue: reads the shims from THIS checkout (the authored source of the
573
- // projection, not an install destination — the destination is declared as the paths above).
574
- const scriptSourceDir = path.join(path.resolve(registryRepoRoot(options)), 'core', 'harnesses', 'antigravity', 'hooks');
575
- const removingOp = (context.operation ?? options.operation ?? '') === 'remove';
576
- const fingerprints = {};
577
- let scriptsOk = true;
578
- let anyPresent = false;
579
- for (const identity of HOOK_SCRIPTS) {
580
- let sourceFp = null;
581
- try { sourceFp = sha256File(fsImpl, path.join(scriptSourceDir, identity)); } catch { /* checkout without that shim */ }
582
- fingerprints[identity] = sourceFp;
583
- const target = scriptTargets[identity];
584
- const current = fsImpl.existsSync(target) ? sha256File(fsImpl, target) : null;
585
- if (current !== null) anyPresent = true;
586
- if (current === null || current !== sourceFp) scriptsOk = false;
587
- }
588
- const doc = readJsonObject(hooksTarget, { fsImpl });
589
- const gate = doc?.[HOOKS_GROUP];
590
- const stop = doc?.[STOP_HOOKS_GROUP];
591
- const gateOk = Boolean(gate?.PreToolUse?.[0]?.hooks?.[0]?.command)
592
- && gate.PreToolUse[0].hooks[0].command === scriptTargets['pre-implementation-gate.sh']
593
- && gate.PreToolUse[0].matcher === GATE_MATCHER;
594
- // Stop registration is matcher-free: handlers sit directly under the event key.
595
- const stopOk = Array.isArray(stop?.Stop) && stop.Stop.length > 0
596
- && stop.Stop[0]?.command === scriptTargets['stop-check.sh'];
597
- const groupsOk = gateOk && stopOk;
598
- statuses.push({ assetId: HOOKS_ASSET_ID, capability: 'hooks', status:
599
- removingOp ? (!anyPresent && !groupsOk ? 'absent' : 'retained')
600
- : (scriptsOk && groupsOk ? 'managed' : (!anyPresent && !groupsOk ? 'absent' : 'missing')),
601
- ownershipIdentity: `${HARNESS}:hooks:registration`, target: hooksTarget });
602
- if (!removingOp && scriptsOk && groupsOk) {
603
- resources.push({ assetId: HOOKS_ASSET_ID, target: hooksTarget,
604
- ownershipIdentity: `${HARNESS}:hooks:registration`, kind: 'hooks-json',
605
- fingerprint: fingerprint(JSON.stringify(doc)), sourceVersion: 'registry-v1',
606
- projection: { renderer: 'antigravity-hooks' } });
607
- for (const identity of HOOK_SCRIPTS) {
608
- resources.push({ assetId: HOOKS_ASSET_ID, target: scriptTargets[identity],
609
- ownershipIdentity: `${HARNESS}:copy-tree:${HOOKS_ASSET_ID}:${identity}`,
610
- kind: 'copy-tree-file', identity, fingerprint: fingerprints[identity],
611
- sourceVersion: 'registry-v1', projection: { renderer: 'antigravity-hooks' } });
612
- }
613
- }
646
+ const hooks = verifyHooks({ options, paths, context, fsImpl });
647
+ statuses.push(...hooks.statuses);
648
+ resources.push(...hooks.resources);
614
649
  }
615
650
 
616
651
  const conflicts = [...statuses.filter((s) => s.status === 'conflict').map((s) => s.reason ?? s.identity), ...mcp.conflicts];
@@ -627,5 +662,6 @@ function conflictsToStatuses(conflicts , assetId , statuses ) {
627
662
 
628
663
  module.exports = {
629
664
  HARNESS, nativePaths: singleton.nativePaths, discover, render, plan, apply, remove, verify,
665
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
630
666
  createAntigravityAdapter,
631
667
  };
@@ -480,6 +480,19 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
480
480
  return { changes, conflicts, prerequisites: [], notices: [...copyTree.notices, ...mcpPlan.notices] };
481
481
  }
482
482
 
483
+ /** The hook front doors (IC-037): the one asset that holds the scripts the settings hooks run. */
484
+ function hookDoorAssets(assets ) { return (assets || []).filter((asset ) => asset.id === 'claude.hooks-scripts'); }
485
+
486
+ function planHookDoors({ assets, scope, scopeRoot, context = {}, ledger } ) {
487
+ const doors = planCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context, ledger, removing: false });
488
+ return { changes: doors.changes, conflicts: doors.conflicts, prerequisites: [], notices: doors.notices };
489
+ }
490
+
491
+ function verifyHookDoors({ assets, scope, scopeRoot, context = {} } ) {
492
+ const doors = verifyCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context });
493
+ return { ok: doors.conflicts.length === 0, statuses: doors.statuses, resources: doors.resources, conflicts: doors.conflicts };
494
+ }
495
+
483
496
  function apply({ changes, scope, scopeRoot, context = {} } ) {
484
497
  for (const change of changes) {
485
498
  if (change.projection?.renderer !== INSTRUCTION_RENDERER) continue;
@@ -536,7 +549,7 @@ function createClaudeAdapter({ declaredPaths = declaredHarnessPaths().claude } =
536
549
  return { ok: conflicts.length === 0, statuses, resources, conflicts };
537
550
  }
538
551
 
539
- return { nativePaths, discover, render, plan, apply, remove, verify };
552
+ return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
540
553
  }
541
554
 
542
555
  const singleton = createClaudeAdapter();
@@ -544,5 +557,6 @@ const singleton = createClaudeAdapter();
544
557
  module.exports = {
545
558
  nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
546
559
  plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
560
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
547
561
  createClaudeAdapter,
548
562
  };
@@ -7,6 +7,7 @@
7
7
  // that are not executable. Trust is reported as a prerequisite rather than being bypassed.
8
8
  const fs = require('node:fs');
9
9
  const path = require('node:path');
10
+ const crypto = require('node:crypto');
10
11
  const { verifyHookCommands } = require('../hook-commands.js');
11
12
  const { mergeHooks } = require('../../helper/settings-merge.js');
12
13
  const { atomicWrite } = require('./config.js');
@@ -102,7 +103,32 @@ function destinationFor(context = {}) {
102
103
  throw new Error('A hook destination or Codex scope is required');
103
104
  }
104
105
 
105
- function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext = {}, trusted = false, fsImpl = fs } = {}) {
106
+ /** DoFlow's record of a deployed hook script, in the ledger's fingerprint form: sha256 of its bytes. */
107
+ function scriptFingerprint(bytes ) { return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`; }
108
+
109
+ /** The scripts beside the config that differ from the shipped source (or are absent), by name, and
110
+ * those among them a ledger row says were edited since DoFlow wrote them. A script with no row
111
+ * (an install from before scripts were recorded) is DoFlow's to refresh: the config deploy always
112
+ * overwrote it. `rows` are this harness's script rows: { target, fingerprint }. */
113
+ function planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows = [], force = false, fsImpl = fs } ) {
114
+ const sources = new Map (commands.checks.map((check ) => [check.script, check.file] ));
115
+ for (const name of shippedHookScripts(sourceHooksDir, fsImpl)) if (!sources.has(name)) sources.set(name, path.join(sourceHooksDir, name));
116
+ const stale = [];
117
+ const edited = [];
118
+ for (const [name, source] of sources) {
119
+ const target = path.join(destinationHooksDir, name);
120
+ if (fsImpl.existsSync(target)) {
121
+ const current = fsImpl.readFileSync(target);
122
+ if (current.equals(fsImpl.readFileSync(source))) continue;
123
+ const row = rows.find((item ) => path.resolve(item.target) === path.resolve(target));
124
+ if (row && row.fingerprint !== scriptFingerprint(current) && !force) edited.push(name);
125
+ }
126
+ stale.push(name);
127
+ }
128
+ return { names: [...sources.keys()], stale, edited };
129
+ }
130
+
131
+ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext = {}, trusted = false, scriptRows = [], force = false, fsImpl = fs } = {}) {
106
132
  let desired = config;
107
133
  try {
108
134
  if (!desired && sourceFile) desired = JSON.parse(fsImpl.readFileSync(sourceFile, 'utf8'));
@@ -130,16 +156,25 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
130
156
  }
131
157
  const content = schema.ok && mergedDesired ? `${JSON.stringify(mergedDesired, null, 2)}\n` : null;
132
158
  if (errors.length) return { ok: false, status: 'invalid-request', destination, original, changes: [], errors, commands };
133
- const changed = original !== content;
159
+ const configChanged = original !== content;
134
160
  const destinationHooksDir = destinationContext.paths?.hooksDirectory
135
161
  || destinationContext.hooksDir
136
162
  || path.join(path.dirname(destination), 'hooks');
163
+ // The scripts are deployed with the config but are not part of it: a script whose bytes differ
164
+ // from the source is redeployed even when the config is already right.
165
+ const scripts = planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows: scriptRows, force, fsImpl });
166
+ if (scripts.edited.length) {
167
+ return { ok: false, status: 'conflict', destination, original, changes: [], commands,
168
+ errors: scripts.edited.map((name ) => `Codex hook script '${name}' was modified outside DoFlow`) };
169
+ }
170
+ const scriptsOnly = !configChanged && scripts.stale.length > 0;
171
+ const changed = configChanged || scriptsOnly;
137
172
  // The scripts deployCodexHooks copies beside hooks.json, so a backup of this change holds them too.
138
173
  const companionTargets = [...new Set([
139
174
  ...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
140
175
  ...shippedHookScripts(sourceHooksDir, fsImpl).map((name ) => path.join(destinationHooksDir, name)),
141
176
  ])].sort();
142
- return { ok: true, status: changed ? 'change' : 'unchanged', destination, original, content,
177
+ return { ok: true, status: changed ? 'change' : 'unchanged', destination, original, content, scriptsOnly, scriptNames: scripts.names,
143
178
  changes: changed ? [{ type: fsImpl.existsSync(destination) ? 'update' : 'create', file: destination, companionTargets }] : [],
144
179
  errors: [], commands, trust: commands.trust, scriptsDir: sourceHooksDir, destinationHooksDir };
145
180
  }
@@ -147,7 +182,7 @@ function planCodexHooks({ config, sourceFile, sourceHooksDir, destinationContext
147
182
  function deployCodexHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
148
183
  if (!plan.ok) return { ...plan, applied: false };
149
184
  if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
150
- atomicWrite(plan.destination, plan.content, fsImpl);
185
+ if (!plan.scriptsOnly) atomicWrite(plan.destination, plan.content, fsImpl);
151
186
  const deployed = new Set ();
152
187
  for (const check of plan.commands.checks) {
153
188
  const target = path.join(plan.destinationHooksDir, check.script);
@@ -200,4 +235,4 @@ function removeCodexHookScripts({ scriptsDir, destinationHooksDir }
200
235
  return { removed };
201
236
  }
202
237
 
203
- module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planCodexHooks, deployCodexHooks, shippedHookScripts, removeCodexHookScripts };
238
+ module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planCodexHooks, deployCodexHooks, scriptFingerprint, shippedHookScripts, removeCodexHookScripts };
@@ -12,7 +12,7 @@ const crypto = require('node:crypto');
12
12
  const { configPath, fingerprint: configFingerprint, parseToml, planCodexConfig, applyCodexConfig } = require('./config.js');
13
13
  const { renderServer, planCodexMcp, applyCodexMcp, ownedCodexMcpIds, userDefinedCodexMcpIds, serverTableText } = require('./mcp.js');
14
14
  const { agentDirectory, discoverCodexAgents, planCodexAgents, applyCodexAgents } = require('./agents.js');
15
- const { planCodexHooks, deployCodexHooks, removeCodexHookScripts } = require('./hooks.js');
15
+ const { planCodexHooks, deployCodexHooks, removeCodexHookScripts, scriptFingerprint } = require('./hooks.js');
16
16
  const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices } = require('../copy-tree.js');
17
17
  const { mergeMarkedSection, removeMarkedSection, MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
18
18
  const { nativeMcpCatalog } = require('../../registry/index.js');
@@ -445,9 +445,16 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
445
445
  /** Codex hooks.json is only replaced when the file on disk still matches the ledger's last-known
446
446
  * fingerprint — a foreign edit (or a record the ledger never owned) downgrades an otherwise-clean
447
447
  * plan to a conflict rather than silently overwriting it. */
448
- function planHooksComponent({ native, neutralResources, context, adopt = false } ) {
448
+ /** This scope's ledger rows for the hook scripts deployed beside hooks.json. */
449
+ function scriptRows(neutralResources , context ) {
450
+ return (neutralResources || []).filter((resource ) => resource?.harness === HARNESS && resource.kind === 'hooks-script'
451
+ && (!resource.scope || resource.scope === context.scope));
452
+ }
453
+
454
+ function planHooksComponent({ native, neutralResources, context, adopt = false, force = false } ) {
449
455
  let hooks = planCodexHooks({ config: native.hooksConfig, sourceFile: native.hooksSourceFile,
450
- sourceHooksDir: native.hooksSourceDir, trusted: native.hooksTrusted, destinationContext: context });
456
+ sourceHooksDir: native.hooksSourceDir, trusted: native.hooksTrusted, destinationContext: context,
457
+ scriptRows: scriptRows(neutralResources, context), force });
451
458
  const hooksFile = hooks.destination;
452
459
  if (hooks.ok && fs.existsSync(hooksFile)) {
453
460
  const [record] = nativeManagedResources(neutralResources, context, { kind: 'hooks-file', target: hooksFile });
@@ -457,7 +464,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
457
464
  // conflict — that one is `--force`'s business, not adoption's.
458
465
  if (!record && !adopt) {
459
466
  hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json exists but is not owned by the neutral ledger'] };
460
- } else if (record && record.fingerprint !== current) {
467
+ } else if (record && record.fingerprint !== current && !force) {
461
468
  hooks = { ...hooks, ok: false, status: 'conflict', changes: [], errors: ['Codex hooks.json was modified outside DoFlow'] };
462
469
  }
463
470
  }
@@ -466,13 +473,22 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
466
473
 
467
474
  /** Agents/hooks share one removing-vs-planning fork: removal always owns both via the neutral
468
475
  * ledger's own records, regardless of what native input the caller provided this run. */
476
+ /** A removal drops the rows of the hook scripts too. The scripts themselves go by their bytes
477
+ * (removeCodexHookScripts), so each row is only released here. */
478
+ function withScriptReleases(plan , neutralResources , context , extra ) {
479
+ const releases = scriptRows(neutralResources, context).map((row ) => ({ type: 'remove', assetId: row.assetId, identity: row.identity,
480
+ target: row.target, ownershipIdentity: row.ownershipIdentity, kind: row.kind, release: true }));
481
+ return { ...plan, ...extra, changes: [...plan.changes, ...releases],
482
+ status: plan.status === 'unchanged' && releases.length ? 'change' : plan.status };
483
+ }
484
+
469
485
  function planAgentsAndHooksComponents({ native, neutralResources, context, agentsDirectory, removing, options, adopt = false } ) {
470
486
  if (removing) {
471
487
  return {
472
488
  agents: ownedRemovalPlan(neutralResources, context, 'custom-agent', agentsDirectory),
473
489
  // The scripts deployed beside hooks.json have no ledger rows; remove() matches them by bytes.
474
- hooks: { ...ownedRemovalPlan(neutralResources, context, 'hooks-file'),
475
- scriptsDir: native.hooksSourceDir, destinationHooksDir: context.paths.hooksDirectory },
490
+ hooks: withScriptReleases(ownedRemovalPlan(neutralResources, context, 'hooks-file'), neutralResources, context,
491
+ { scriptsDir: native.hooksSourceDir, destinationHooksDir: context.paths.hooksDirectory }),
476
492
  };
477
493
  }
478
494
  const components = {};
@@ -483,7 +499,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
483
499
  components.agents = planCodexAgents({ ...context, managedResources, sourceDir: native.agentsSourceDir, sourceVersion: options.sourceVersion ?? options.context?.sourceVersion, adopt });
484
500
  }
485
501
  if (native.hooksSourceFile !== undefined || native.hooksConfig !== undefined) {
486
- components.hooks = planHooksComponent({ native, neutralResources, context, adopt });
502
+ components.hooks = planHooksComponent({ native, neutralResources, context, adopt, force: options.context?.force === true });
487
503
  }
488
504
  return components;
489
505
  }
@@ -542,6 +558,25 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
542
558
  notices: [...copyTree.notices, ...(components.mcp?.notices || [])] };
543
559
  }
544
560
 
561
+ /** The hook front doors (IC-037): hooks.json and the scripts deployed beside it, planned and
562
+ * verified alone. A conflict reaches the lifecycle through `components`, as in plan(). */
563
+ function planHookDoors(options ) {
564
+ const native = projectedNativeOptions(options);
565
+ const hooks = native.hooksSourceFile === undefined && native.hooksConfig === undefined ? null
566
+ : planHooksComponent({ native, neutralResources: options.managedResources || options.ledger?.resources || [], context: nativeContext(options),
567
+ adopt: options.context?.adopt === true, force: options.context?.force === true });
568
+ const changes = [];
569
+ if (hooks) addChanges(changes, assetIdFor(options, 'hooks'), hooks, 'hooks');
570
+ return { changes, components: hooks ? { hooks } : {}, prerequisites: [], notices: [] };
571
+ }
572
+
573
+ function verifyHookDoors(options = {}) {
574
+ const hooks = verifyHooksResource({ native: projectedNativeOptions(options), context: nativeContext(options), options,
575
+ sourceVersion: options.sourceVersion ?? options.context?.sourceVersion, recoveryRef: options.recoveryRef });
576
+ return { ok: hooks.conflicts.length === 0 && hooks.statuses.every((status ) => status.status !== 'missing' && status.status !== 'conflict'),
577
+ resources: hooks.resources, statuses: hooks.statuses, conflicts: hooks.conflicts };
578
+ }
579
+
545
580
  /** Apply only native plans emitted by this adapter. MCP is first because it and
546
581
  * config.toml share a file; config is then re-planned over the MCP result so it
547
582
  * cannot overwrite an adjacent MCP table. */
@@ -579,7 +614,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
579
614
  function remove({ changes = [], dryRun = false } = {}) {
580
615
  const result = apply({ changes, dryRun });
581
616
  const direct = new Map();
582
- for (const change of changes) if (change?.nativePlan?.directRemove) direct.set(change.target, change);
617
+ for (const change of changes) if (change?.nativePlan?.directRemove && !change.retained) direct.set(change.target, change);
583
618
  for (const change of direct.values()) {
584
619
  if (!fs.existsSync(change.target)) continue;
585
620
  const current = sha256(fs.readFileSync(change.target, 'utf8'));
@@ -651,6 +686,13 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
651
686
  if (!hooks.ok) conflicts.push(...hooks.errors);
652
687
  if (status === 'managed') resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'hooks'), kind: 'hooks-file', identity: 'hooks.json',
653
688
  target: hooks.destination, fingerprint: sha256(fs.readFileSync(hooks.destination, 'utf8')), sourceVersion, recoveryRef, projection: { renderer: 'codex-hooks' } }));
689
+ if (status === 'managed') {
690
+ for (const name of hooks.scriptNames) {
691
+ resources.push(lifecycleResource({ scope: options.scope, assetId: assetIdFor(options, 'hooks'), kind: 'hooks-script', identity: name,
692
+ target: path.join(hooks.destinationHooksDir, name), fingerprint: scriptFingerprint(fs.readFileSync(path.join(hooks.destinationHooksDir, name))),
693
+ sourceVersion, recoveryRef, projection: { renderer: 'codex-hooks' } }));
694
+ }
695
+ }
654
696
  return { statuses, resources, conflicts };
655
697
  }
656
698
 
@@ -691,7 +733,7 @@ function createCodexAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
691
733
  return { ok: conflicts.length === 0 && statuses.every((status) => status.status !== 'missing' && status.status !== 'conflict'), resources, statuses, conflicts };
692
734
  }
693
735
 
694
- return { normalizeContext, nativeContext, projectedNativeOptions, nativeManagedResources, ownershipIdentity, discover, render, plan, apply, remove, verify };
736
+ return { normalizeContext, nativeContext, projectedNativeOptions, nativeManagedResources, ownershipIdentity, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
695
737
  }
696
738
 
697
739
  const singleton = createCodexAdapter();
@@ -703,5 +745,6 @@ module.exports = {
703
745
  ownershipIdentity: singleton.ownershipIdentity,
704
746
  discover: singleton.discover, render: singleton.render, plan: singleton.plan, apply: singleton.apply,
705
747
  remove: singleton.remove, verify: singleton.verify,
748
+ planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
706
749
  createCodexAdapter,
707
750
  };