@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.
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +24 -1
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +83 -47
- package/src/adapters/claude/index.js +15 -1
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +52 -9
- package/src/adapters/copy-tree.js +4 -4
- package/src/adapters/gemini/hooks.js +57 -16
- package/src/adapters/gemini/index.js +57 -18
- package/src/adapters/kiro/index.js +15 -1
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/index.js +34 -9
- package/src/cli/commands/install.js +1 -1
- package/src/cli/commands/update.js +1 -1
- package/src/cli/index.js +17 -5
- package/src/cli/runtime-commands.js +51 -12
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/index.js +28 -14
- package/src/registry/index.js +136 -7
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/claims.js +14 -5
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +35 -268
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1029 -244
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +7 -0
- package/src/runtime/freshness.js +44 -7
- package/src/runtime/outcome.js +1 -1
- package/src/runtime/readiness.js +83 -13
- package/src/runtime/retrieval-plan.js +422 -73
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/artifacts.js +3 -0
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/task-attempts.js +1 -1
- package/src/runtime/task-scope.js +28 -3
- package/src/runtime/task-state.js +72 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- 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/
|
|
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
|
|
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)
|
|
116
|
-
# crash exits 1 and is recorded by the CLI itself, so it is
|
|
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)
|
|
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
|
|
68
|
-
|
|
69
|
-
`do-brainstorm` may not have batched evidence
|
|
70
|
-
treats
|
|
71
|
-
reading `requirement.md` directly
|
|
72
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
still be legitimate: step 4 lets a task reach
|
|
170
|
-
link no evidence. Do not withhold dispatch on
|
|
171
|
-
task may be worked. Instead, carry the empty
|
|
172
|
-
dispatch, next to the task-brief output, that
|
|
173
|
-
task, so the subagent works from the
|
|
174
|
-
assuming a grounding evidence base that
|
|
175
|
-
as the subagent's prior context, same
|
|
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
|
|
115
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
existence only — but
|
|
75
|
-
|
|
76
|
-
|
|
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
|
@@ -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
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
};
|