@khoavu882/doflow 2.0.0-beta.3 → 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/bin/doflow.js +2 -2
- 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 +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- 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-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- 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 +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- 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
- package/src/runtime/context-pack.ts +0 -492
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
const crypto = require('node:crypto');
|
|
9
9
|
const fs = require('node:fs');
|
|
10
10
|
const path = require('node:path');
|
|
11
|
-
const { parseToml, stripComment, tableViolations } = require('../../helper/toml.
|
|
11
|
+
const { parseToml, stripComment, tableViolations } = require('../../helper/toml.js');
|
|
12
12
|
|
|
13
13
|
const CONFIG_NAME = 'config.toml';
|
|
14
14
|
const CONFIG_KIND = 'configuration-entry';
|
|
15
15
|
|
|
16
|
-
function configPath({ scope, codexDir, projectRoot, paths }
|
|
16
|
+
function configPath({ scope, codexDir, projectRoot, paths } = {}) {
|
|
17
17
|
// Preferred: the declared path resolved by the adapter (harnesses.json "paths".configFile).
|
|
18
18
|
if (paths?.configFile) return paths.configFile;
|
|
19
19
|
// Legacy derivation for direct callers passing raw scope inputs (tests, reconcileCodexConfig);
|
|
@@ -29,23 +29,23 @@ function configPath({ scope, codexDir, projectRoot, paths }: { scope?: string, c
|
|
|
29
29
|
throw new Error(`Unsupported Codex configuration scope: ${scope}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function fingerprint(value
|
|
32
|
+
function fingerprint(value ) {
|
|
33
33
|
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function commentSuffix(line
|
|
36
|
+
function commentSuffix(line ) {
|
|
37
37
|
const withoutComment = stripComment(line);
|
|
38
38
|
return line.slice(withoutComment.length);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function renderValue(value
|
|
41
|
+
function renderValue(value ) {
|
|
42
42
|
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
43
43
|
if (typeof value === 'string') return JSON.stringify(value);
|
|
44
44
|
throw new Error('Managed Codex config values must be boolean, number, or string');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function normaliseDesired(resources
|
|
48
|
-
const result = new Map
|
|
47
|
+
function normaliseDesired(resources ) {
|
|
48
|
+
const result = new Map ();
|
|
49
49
|
for (const resource of resources || []) {
|
|
50
50
|
if (!resource || (resource.kind && resource.kind !== CONFIG_KIND && resource.kind !== 'config-entry')) continue;
|
|
51
51
|
const identity = resource.identity ?? resource.key;
|
|
@@ -62,49 +62,49 @@ function normaliseDesired(resources: any): Map<string, any> {
|
|
|
62
62
|
|
|
63
63
|
/** The comment on a line DoFlow added to a table the user opened with no entry: it is how removal
|
|
64
64
|
* knows to keep the user's header and restore the file's exact bytes. */
|
|
65
|
-
function addedMarker(table
|
|
65
|
+
function addedMarker(table ) { return ` # DoFlow added this to your [${table}] table`; }
|
|
66
66
|
|
|
67
67
|
/** Where a new entry `<table>.<key>` goes in the parsed file: after the last entry of its table;
|
|
68
68
|
* on the line right after the table's empty header, marked; as a root dotted line after the last
|
|
69
69
|
* root dotted line of that table; refused (`valueEntry`) when the table or a parent of it is set as
|
|
70
70
|
* a value, which TOML cannot extend; otherwise in a new table (no `at`). */
|
|
71
|
-
function placeEntry(identity
|
|
71
|
+
function placeEntry(identity , value , parsed ) {
|
|
72
72
|
const parts = identity.split('.');
|
|
73
73
|
const table = parts.slice(0, -1).join('.');
|
|
74
74
|
const rendered = renderValue(value);
|
|
75
75
|
const assignment = `${parts.at(-1)} = ${rendered}`;
|
|
76
|
-
const entries
|
|
77
|
-
const after = (items
|
|
78
|
-
const inTable = entries.filter(([, entry]
|
|
76
|
+
const entries = [...parsed.entries];
|
|
77
|
+
const after = (items ) => Math.max(...items.map(([, entry] ) => entry.line)) + 1;
|
|
78
|
+
const inTable = entries.filter(([, entry] ) => entry.table === table);
|
|
79
79
|
if (inTable.length) return { table, at: after(inTable), line: assignment };
|
|
80
|
-
const header = parsed.headers.find((item
|
|
80
|
+
const header = parsed.headers.find((item ) => item.table === table);
|
|
81
81
|
if (header) return { table, at: header.line + 1, line: `${assignment}${addedMarker(table)}` };
|
|
82
|
-
const dotted = entries.filter(([key, entry]
|
|
82
|
+
const dotted = entries.filter(([key, entry] ) => entry.table === '' && key.startsWith(`${table}.`));
|
|
83
83
|
if (dotted.length) return { table, at: after(dotted), line: `${identity} = ${rendered}` };
|
|
84
|
-
const valueEntry = entries.find(([key]
|
|
84
|
+
const valueEntry = entries.find(([key] ) => key === table || table.startsWith(`${key}.`));
|
|
85
85
|
if (valueEntry) return { table, valueEntry };
|
|
86
86
|
return { table, line: assignment };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/** Whether a managed entry's line is one placeEntry put into the user's own table: marked, or
|
|
90
90
|
* written as a dotted key outside its table. */
|
|
91
|
-
function placedLine(identity
|
|
91
|
+
function placedLine(identity , entry , lines ) {
|
|
92
92
|
const table = identity.split('.').slice(0, -1).join('.');
|
|
93
93
|
return entry.table !== table || lines[entry.line].trimEnd().endsWith(addedMarker(table));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** A conflict when `content` breaks a TOML table rule that `original` did not already break, so a
|
|
97
97
|
* file that was valid is never written invalid; null otherwise. */
|
|
98
|
-
function newViolation({ file, original, content, identities }
|
|
99
|
-
const refused = (reason
|
|
98
|
+
function newViolation({ file, original, content, identities } ) {
|
|
99
|
+
const refused = (reason ) => `${file}: DoFlow cannot write ${identities.map((identity) => `'${identity}'`).join(', ')} without making the file invalid TOML (${reason}). Nothing was written.`;
|
|
100
100
|
const before = tableViolations(original);
|
|
101
101
|
let after;
|
|
102
|
-
try { after = tableViolations(content); } catch (error
|
|
103
|
-
const added = after.find((item
|
|
102
|
+
try { after = tableViolations(content); } catch (error ) { return refused(error.message); }
|
|
103
|
+
const added = after.find((item ) => !before.some((known ) => known.kind === item.kind && known.table === item.table));
|
|
104
104
|
return added ? refused(`${added.kind} '${added.table}' on line ${added.line}`) : null;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function isOwnedRecord(resource
|
|
107
|
+
function isOwnedRecord(resource , scope ) {
|
|
108
108
|
return resource && resource.target === 'codex' && resource.scope === scope &&
|
|
109
109
|
(resource.kind === CONFIG_KIND || resource.kind === 'config-entry') && typeof resource.identity === 'string';
|
|
110
110
|
}
|
|
@@ -118,20 +118,20 @@ function isOwnedRecord(resource: any, scope: any): boolean {
|
|
|
118
118
|
* entries written before the neutral ledger existed could never be re-adopted — the condition
|
|
119
119
|
* blocking the install was the same condition an install had to run to clear.
|
|
120
120
|
*/
|
|
121
|
-
function planCodexConfig({ file, scope, managedResources = [], desiredResources = [], adopt = false }
|
|
121
|
+
function planCodexConfig({ file, scope, managedResources = [], desiredResources = [], adopt = false } ) {
|
|
122
122
|
const original = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
123
123
|
let parsed;
|
|
124
|
-
try { parsed = parseToml(original); } catch (error
|
|
124
|
+
try { parsed = parseToml(original); } catch (error ) {
|
|
125
125
|
return { ok: false, status: 'malformed', file, original, changes: [], conflicts: [error.message], managedResources };
|
|
126
126
|
}
|
|
127
127
|
let desired;
|
|
128
|
-
try { desired = normaliseDesired(desiredResources); } catch (error
|
|
128
|
+
try { desired = normaliseDesired(desiredResources); } catch (error ) {
|
|
129
129
|
return { ok: false, status: 'invalid-request', file, original, changes: [], conflicts: [error.message], managedResources };
|
|
130
130
|
}
|
|
131
|
-
const owned = new Map
|
|
132
|
-
const changes
|
|
133
|
-
const conflicts
|
|
134
|
-
const placements = new Map
|
|
131
|
+
const owned = new Map (managedResources.filter((resource ) => isOwnedRecord(resource, scope)).map((resource ) => [resource.identity, resource]));
|
|
132
|
+
const changes = [];
|
|
133
|
+
const conflicts = [];
|
|
134
|
+
const placements = new Map ();
|
|
135
135
|
const all = new Set([...desired.keys(), ...owned.keys()]);
|
|
136
136
|
for (const identity of all) {
|
|
137
137
|
const entry = parsed.entries.get(identity);
|
|
@@ -164,10 +164,10 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
|
|
|
164
164
|
}
|
|
165
165
|
if (conflicts.length) return { ok: false, status: 'conflict', file, original, changes: [], conflicts, managedResources };
|
|
166
166
|
const nextLines = [...parsed.lines];
|
|
167
|
-
for (const change of changes.filter((change
|
|
167
|
+
for (const change of changes.filter((change ) => change.type === 'update')) {
|
|
168
168
|
const key = change.identity.split('.').at(-1);
|
|
169
169
|
const originalLine = nextLines[change.line];
|
|
170
|
-
const indentation = originalLine.match(/^\s*/)
|
|
170
|
+
const indentation = originalLine.match(/^\s*/) [0];
|
|
171
171
|
// A user comment beside an owned value is still user content. Retain it even though the
|
|
172
172
|
// owned scalar changes, so reconciliation does not silently erase unrelated explanation.
|
|
173
173
|
const suffix = commentSuffix(originalLine);
|
|
@@ -175,18 +175,18 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
|
|
|
175
175
|
}
|
|
176
176
|
// A line DoFlow placed in the user's own table (marker or dotted form) is deleted outright and its
|
|
177
177
|
// header kept, so the file returns to its exact bytes. Any other removal leaves a blank line.
|
|
178
|
-
const removals = changes.filter((change
|
|
179
|
-
const placedInUserTable = removals.filter((change
|
|
178
|
+
const removals = changes.filter((change ) => change.type === 'remove');
|
|
179
|
+
const placedInUserTable = removals.filter((change ) => placedLine(change.identity, parsed.entries.get(change.identity), parsed.lines));
|
|
180
180
|
for (const change of removals) nextLines[change.line] = placedInUserTable.includes(change) ? null : '';
|
|
181
|
-
dropEmptiedTables(nextLines, changes.filter((change
|
|
181
|
+
dropEmptiedTables(nextLines, changes.filter((change ) => !placedInUserTable.includes(change)), parsed.headers);
|
|
182
182
|
// A new key must land INSIDE its own table. Appending at end-of-file only happens to be
|
|
183
183
|
// correct when that table is the file's last one — otherwise the key silently joins whichever
|
|
184
184
|
// table trails the file, so `features.hooks` written after an `[mcp_servers.x]` block becomes
|
|
185
185
|
// `mcp_servers.x.hooks`. placeEntry chooses the spot; genuinely new tables are appended once
|
|
186
186
|
// each, with all of their keys grouped under a single header.
|
|
187
|
-
const insertions
|
|
188
|
-
const newTables = new Map
|
|
189
|
-
for (const change of changes.filter((change
|
|
187
|
+
const insertions = [];
|
|
188
|
+
const newTables = new Map ();
|
|
189
|
+
for (const change of changes.filter((change ) => change.type === 'create')) {
|
|
190
190
|
const placement = placements.get(change.identity);
|
|
191
191
|
if (placement.at !== undefined) insertions.push({ at: placement.at, line: placement.line });
|
|
192
192
|
else newTables.set(placement.table, [...(newTables.get(placement.table) || []), placement.line]);
|
|
@@ -204,11 +204,11 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
|
|
|
204
204
|
}
|
|
205
205
|
let content = nextLines.join('\n');
|
|
206
206
|
if (content && !content.endsWith('\n')) content += '\n';
|
|
207
|
-
const invalid = newViolation({ file, original, content, identities: changes.map((change
|
|
207
|
+
const invalid = newViolation({ file, original, content, identities: changes.map((change ) => change.identity) });
|
|
208
208
|
if (invalid) return { ok: false, status: 'conflict', file, original, changes: [], conflicts: [invalid], managedResources };
|
|
209
|
-
const nextManagedResources = managedResources.filter((resource
|
|
209
|
+
const nextManagedResources = managedResources.filter((resource ) => !isOwnedRecord(resource, scope) || desired.has(resource.identity));
|
|
210
210
|
for (const resource of desired.values()) {
|
|
211
|
-
const index = nextManagedResources.findIndex((item
|
|
211
|
+
const index = nextManagedResources.findIndex((item ) => isOwnedRecord(item, scope) && item.identity === resource.identity);
|
|
212
212
|
const updated = { ...resource, target: 'codex', scope, kind: CONFIG_KIND, fingerprint: fingerprint(resource.value) };
|
|
213
213
|
if (index >= 0) nextManagedResources[index] = updated;
|
|
214
214
|
else nextManagedResources.push(updated);
|
|
@@ -221,17 +221,17 @@ function planCodexConfig({ file, scope, managedResources = [], desiredResources
|
|
|
221
221
|
* it was the file's last table, the blank line the table append puts before a new header goes too.
|
|
222
222
|
* A table that also receives a created key, or whose header line carries a comment, is kept.
|
|
223
223
|
* Headers come from the parser's own records, never from re-reading lines. */
|
|
224
|
-
function dropEmptiedTables(lines
|
|
225
|
-
const tableOf = (identity
|
|
226
|
-
const removedTables = new Map
|
|
227
|
-
const createdTables = new Set
|
|
228
|
-
const headers
|
|
224
|
+
function dropEmptiedTables(lines , changes , headerRecords ) {
|
|
225
|
+
const tableOf = (identity ) => identity.split('.').slice(0, -1).join('.');
|
|
226
|
+
const removedTables = new Map (changes.filter((change) => change.type === 'remove').map((change) => [change.line, tableOf(change.identity)]));
|
|
227
|
+
const createdTables = new Set (changes.filter((change) => change.type === 'create').map((change) => tableOf(change.identity)));
|
|
228
|
+
const headers = headerRecords.map((header) => header.line);
|
|
229
229
|
headers.forEach((start, position) => {
|
|
230
230
|
const end = headers[position + 1] ?? lines.length;
|
|
231
231
|
if (stripComment(lines[start]) !== lines[start]) return;
|
|
232
|
-
const body = lines.slice(start + 1, end).map((_
|
|
232
|
+
const body = lines.slice(start + 1, end).map((_ , offset ) => start + 1 + offset);
|
|
233
233
|
const removedHere = body.filter((index) => removedTables.has(index));
|
|
234
|
-
if (!removedHere.length || removedHere.some((index) => createdTables.has(removedTables.get(index)
|
|
234
|
+
if (!removedHere.length || removedHere.some((index) => createdTables.has(removedTables.get(index) ))) return;
|
|
235
235
|
if (body.some((index) => !removedTables.has(index) && lines[index] !== null && lines[index].trim() !== '')) return;
|
|
236
236
|
lines[start] = null;
|
|
237
237
|
for (const index of removedHere) lines[index] = null;
|
|
@@ -241,7 +241,7 @@ function dropEmptiedTables(lines: any[], changes: any[], headerRecords: any[]):
|
|
|
241
241
|
|
|
242
242
|
/** Replace a file through a sibling temporary file, keeping the existing file's permission bits so
|
|
243
243
|
* a config kept private stays so. A file this creates gets the process's default mode. */
|
|
244
|
-
function atomicWrite(file
|
|
244
|
+
function atomicWrite(file , content , fsImpl = fs) {
|
|
245
245
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
246
246
|
const mode = fsImpl.existsSync(file) ? fsImpl.statSync(file).mode & 0o7777 : null;
|
|
247
247
|
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
@@ -254,7 +254,7 @@ function atomicWrite(file: string, content: string, fsImpl: any = fs): void {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
function applyCodexConfig(plan
|
|
257
|
+
function applyCodexConfig(plan , { dryRun = false, fsImpl = fs } = {}) {
|
|
258
258
|
if (!plan.ok) return { ...plan, applied: false };
|
|
259
259
|
if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
|
|
260
260
|
// A removal that leaves nothing behind deletes the file rather than leaving it empty.
|
|
@@ -263,7 +263,7 @@ function applyCodexConfig(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?:
|
|
|
263
263
|
return { ...plan, applied: true };
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
function reconcileCodexConfig(options
|
|
266
|
+
function reconcileCodexConfig(options ) {
|
|
267
267
|
const file = options.file ?? configPath(options);
|
|
268
268
|
const plan = planCodexConfig({ ...options, file });
|
|
269
269
|
return applyCodexConfig(plan, options);
|
|
@@ -7,19 +7,20 @@
|
|
|
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
|
|
11
|
-
const {
|
|
12
|
-
const {
|
|
10
|
+
const crypto = require('node:crypto');
|
|
11
|
+
const { verifyHookCommands } = require('../hook-commands.js');
|
|
12
|
+
const { mergeHooks } = require('../../helper/settings-merge.js');
|
|
13
|
+
const { atomicWrite } = require('./config.js');
|
|
13
14
|
|
|
14
15
|
const SUPPORTED_EVENTS = new Set([
|
|
15
16
|
'PreToolUse', 'PermissionRequest', 'PostToolUse', 'PreCompact', 'PostCompact',
|
|
16
17
|
'UserPromptSubmit', 'SubagentStop', 'Stop', 'SessionStart', 'SubagentStart', 'SessionEnd',
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
|
-
function validationError(errors
|
|
20
|
+
function validationError(errors , message ) { errors.push(message); }
|
|
20
21
|
|
|
21
|
-
function validateHooksConfig(config
|
|
22
|
-
const errors
|
|
22
|
+
function validateHooksConfig(config ) {
|
|
23
|
+
const errors = [];
|
|
23
24
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
24
25
|
return { ok: false, errors: ['Hook configuration must be an object'], events: [] };
|
|
25
26
|
}
|
|
@@ -35,7 +36,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
35
36
|
validationError(errors, `Hook event '${event}' requires a non-empty array of matcher groups`);
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
|
-
groups.forEach((group
|
|
39
|
+
groups.forEach((group , groupIndex ) => {
|
|
39
40
|
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
40
41
|
validationError(errors, `Hook event '${event}' group ${groupIndex} must be an object`);
|
|
41
42
|
return;
|
|
@@ -47,7 +48,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
47
48
|
validationError(errors, `Hook event '${event}' group ${groupIndex} requires a non-empty hooks array`);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
|
-
group.hooks.forEach((handler
|
|
51
|
+
group.hooks.forEach((handler , handlerIndex ) => {
|
|
51
52
|
const label = `Hook event '${event}' group ${groupIndex} handler ${handlerIndex}`;
|
|
52
53
|
if (!handler || typeof handler !== 'object' || Array.isArray(handler)) {
|
|
53
54
|
validationError(errors, `${label} must be an object`);
|
|
@@ -73,9 +74,9 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/** Maps Claude guardrail intent, never fabricating a Codex equivalent for unavailable events. */
|
|
76
|
-
function classifyClaudeGuardrails(claudeHooks
|
|
77
|
-
const supported
|
|
78
|
-
const unsupported
|
|
77
|
+
function classifyClaudeGuardrails(claudeHooks = {}) {
|
|
78
|
+
const supported = [];
|
|
79
|
+
const unsupported = [];
|
|
79
80
|
for (const [event, groups] of Object.entries(claudeHooks || {})) {
|
|
80
81
|
const item = { sourceEvent: event, targetEvent: event, groups: Array.isArray(groups) ? groups.length : 0 };
|
|
81
82
|
if (SUPPORTED_EVENTS.has(event)) supported.push(item);
|
|
@@ -84,7 +85,7 @@ function classifyClaudeGuardrails(claudeHooks: any = {}): { supported: any[], un
|
|
|
84
85
|
return { supported, unsupported };
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function destinationFor(context
|
|
88
|
+
function destinationFor(context = {}) {
|
|
88
89
|
// Preferred: the declared path resolved by the adapter (harnesses.json "paths".hooksFile).
|
|
89
90
|
if (context.paths?.hooksFile) return context.paths.hooksFile;
|
|
90
91
|
// Legacy derivation for direct callers passing raw scope inputs; kept byte-compatible with the
|
|
@@ -102,20 +103,45 @@ function destinationFor(context: any = {}): string {
|
|
|
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'));
|
|
109
|
-
} catch (error
|
|
135
|
+
} catch (error ) {
|
|
110
136
|
return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${sourceFile}: ${error.message}`] };
|
|
111
137
|
}
|
|
112
138
|
const schema = validateHooksConfig(desired);
|
|
113
139
|
let destination;
|
|
114
|
-
try { destination = destinationFor(destinationContext); } catch (error
|
|
140
|
+
try { destination = destinationFor(destinationContext); } catch (error ) {
|
|
115
141
|
return { ok: false, status: 'invalid-request', changes: [], errors: [...schema.errors, error.message] };
|
|
116
142
|
}
|
|
117
143
|
const commands = verifyHookCommands(desired || {}, { scriptsDir: sourceHooksDir, trusted, fsImpl });
|
|
118
|
-
const errors = [...schema.errors, ...commands.checks.filter((check
|
|
144
|
+
const errors = [...schema.errors, ...commands.checks.filter((check ) => !check.ok).map((check ) => `${check.event}: ${check.reason}${check.script ? ` (${check.script})` : ''}`)];
|
|
119
145
|
let mergedDesired = desired;
|
|
120
146
|
const original = fsImpl.existsSync(destination) ? fsImpl.readFileSync(destination, 'utf8') : '';
|
|
121
147
|
if (original.trim()) {
|
|
@@ -130,25 +156,34 @@ 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
|
-
...commands.checks.map((check
|
|
140
|
-
...shippedHookScripts(sourceHooksDir, fsImpl).map((name
|
|
174
|
+
...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
|
|
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
|
}
|
|
146
181
|
|
|
147
|
-
function deployCodexHooks(plan
|
|
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);
|
|
151
|
-
const deployed = new Set
|
|
185
|
+
if (!plan.scriptsOnly) atomicWrite(plan.destination, plan.content, fsImpl);
|
|
186
|
+
const deployed = new Set ();
|
|
152
187
|
for (const check of plan.commands.checks) {
|
|
153
188
|
const target = path.join(plan.destinationHooksDir, check.script);
|
|
154
189
|
fsImpl.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -174,16 +209,16 @@ function deployCodexHooks(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?:
|
|
|
174
209
|
|
|
175
210
|
/** Every file deployCodexHooks copies beside hooks.json: each regular, non-JSON file in the
|
|
176
211
|
* shipped scripts directory (the command-named scripts are among them). */
|
|
177
|
-
function shippedHookScripts(scriptsDir
|
|
212
|
+
function shippedHookScripts(scriptsDir , fsImpl = fs) {
|
|
178
213
|
if (!scriptsDir || !fsImpl.existsSync(scriptsDir)) return [];
|
|
179
214
|
return fsImpl.readdirSync(scriptsDir)
|
|
180
|
-
.filter((name
|
|
215
|
+
.filter((name ) => !name.endsWith('.json') && fsImpl.statSync(path.join(scriptsDir, name)).isFile());
|
|
181
216
|
}
|
|
182
217
|
|
|
183
218
|
/** Undo deployCodexHooks' script copies. The ledger records only hooks.json, so ownership of a
|
|
184
219
|
* script is proven by its bytes: one still identical to the shipped source is deleted, one the
|
|
185
220
|
* user edited or added stays. The directory goes only once it is empty. */
|
|
186
|
-
function removeCodexHookScripts({ scriptsDir, destinationHooksDir }
|
|
221
|
+
function removeCodexHookScripts({ scriptsDir, destinationHooksDir } = {}, { dryRun = false, fsImpl = fs } = {}) {
|
|
187
222
|
if (!destinationHooksDir || !fsImpl.existsSync(destinationHooksDir)) return { removed: 0 };
|
|
188
223
|
let removed = 0;
|
|
189
224
|
for (const name of shippedHookScripts(scriptsDir, fsImpl)) {
|
|
@@ -200,4 +235,4 @@ function removeCodexHookScripts({ scriptsDir, destinationHooksDir }: { scriptsDi
|
|
|
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 };
|