@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
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// if a third harness with the same shape is ever added.
|
|
15
15
|
const fs = require('node:fs');
|
|
16
16
|
const path = require('node:path');
|
|
17
|
-
const
|
|
18
|
-
const {
|
|
17
|
+
const crypto = require('node:crypto');
|
|
18
|
+
const { verifyHookCommands } = require('../hook-commands.js');
|
|
19
|
+
const { mergeHooks } = require('../../helper/settings-merge.js');
|
|
19
20
|
|
|
20
21
|
const SUPPORTED_EVENTS = new Set([
|
|
21
22
|
'SessionStart', 'SessionEnd', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'AfterModel',
|
|
@@ -27,7 +28,7 @@ const SUPPORTED_EVENTS = new Set([
|
|
|
27
28
|
// Stop, SubagentStart/Stop, and ConfigChange have no correct Gemini equivalent and are
|
|
28
29
|
// intentionally omitted rather than approximated onto BeforeAgent/AfterAgent (design.md §7/§8 —
|
|
29
30
|
// no false parity).
|
|
30
|
-
const CLAUDE_TO_GEMINI_EVENT
|
|
31
|
+
const CLAUDE_TO_GEMINI_EVENT = {
|
|
31
32
|
SessionStart: 'SessionStart',
|
|
32
33
|
SessionEnd: 'SessionEnd',
|
|
33
34
|
PreToolUse: 'BeforeTool',
|
|
@@ -35,10 +36,10 @@ const CLAUDE_TO_GEMINI_EVENT: Record<string, string> = {
|
|
|
35
36
|
PreCompact: 'PreCompress',
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
function validationError(errors
|
|
39
|
+
function validationError(errors , message ) { errors.push(message); }
|
|
39
40
|
|
|
40
|
-
function validateHooksConfig(config
|
|
41
|
-
const errors
|
|
41
|
+
function validateHooksConfig(config ) {
|
|
42
|
+
const errors = [];
|
|
42
43
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
43
44
|
return { ok: false, errors: ['Hook configuration must be an object'], events: [] };
|
|
44
45
|
}
|
|
@@ -54,7 +55,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
54
55
|
validationError(errors, `Hook event '${event}' requires a non-empty array of matcher groups`);
|
|
55
56
|
continue;
|
|
56
57
|
}
|
|
57
|
-
groups.forEach((group
|
|
58
|
+
groups.forEach((group , groupIndex ) => {
|
|
58
59
|
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
59
60
|
validationError(errors, `Hook event '${event}' group ${groupIndex} must be an object`);
|
|
60
61
|
return;
|
|
@@ -66,7 +67,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
66
67
|
validationError(errors, `Hook event '${event}' group ${groupIndex} requires a non-empty hooks array`);
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
|
-
group.hooks.forEach((handler
|
|
70
|
+
group.hooks.forEach((handler , handlerIndex ) => {
|
|
70
71
|
const label = `Hook event '${event}' group ${groupIndex} handler ${handlerIndex}`;
|
|
71
72
|
if (!handler || typeof handler !== 'object' || Array.isArray(handler)) {
|
|
72
73
|
validationError(errors, `${label} must be an object`); return;
|
|
@@ -85,9 +86,9 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
/** Maps Claude guardrail intent, never fabricating a Gemini equivalent for unavailable events. */
|
|
88
|
-
function classifyClaudeGuardrails(claudeHooks
|
|
89
|
-
const supported
|
|
90
|
-
const unsupported
|
|
89
|
+
function classifyClaudeGuardrails(claudeHooks = {}) {
|
|
90
|
+
const supported = [];
|
|
91
|
+
const unsupported = [];
|
|
91
92
|
for (const [event, groups] of Object.entries(claudeHooks || {})) {
|
|
92
93
|
const targetEvent = CLAUDE_TO_GEMINI_EVENT[event] || null;
|
|
93
94
|
const item = { sourceEvent: event, targetEvent, groups: Array.isArray(groups) ? groups.length : 0 };
|
|
@@ -103,58 +104,98 @@ function classifyClaudeGuardrails(claudeHooks: any = {}): { supported: any[], un
|
|
|
103
104
|
* exists but fails to parse is a hard error, not silently treated as empty (this path merges
|
|
104
105
|
* into content DoFlow does not own — a malformed-file work-around here risks losing it).
|
|
105
106
|
*/
|
|
106
|
-
|
|
107
|
+
/** Every regular, non-JSON file in the shipped scripts directory: what deployGeminiHooks copies. */
|
|
108
|
+
function shippedScripts(scriptsDir , fsImpl = fs) {
|
|
109
|
+
if (!scriptsDir || !fsImpl.existsSync(scriptsDir)) return [];
|
|
110
|
+
return fsImpl.readdirSync(scriptsDir)
|
|
111
|
+
.filter((name ) => !name.endsWith('.json') && fsImpl.statSync(path.join(scriptsDir, name)).isFile());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** DoFlow's record of a deployed hook script: the sha256 of its bytes. */
|
|
115
|
+
function scriptFingerprint(bytes ) { return crypto.createHash('sha256').update(bytes).digest('hex'); }
|
|
116
|
+
|
|
117
|
+
/** The scripts beside the config that differ from the shipped source (or are absent), by name, and
|
|
118
|
+
* those among them a ledger row says were edited since DoFlow wrote them. A script with no row
|
|
119
|
+
* (an install from before scripts were recorded) is DoFlow's to refresh: the config deploy always
|
|
120
|
+
* overwrote it. `rows` are this harness's script rows: { target, fingerprint }. */
|
|
121
|
+
function planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows = [], force = false, fsImpl = fs } ) {
|
|
122
|
+
const sources = new Map (commands.checks.map((check ) => [check.script, check.file] ));
|
|
123
|
+
for (const name of shippedScripts(sourceHooksDir, fsImpl)) if (!sources.has(name)) sources.set(name, path.join(sourceHooksDir, name));
|
|
124
|
+
const stale = [];
|
|
125
|
+
const edited = [];
|
|
126
|
+
for (const [name, source] of sources) {
|
|
127
|
+
const target = path.join(destinationHooksDir, name);
|
|
128
|
+
if (fsImpl.existsSync(target)) {
|
|
129
|
+
const current = fsImpl.readFileSync(target);
|
|
130
|
+
if (current.equals(fsImpl.readFileSync(source))) continue;
|
|
131
|
+
const row = rows.find((item ) => path.resolve(item.target) === path.resolve(target));
|
|
132
|
+
if (row && row.fingerprint !== scriptFingerprint(current) && !force) edited.push(name);
|
|
133
|
+
}
|
|
134
|
+
stale.push(name);
|
|
135
|
+
}
|
|
136
|
+
return { names: [...sources.keys()], stale, edited };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, destinationHooksDir: declaredHooksDir, trusted = false, scriptRows = [], force = false, fsImpl = fs } = {}) {
|
|
107
140
|
let desired = config;
|
|
108
141
|
try {
|
|
109
142
|
if (!desired && sourceFile) desired = JSON.parse(fsImpl.readFileSync(sourceFile, 'utf8'));
|
|
110
|
-
} catch (error
|
|
143
|
+
} catch (error ) {
|
|
111
144
|
return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${sourceFile}: ${error.message}`] };
|
|
112
145
|
}
|
|
113
146
|
if (!settingsFile) return { ok: false, status: 'invalid-request', changes: [], errors: ['settingsFile is required'] };
|
|
114
147
|
const schema = validateHooksConfig(desired);
|
|
115
148
|
const commands = verifyHookCommands(desired || {}, { scriptsDir: sourceHooksDir, trusted, fsImpl });
|
|
116
|
-
const errors = [...schema.errors, ...commands.checks.filter((check
|
|
149
|
+
const errors = [...schema.errors, ...commands.checks.filter((check ) => !check.ok).map((check ) => `${check.event}: ${check.reason}${check.script ? ` (${check.script})` : ''}`)];
|
|
117
150
|
|
|
118
|
-
let existing
|
|
151
|
+
let existing = {};
|
|
119
152
|
if (fsImpl.existsSync(settingsFile)) {
|
|
120
153
|
try { existing = JSON.parse(fsImpl.readFileSync(settingsFile, 'utf8')); }
|
|
121
|
-
catch (error
|
|
154
|
+
catch (error ) { return { ok: false, status: 'malformed', changes: [], errors: [`Refusing to touch malformed ${settingsFile}: ${error.message}`] }; }
|
|
122
155
|
}
|
|
123
156
|
if (errors.length) return { ok: false, status: 'invalid-request', settingsFile, existing, changes: [], errors, commands };
|
|
124
157
|
|
|
125
158
|
const mergedHooks = existing.hooks ? mergeHooks(existing.hooks, desired.hooks) : desired.hooks;
|
|
126
159
|
const merged = { ...existing, hooks: mergedHooks };
|
|
127
|
-
const
|
|
160
|
+
const configChanged = JSON.stringify(existing.hooks) !== JSON.stringify(mergedHooks);
|
|
128
161
|
// Preferred: the declared hooks directory (harnesses.json "paths".hooksDirectory); the derived
|
|
129
162
|
// sibling-of-settings fallback stays for direct callers passing raw inputs, and resolves to the
|
|
130
163
|
// same location.
|
|
131
164
|
const destinationHooksDir = declaredHooksDir || path.join(path.dirname(settingsFile), 'hooks');
|
|
165
|
+
// The scripts are deployed with the hooks key but are not part of it: a script whose bytes differ
|
|
166
|
+
// from the source is redeployed even when the key is already right.
|
|
167
|
+
const scripts = planHookScripts({ commands, sourceHooksDir, destinationHooksDir, rows: scriptRows, force, fsImpl });
|
|
168
|
+
if (scripts.edited.length) {
|
|
169
|
+
return { ok: false, status: 'conflict', settingsFile, existing, changes: [], commands,
|
|
170
|
+
errors: scripts.edited.map((name ) => `Gemini hook script '${name}' was modified outside DoFlow`) };
|
|
171
|
+
}
|
|
172
|
+
const scriptsOnly = !configChanged && scripts.stale.length > 0;
|
|
173
|
+
const changed = configChanged || scriptsOnly;
|
|
132
174
|
// The scripts deployGeminiHooks copies beside settings.json, so a backup of this change holds them too.
|
|
133
|
-
const shipped = sourceHooksDir && fsImpl.existsSync(sourceHooksDir)
|
|
134
|
-
? fsImpl.readdirSync(sourceHooksDir).filter((name: string) => !name.endsWith('.json') && fsImpl.statSync(path.join(sourceHooksDir, name)).isFile())
|
|
135
|
-
: [];
|
|
136
175
|
const companionTargets = [...new Set([
|
|
137
|
-
...commands.checks.map((check
|
|
138
|
-
...
|
|
176
|
+
...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
|
|
177
|
+
...shippedScripts(sourceHooksDir, fsImpl).map((name ) => path.join(destinationHooksDir, name)),
|
|
139
178
|
])].sort();
|
|
140
|
-
return { ok: true, status: changed ? 'change' : 'unchanged', settingsFile, existing, merged,
|
|
179
|
+
return { ok: true, status: changed ? 'change' : 'unchanged', settingsFile, existing, merged, scriptsOnly, scriptNames: scripts.names,
|
|
141
180
|
changes: changed ? [{ type: fsImpl.existsSync(settingsFile) ? 'update' : 'create', file: settingsFile, key: 'hooks' }] : [],
|
|
142
181
|
errors: [], commands, trust: commands.trust, scriptsDir: sourceHooksDir, destinationHooksDir, companionTargets };
|
|
143
182
|
}
|
|
144
183
|
|
|
145
|
-
function deployGeminiHooks(plan
|
|
184
|
+
function deployGeminiHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
|
|
146
185
|
if (!plan.ok) return { ...plan, applied: false };
|
|
147
186
|
if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
187
|
+
if (!plan.scriptsOnly) {
|
|
188
|
+
fsImpl.mkdirSync(path.dirname(plan.settingsFile), { recursive: true });
|
|
189
|
+
const content = `${JSON.stringify(plan.merged, null, 2)}\n`;
|
|
190
|
+
const temporary = path.join(path.dirname(plan.settingsFile), `.${path.basename(plan.settingsFile)}.${process.pid}.${Date.now()}.tmp`);
|
|
191
|
+
try {
|
|
192
|
+
fsImpl.writeFileSync(temporary, content, { flag: 'wx' });
|
|
193
|
+
fsImpl.renameSync(temporary, plan.settingsFile);
|
|
194
|
+
} finally {
|
|
195
|
+
if (fsImpl.existsSync(temporary)) fsImpl.unlinkSync(temporary);
|
|
196
|
+
}
|
|
156
197
|
}
|
|
157
|
-
const deployed = new Set
|
|
198
|
+
const deployed = new Set ();
|
|
158
199
|
for (const check of plan.commands.checks) {
|
|
159
200
|
const target = path.join(plan.destinationHooksDir, check.script);
|
|
160
201
|
fsImpl.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -182,18 +223,18 @@ function deployGeminiHooks(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?
|
|
|
182
223
|
* cross-check): if the key isn't present, there's nothing to do; otherwise propose removing only
|
|
183
224
|
* that key, leaving every other key in the shared file untouched (FR-002).
|
|
184
225
|
*/
|
|
185
|
-
function planRemoveGeminiHooks({ settingsFile, fsImpl = fs }
|
|
226
|
+
function planRemoveGeminiHooks({ settingsFile, fsImpl = fs } = {}) {
|
|
186
227
|
if (!settingsFile) return { ok: false, status: 'invalid-request', changes: [], errors: ['settingsFile is required'] };
|
|
187
228
|
if (!fsImpl.existsSync(settingsFile)) return { ok: true, status: 'unchanged', settingsFile, changes: [] };
|
|
188
229
|
let existing;
|
|
189
230
|
try { existing = JSON.parse(fsImpl.readFileSync(settingsFile, 'utf8')); }
|
|
190
|
-
catch (error
|
|
231
|
+
catch (error ) { return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${settingsFile}: ${error.message}`] }; }
|
|
191
232
|
if (existing.hooks === undefined) return { ok: true, status: 'unchanged', settingsFile, changes: [] };
|
|
192
233
|
const { hooks: _dropped, ...merged } = existing;
|
|
193
234
|
return { ok: true, status: 'change', settingsFile, merged, changes: [{ type: 'update', file: settingsFile, key: 'hooks', operation: 'remove' }] };
|
|
194
235
|
}
|
|
195
236
|
|
|
196
|
-
function deployRemoveGeminiHooks(plan
|
|
237
|
+
function deployRemoveGeminiHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
|
|
197
238
|
if (!plan.ok || plan.status !== 'change' || dryRun) return { ...plan, applied: false };
|
|
198
239
|
fsImpl.mkdirSync(path.dirname(plan.settingsFile), { recursive: true });
|
|
199
240
|
const content = `${JSON.stringify(plan.merged, null, 2)}\n`;
|
|
@@ -207,4 +248,4 @@ function deployRemoveGeminiHooks(plan: any, { dryRun = false, fsImpl = fs }: { d
|
|
|
207
248
|
return { ...plan, applied: true };
|
|
208
249
|
}
|
|
209
250
|
|
|
210
|
-
module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planGeminiHooks, deployGeminiHooks, planRemoveGeminiHooks, deployRemoveGeminiHooks };
|
|
251
|
+
module.exports = { SUPPORTED_EVENTS, validateHooksConfig, classifyClaudeGuardrails, verifyHookCommands, planGeminiHooks, deployGeminiHooks, scriptFingerprint, shippedScripts, planRemoveGeminiHooks, deployRemoveGeminiHooks };
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// settings/MCP/extensions as first-class native-surface results for the lifecycle UI.
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const path = require('node:path');
|
|
10
|
-
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.
|
|
11
|
-
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.
|
|
12
|
-
const { planGeminiHooks, deployGeminiHooks, planRemoveGeminiHooks, deployRemoveGeminiHooks } = require('./hooks.
|
|
10
|
+
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.js');
|
|
11
|
+
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
|
|
12
|
+
const { planGeminiHooks, deployGeminiHooks, planRemoveGeminiHooks, deployRemoveGeminiHooks, scriptFingerprint } = require('./hooks.js');
|
|
13
13
|
|
|
14
14
|
const MARKER_START = '<!-- doflow:start -->';
|
|
15
15
|
const MARKER_END = '<!-- doflow:end -->';
|
|
@@ -22,12 +22,12 @@ const MARKER_END = '<!-- doflow:end -->';
|
|
|
22
22
|
const DEFAULTS_FILE = path.resolve(__dirname, '../../../core/harnesses/gemini/settings/adapter-defaults.json');
|
|
23
23
|
const HARNESS = 'gemini';
|
|
24
24
|
|
|
25
|
-
function defaults({ fsImpl = fs }
|
|
25
|
+
function defaults({ fsImpl = fs } = {}) { return JSON.parse(fsImpl.readFileSync(DEFAULTS_FILE, 'utf8')); }
|
|
26
26
|
|
|
27
27
|
/** Pure string helper (no path facts): replace only the span between DoFlow's markers in an
|
|
28
28
|
* existing GEMINI.md body. An existing file with no DoFlow section is refused rather than appended
|
|
29
29
|
* to — GEMINI.md is shared with the user and other tools. */
|
|
30
|
-
function managedInstruction(existing
|
|
30
|
+
function managedInstruction(existing , rendered ) {
|
|
31
31
|
if (existing === null) return { ok: true, operation: 'create', content: rendered };
|
|
32
32
|
const start = existing.indexOf(MARKER_START);
|
|
33
33
|
const end = existing.indexOf(MARKER_END);
|
|
@@ -38,7 +38,7 @@ function managedInstruction(existing: any, rendered: any) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Server names in one settings read, or null when the file is absent or unreadable. */
|
|
41
|
-
function settingsServerNames(read
|
|
41
|
+
function settingsServerNames(read ) {
|
|
42
42
|
if (!read.exists || read.error || !read.value || typeof read.value !== 'object') return null;
|
|
43
43
|
const servers = read.value.mcpServers;
|
|
44
44
|
return Object.keys(servers && typeof servers === 'object' ? servers : {});
|
|
@@ -50,7 +50,7 @@ function settingsServerNames(read: any) {
|
|
|
50
50
|
* neither can be read. Gemini also merges system settings and extension servers, which DoFlow does
|
|
51
51
|
* not read, so the view is never complete (`mcpComplete: false`) and absence is never proof.
|
|
52
52
|
*/
|
|
53
|
-
function configuredGeminiMcp({ scope, settings, homeDir, fsImpl }
|
|
53
|
+
function configuredGeminiMcp({ scope, settings, homeDir, fsImpl } ) {
|
|
54
54
|
const own = settingsServerNames(settings);
|
|
55
55
|
const user = scope === 'project' && homeDir ? settingsServerNames(readJson(path.join(homeDir, '.gemini', 'settings.json'), { fsImpl })) : null;
|
|
56
56
|
const mcpConfigured = own || user ? [...new Set([...(own || []), ...(user || [])])].sort() : null;
|
|
@@ -66,13 +66,13 @@ function configuredGeminiMcp({ scope, settings, homeDir, fsImpl }: any) {
|
|
|
66
66
|
* historical shape.
|
|
67
67
|
*/
|
|
68
68
|
function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
|
|
69
|
-
function nativePaths({ scope, scopeRoot, homeDir, fsImpl = fs }
|
|
69
|
+
function nativePaths({ scope, scopeRoot, homeDir, fsImpl = fs } ) {
|
|
70
70
|
const resolved = resolveHarnessPaths(declaredPaths, { scope, scopeRoot, homeDir });
|
|
71
71
|
// Non-path native fact carried alongside the resolved surfaces (see DEFAULTS_FILE residue note).
|
|
72
72
|
return { ...resolved, extensionMode: defaults({ fsImpl }).extensionInstall };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
function discover({ scope, scopeRoot, context = {}, fsImpl = fs }
|
|
75
|
+
function discover({ scope, scopeRoot, context = {}, fsImpl = fs } ) {
|
|
76
76
|
const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir, fsImpl });
|
|
77
77
|
const instruction = fsImpl.existsSync(paths.instruction) ? fsImpl.readFileSync(paths.instruction, 'utf8') : null;
|
|
78
78
|
// Gemini takes no servers from DoFlow's MCP catalog.
|
|
@@ -81,7 +81,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
81
81
|
return { paths, instruction, settings, mcpOwned: [], mcpConfigured, mcpComplete };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function render({ content = '' }
|
|
84
|
+
function render({ content = '' } = {}) {
|
|
85
85
|
const body = String(content).trimEnd();
|
|
86
86
|
return `${MARKER_START}\n${body}\n${MARKER_END}\n`;
|
|
87
87
|
}
|
|
@@ -99,7 +99,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
99
99
|
* (instruction vs. the guidance tree), not on any single declared path — expressible only by
|
|
100
100
|
* joining and diffing, which is computation, not declaration.
|
|
101
101
|
*/
|
|
102
|
-
function geminiDestDir(paths
|
|
102
|
+
function geminiDestDir(paths , asset ) {
|
|
103
103
|
if (asset.nativeDir && asset.nativeDir.startsWith('../')) {
|
|
104
104
|
const dest = path.join(paths.root, asset.nativeDir.slice(3));
|
|
105
105
|
const rel = path.relative(paths.root, dest);
|
|
@@ -111,8 +111,8 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
111
111
|
return copyTreeDestDir(paths.configDir, asset);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
function rewriteGuidanceImport(content
|
|
115
|
-
const guidance = assets.find((item
|
|
114
|
+
function rewriteGuidanceImport(content , assets , paths ) {
|
|
115
|
+
const guidance = assets.find((item ) => item.id === 'guidance.context-layer');
|
|
116
116
|
if (!guidance?.nativeDir || !paths?.instruction || !paths?.configDir) return content;
|
|
117
117
|
const guidanceDir = geminiDestDir(paths, guidance);
|
|
118
118
|
const rel = path.relative(path.dirname(paths.instruction), guidanceDir).split(path.sep).join('/');
|
|
@@ -123,22 +123,22 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
123
123
|
return content.replace(/@\S*?(DOFLOW_CORE\.md)/g, `@${prefix}/$1`);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
function instructionContent({ assets, context, paths, fsImpl = fs }
|
|
126
|
+
function instructionContent({ assets, context, paths, fsImpl = fs } ) {
|
|
127
127
|
if (context.instructionContent !== undefined) return String(context.instructionContent);
|
|
128
|
-
const asset = assets.find((item
|
|
128
|
+
const asset = assets.find((item ) => item.id === 'guidance.core');
|
|
129
129
|
if (!asset || !context.repoRoot) return null;
|
|
130
130
|
const source = path.resolve(context.repoRoot, asset.source);
|
|
131
131
|
if (!fsImpl.existsSync(source)) return null;
|
|
132
132
|
return rewriteGuidanceImport(fsImpl.readFileSync(source, 'utf8'), assets, paths);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function policyStatuses(policies
|
|
136
|
-
return policies.map((policy
|
|
135
|
+
function policyStatuses(policies = []) {
|
|
136
|
+
return policies.map((policy ) => ({ id: policy.id, status: 'unavailable', fallback: 'guidance', reason: 'Gemini policy automation is not rendered by this adapter' }));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// ---- copy-tree assets (rules, skills, agents, modes, references) ----
|
|
140
140
|
|
|
141
|
-
function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs }
|
|
141
|
+
function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs } ) {
|
|
142
142
|
const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir, fsImpl });
|
|
143
143
|
const changes = [];
|
|
144
144
|
const conflicts = [];
|
|
@@ -156,10 +156,10 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
156
156
|
// Found by the guard written for the six adapters that forwarded nothing.
|
|
157
157
|
force: !removing && context?.force === true });
|
|
158
158
|
treeResults.push(result);
|
|
159
|
-
conflicts.push(...result.conflicts.map((reason
|
|
159
|
+
conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
|
|
160
160
|
// A hand-edited file at a path DoFlow no longer writes (the agents folder layout) is the
|
|
161
161
|
// user's: it stays, only its ownership row is released.
|
|
162
|
-
notices.push(...result.kept.map((item
|
|
162
|
+
notices.push(...result.kept.map((item ) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
|
|
163
163
|
for (const change of result.changes) {
|
|
164
164
|
changes.push({
|
|
165
165
|
assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
|
|
@@ -175,7 +175,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
175
175
|
return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
function applyCopyTreeAssets(changes
|
|
178
|
+
function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
|
|
179
179
|
let applied = 0;
|
|
180
180
|
const grouped = new Map();
|
|
181
181
|
for (const change of changes) {
|
|
@@ -190,13 +190,13 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
190
190
|
return applied;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function removeCopyTreeAssets(changes
|
|
194
|
-
const treeChanges = changes.filter((change
|
|
195
|
-
.map((change
|
|
193
|
+
function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
|
|
194
|
+
const treeChanges = changes.filter((change ) => change.projection?.renderer === 'copy-tree' && change.operation === 'remove' && !change.retained)
|
|
195
|
+
.map((change ) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
|
|
196
196
|
return removeTree({ changes: treeChanges, fsImpl }).removed;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs }
|
|
199
|
+
function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs } ) {
|
|
200
200
|
const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir, fsImpl });
|
|
201
201
|
const statuses = [];
|
|
202
202
|
const resources = [];
|
|
@@ -205,7 +205,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
205
205
|
const destDir = geminiDestDir(paths, asset);
|
|
206
206
|
const sourceDir = sourceDirFor(asset, context, fsImpl, 'Gemini');
|
|
207
207
|
const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
|
|
208
|
-
conflicts.push(...result.conflicts.map((reason
|
|
208
|
+
conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
|
|
209
209
|
for (const resource of result.resources) {
|
|
210
210
|
resources.push({
|
|
211
211
|
assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:gemini:copy-tree:${asset.id}:${resource.relPath}`,
|
|
@@ -225,21 +225,36 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
225
225
|
// integration) — the lifecycle layer's normalizeChange() requires every change's assetId to
|
|
226
226
|
// match a real registered asset, so this piggybacks on guidance.core's id the same way Codex's
|
|
227
227
|
// assetIdFor(options, 'hooks') does, rather than inventing an unregistered 'hooks' id.
|
|
228
|
-
function hooksAssetId(assets
|
|
229
|
-
return assets.find((asset
|
|
228
|
+
function hooksAssetId(assets ) {
|
|
229
|
+
return assets.find((asset ) => asset.id === 'guidance.core')?.id ?? assets[0]?.id;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** This scope's ledger rows for the hook scripts deployed beside settings.json. */
|
|
233
|
+
function scriptRows(ledger , scope ) {
|
|
234
|
+
return (ledger?.resources || []).filter((row ) => row?.harness === 'gemini' && row.kind === 'hooks-script' && (!row.scope || row.scope === scope));
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
/** Plan the hooks pseudo-component's single change (or none), mirroring the {changes, conflicts}
|
|
233
238
|
* shape planCopyTreeAssets already returns so plan() can fold it in the same way. The hook
|
|
234
239
|
* scripts directory beside settings.json is declared (paths.hooksDirectory); the merge/remove
|
|
235
|
-
* planning itself lives in ./hooks.ts. */
|
|
236
|
-
function planHooksChange({ removing, found, context, assets, fsImpl }
|
|
240
|
+
* planning itself lives in ./hooks.ts. A removal also releases the rows of the hook scripts. */
|
|
241
|
+
function planHooksChange({ removing, found, context, assets, ledger, scope, fsImpl } ) {
|
|
242
|
+
const component = planHooksComponentChange({ removing, found, context, assets, ledger, scope, fsImpl });
|
|
243
|
+
if (!removing) return component;
|
|
244
|
+
const releases = scriptRows(ledger, scope).map((row ) => ({ assetId: row.assetId, target: row.target, operation: 'remove',
|
|
245
|
+
ownershipIdentity: row.ownershipIdentity, kind: row.kind, identity: row.identity, projection: row.projection,
|
|
246
|
+
retained: true, retainedFor: [] }));
|
|
247
|
+
return { ...component, changes: [...component.changes, ...releases] };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function planHooksComponentChange({ removing, found, context, assets, ledger, scope, fsImpl } ) {
|
|
237
251
|
if (found.settings.error) return { changes: [], conflicts: [] };
|
|
238
252
|
const hooksPlan = removing
|
|
239
253
|
? planRemoveGeminiHooks({ settingsFile: found.paths.settings, fsImpl })
|
|
240
254
|
: (context.geminiHooksSourceFile
|
|
241
255
|
? planGeminiHooks({ sourceFile: context.geminiHooksSourceFile, sourceHooksDir: context.geminiHooksSourceDir,
|
|
242
|
-
settingsFile: found.paths.settings, destinationHooksDir: found.paths.hooksDirectory, trusted: context.hooksTrusted,
|
|
256
|
+
settingsFile: found.paths.settings, destinationHooksDir: found.paths.hooksDirectory, trusted: context.hooksTrusted,
|
|
257
|
+
scriptRows: scriptRows(ledger, scope), force: context.force === true, fsImpl })
|
|
243
258
|
: null);
|
|
244
259
|
if (hooksPlan && !hooksPlan.ok) return { changes: [], conflicts: hooksPlan.errors || [] };
|
|
245
260
|
if (hooksPlan?.status !== 'change') return { changes: [], conflicts: [] };
|
|
@@ -248,7 +263,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
248
263
|
companionTargets: removing ? [] : (hooksPlan.companionTargets ?? []) }], conflicts: [] };
|
|
249
264
|
}
|
|
250
265
|
|
|
251
|
-
function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs }
|
|
266
|
+
function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs } ) {
|
|
252
267
|
const found = discovery || discover({ scope, scopeRoot, context, fsImpl });
|
|
253
268
|
const changes = []; const conflicts = [];
|
|
254
269
|
const removing = context.operation === 'remove';
|
|
@@ -256,7 +271,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
256
271
|
// Removal only ever targets a section this adapter itself owns — a foreign or absent
|
|
257
272
|
// GEMINI.md yields no change, never a forced deletion of user content.
|
|
258
273
|
const owns = found.instruction !== null && found.instruction.includes(MARKER_START) && found.instruction.includes(MARKER_END);
|
|
259
|
-
if (assets.some((asset
|
|
274
|
+
if (assets.some((asset ) => asset.id === 'guidance.core') && owns) {
|
|
260
275
|
changes.push({ assetId: 'guidance.core', target: found.paths.instruction, operation: 'remove', ownershipIdentity: 'gemini:instructions', projection: { renderer: 'gemini-instructions' } });
|
|
261
276
|
}
|
|
262
277
|
} else {
|
|
@@ -270,21 +285,21 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
270
285
|
const copyTree = planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl });
|
|
271
286
|
changes.push(...copyTree.changes); conflicts.push(...copyTree.conflicts);
|
|
272
287
|
if (found.settings.error) conflicts.push(found.settings.error);
|
|
273
|
-
const hooks = planHooksChange({ removing, found, context, assets, fsImpl });
|
|
288
|
+
const hooks = planHooksChange({ removing, found, context, assets, ledger, scope, fsImpl });
|
|
274
289
|
changes.push(...hooks.changes); conflicts.push(...hooks.conflicts);
|
|
275
290
|
return {
|
|
276
291
|
changes, conflicts, prerequisites: [], notices: copyTree.notices,
|
|
277
292
|
surfaces: {
|
|
278
293
|
instructions: { status: 'supported', target: found.paths.instruction },
|
|
279
294
|
settings: { status: found.settings.error ? 'invalid' : 'supported', target: found.paths.settings },
|
|
280
|
-
mcp: { status: found.settings.error ? 'blocked' : 'supported', target: found.paths.settings, selected: mcp.map((item
|
|
295
|
+
mcp: { status: found.settings.error ? 'blocked' : 'supported', target: found.paths.settings, selected: mcp.map((item ) => item.id) },
|
|
281
296
|
extensions: { status: 'different', mode: found.paths.extensionMode, action: 'use Gemini CLI extension management' },
|
|
282
297
|
policies: policyStatuses(context.policies),
|
|
283
298
|
},
|
|
284
299
|
};
|
|
285
300
|
}
|
|
286
301
|
|
|
287
|
-
function atomicWrite(file
|
|
302
|
+
function atomicWrite(file , content , { fsImpl = fs } = {}) {
|
|
288
303
|
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
289
304
|
const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
290
305
|
try {
|
|
@@ -294,9 +309,9 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
294
309
|
if (fsImpl.existsSync(temp)) fsImpl.rmSync(temp, { force: true });
|
|
295
310
|
}
|
|
296
311
|
}
|
|
297
|
-
function apply({ changes = [], fsImpl = fs }
|
|
312
|
+
function apply({ changes = [], fsImpl = fs } ) {
|
|
298
313
|
for (const change of changes) if (change.content !== undefined) atomicWrite(change.target, change.content, { fsImpl });
|
|
299
|
-
const hooksChange = changes.find((change
|
|
314
|
+
const hooksChange = changes.find((change ) => change.nativeComponent === 'hooks');
|
|
300
315
|
if (hooksChange?.nativePlan) deployGeminiHooks(hooksChange.nativePlan, { fsImpl });
|
|
301
316
|
applyCopyTreeAssets(changes, { fsImpl });
|
|
302
317
|
// An update that relocates an asset (the agents folder layout -> flat files) carries the old
|
|
@@ -307,7 +322,7 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
307
322
|
/** Strip only the DoFlow-managed span, exactly like the Claude adapter's removeManagedSection —
|
|
308
323
|
* a file with user content before/after the managed section must keep that content. Only delete
|
|
309
324
|
* the file outright once nothing but the managed section (and its separator) remains. */
|
|
310
|
-
function removeManagedSection(file
|
|
325
|
+
function removeManagedSection(file , { fsImpl = fs } = {}) {
|
|
311
326
|
if (!fsImpl.existsSync(file)) return false;
|
|
312
327
|
const existing = fsImpl.readFileSync(file, 'utf8');
|
|
313
328
|
const start = existing.indexOf(MARKER_START);
|
|
@@ -324,38 +339,62 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
324
339
|
else fsImpl.writeFileSync(file, next);
|
|
325
340
|
return true;
|
|
326
341
|
}
|
|
327
|
-
function remove({ changes = [], fsImpl = fs }
|
|
342
|
+
function remove({ changes = [], fsImpl = fs } ) {
|
|
328
343
|
for (const change of changes) if (change.target && change.projection?.renderer === 'gemini-instructions') removeManagedSection(change.target, { fsImpl });
|
|
329
|
-
const hooksChange = changes.find((change
|
|
344
|
+
const hooksChange = changes.find((change ) => change.nativeComponent === 'hooks');
|
|
330
345
|
if (hooksChange?.nativePlan) deployRemoveGeminiHooks(hooksChange.nativePlan, { fsImpl });
|
|
331
346
|
removeCopyTreeAssets(changes, { fsImpl });
|
|
332
347
|
}
|
|
333
348
|
|
|
334
|
-
|
|
349
|
+
/** Whether the hooks pseudo-component is on disk as the source declares it, with the resource that
|
|
350
|
+
* records it when so. Null status when the run carries no hooks source or settings.json is unreadable. */
|
|
351
|
+
function verifyHooksComponent({ found, assets, context, fsImpl } ) {
|
|
352
|
+
if (!context.geminiHooksSourceFile || found.settings.error) return { hooksStatus: null, resources: [], conflicts: [] };
|
|
353
|
+
const hooksPlan = planGeminiHooks({ sourceFile: context.geminiHooksSourceFile, sourceHooksDir: context.geminiHooksSourceDir,
|
|
354
|
+
settingsFile: found.paths.settings, destinationHooksDir: found.paths.hooksDirectory, trusted: context.hooksTrusted, fsImpl });
|
|
355
|
+
const hooksStatus = hooksPlan.ok && hooksPlan.status === 'unchanged' ? 'managed' : (hooksPlan.ok ? 'missing' : 'conflict');
|
|
356
|
+
const resources = hooksStatus === 'managed' ? [{ assetId: hooksAssetId(assets), target: hooksPlan.settingsFile, ownershipIdentity: 'gemini:hooks',
|
|
357
|
+
fingerprint: fingerprint(hooksPlan.merged.hooks), sourceVersion: context.sourceVersion ?? 'unknown', projection: { renderer: 'gemini-hooks' } },
|
|
358
|
+
...hooksPlan.scriptNames.map((identity ) => {
|
|
359
|
+
const target = path.join(hooksPlan.destinationHooksDir, identity);
|
|
360
|
+
return { assetId: hooksAssetId(assets), target, ownershipIdentity: `gemini:hooks-script:${identity}`, kind: 'hooks-script', identity,
|
|
361
|
+
fingerprint: scriptFingerprint(fs.readFileSync(target)), sourceVersion: context.sourceVersion ?? 'unknown', projection: { renderer: 'gemini-hooks' } };
|
|
362
|
+
})] : [];
|
|
363
|
+
return { hooksStatus, resources, conflicts: hooksPlan.ok ? [] : (hooksPlan.errors || []) };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The hook front doors (IC-037): the hooks pseudo-component alone. */
|
|
367
|
+
function planHookDoors({ scope, scopeRoot, assets = [], discovery, context = {}, ledger, fsImpl = fs } ) {
|
|
368
|
+
const found = discovery || discover({ scope, scopeRoot, context, fsImpl });
|
|
369
|
+
const hooks = planHooksChange({ removing: false, found, context, assets, ledger, scope, fsImpl });
|
|
370
|
+
return { changes: hooks.changes, conflicts: [...(found.settings.error ? [found.settings.error] : []), ...hooks.conflicts], prerequisites: [], notices: [] };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function verifyHookDoors({ scope, scopeRoot, assets = [], context = {}, fsImpl = fs } ) {
|
|
374
|
+
const found = discover({ scope, scopeRoot, context, fsImpl });
|
|
375
|
+
const hooks = verifyHooksComponent({ found, assets, context, fsImpl });
|
|
376
|
+
const conflicts = [...(found.settings.error ? [found.settings.error] : []), ...hooks.conflicts];
|
|
377
|
+
return { ok: conflicts.length === 0 && hooks.hooksStatus !== 'conflict', resources: hooks.resources, statuses: { hooks: hooks.hooksStatus }, conflicts };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function verify({ scope, scopeRoot, assets = [], context = {}, fsImpl = fs } ) {
|
|
335
381
|
const found = discover({ scope, scopeRoot, context, fsImpl });
|
|
336
382
|
const resources = [];
|
|
337
|
-
if (assets.some((asset
|
|
383
|
+
if (assets.some((asset ) => asset.id === 'guidance.core') && found.instruction?.includes(MARKER_START) && found.instruction.includes(MARKER_END)) {
|
|
338
384
|
resources.push({ assetId: 'guidance.core', target: found.paths.instruction, ownershipIdentity: 'gemini:instructions', fingerprint: fingerprint(found.instruction), sourceVersion: context.sourceVersion ?? 'unknown', projection: { renderer: 'gemini-instructions' } });
|
|
339
385
|
}
|
|
340
386
|
const copyTree = verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl });
|
|
341
387
|
resources.push(...copyTree.resources);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
hooksStatus = hooksPlan.ok && hooksPlan.status === 'unchanged' ? 'managed' : (hooksPlan.ok ? 'missing' : 'conflict');
|
|
347
|
-
if (!hooksPlan.ok) conflicts.push(...(hooksPlan.errors || []));
|
|
348
|
-
if (hooksStatus === 'managed') {
|
|
349
|
-
resources.push({ assetId: hooksAssetId(assets), target: hooksPlan.settingsFile, ownershipIdentity: 'gemini:hooks',
|
|
350
|
-
fingerprint: fingerprint(hooksPlan.merged.hooks), sourceVersion: context.sourceVersion ?? 'unknown', projection: { renderer: 'gemini-hooks' } });
|
|
351
|
-
}
|
|
352
|
-
}
|
|
388
|
+
const hooks = verifyHooksComponent({ found, assets, context, fsImpl });
|
|
389
|
+
resources.push(...hooks.resources);
|
|
390
|
+
const hooksStatus = hooks.hooksStatus;
|
|
391
|
+
const conflicts = [...(found.settings.error ? [found.settings.error] : []), ...copyTree.conflicts, ...hooks.conflicts];
|
|
353
392
|
return { ok: !found.settings.error && copyTree.conflicts.length === 0 && hooksStatus !== 'conflict', resources,
|
|
354
393
|
statuses: { settings: found.settings.error ? 'invalid' : 'supported', extensions: 'different', policies: policyStatuses(context.policies), copyTree: copyTree.statuses, hooks: hooksStatus },
|
|
355
394
|
conflicts };
|
|
356
395
|
}
|
|
357
396
|
|
|
358
|
-
return { nativePaths, discover, render, plan, apply, remove, verify };
|
|
397
|
+
return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
|
|
359
398
|
}
|
|
360
399
|
|
|
361
400
|
const singleton = createGeminiAdapter();
|
|
@@ -370,7 +409,7 @@ const singleton = createGeminiAdapter();
|
|
|
370
409
|
* @param {Object} target a lifecycle status target ({adapterInput, discovery})
|
|
371
410
|
* @returns {*} the plan's trust, or null
|
|
372
411
|
*/
|
|
373
|
-
function hookTrustFor(target
|
|
412
|
+
function hookTrustFor(target ) {
|
|
374
413
|
const context = target.adapterInput?.context || {};
|
|
375
414
|
const settingsFile = target.discovery?.paths?.settings;
|
|
376
415
|
if (!context.geminiHooksSourceFile || !settingsFile || target.discovery?.settings?.error) return null;
|
|
@@ -385,6 +424,6 @@ module.exports = {
|
|
|
385
424
|
MARKER_START, MARKER_END,
|
|
386
425
|
nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
|
|
387
426
|
managedInstruction, plan: singleton.plan, apply: singleton.apply, remove: singleton.remove,
|
|
388
|
-
verify: singleton.verify, planGeminiHooks, hookTrustFor,
|
|
427
|
+
verify: singleton.verify, planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors, planGeminiHooks, hookTrustFor,
|
|
389
428
|
createGeminiAdapter,
|
|
390
429
|
};
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
|
|
14
|
-
function commandScriptNames(command
|
|
14
|
+
function commandScriptNames(command ) {
|
|
15
15
|
// DoFlow's wrappers are shell scripts selected from a project or CODEX_HOME hooks directory.
|
|
16
16
|
// Extracting the basename works for both literal and command-substitution based paths.
|
|
17
17
|
return [...new Set((command.match(/[A-Za-z0-9][A-Za-z0-9_-]*\.sh\b/g) || []))];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function hookHandlers(config
|
|
21
|
-
return Object.entries(config.hooks || {}).flatMap(([event, groups]
|
|
22
|
-
(Array.isArray(groups) ? groups : []).flatMap((group
|
|
23
|
-
(Array.isArray(group?.hooks) ? group.hooks : []).map((handler
|
|
20
|
+
function hookHandlers(config ) {
|
|
21
|
+
return Object.entries(config.hooks || {}).flatMap(([event, groups] ) =>
|
|
22
|
+
(Array.isArray(groups) ? groups : []).flatMap((group ) =>
|
|
23
|
+
(Array.isArray(group?.hooks) ? group.hooks : []).map((handler ) => ({ event, handler }))));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// On `trusted`: both harnesses gate hook execution behind their own trust prompt — Codex through
|
|
@@ -36,8 +36,8 @@ function hookHandlers(config: any): { event: string, handler: any }[] {
|
|
|
36
36
|
// exec-bit half of the check is POSIX-only; existence is checked everywhere.
|
|
37
37
|
const EXEC_BITS_EXIST = process.platform !== 'win32';
|
|
38
38
|
|
|
39
|
-
function verifyHookCommands(config
|
|
40
|
-
const checks
|
|
39
|
+
function verifyHookCommands(config , { scriptsDir, trusted = false, fsImpl = fs } = {}) {
|
|
40
|
+
const checks = [];
|
|
41
41
|
for (const { event, handler } of hookHandlers(config)) {
|
|
42
42
|
if (!handler || handler.type !== 'command' || typeof handler.command !== 'string' || !handler.command.trim()) continue;
|
|
43
43
|
const names = commandScriptNames(handler.command);
|
|
@@ -54,7 +54,7 @@ function verifyHookCommands(config: any, { scriptsDir, trusted = false, fsImpl =
|
|
|
54
54
|
requiresTrust: true, trusted });
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
return { ok: checks.every((check
|
|
57
|
+
return { ok: checks.every((check ) => check.ok), checks, trust: { required: checks.length > 0, trusted: Boolean(trusted), status: trusted ? 'trusted' : 'review-required' } };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
module.exports = { commandScriptNames, hookHandlers, verifyHookCommands };
|