@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.4
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/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/registry/storage.json +87 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +60 -60
- package/src/adapters/claude/{index.ts → index.js} +48 -48
- 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} +23 -23
- package/src/adapters/codex/{index.ts → index.js} +98 -98
- 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} +70 -70
- package/src/adapters/gemini/{hooks.ts → hooks.js} +24 -24
- package/src/adapters/gemini/{index.ts → index.js} +41 -41
- 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} +34 -34
- 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/{index.ts → index.js} +30 -30
- 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} +24 -24
- 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} +19 -19
- package/src/cli/{index.ts → index.js} +26 -26
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +48 -48
- 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/{index.ts → index.js} +140 -140
- 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} +60 -60
- 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/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +30 -30
- package/src/runtime/{cli-result.ts → cli-result.js} +4 -4
- package/src/runtime/{cli.ts → cli.js} +37 -37
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/{context-pack.ts → context-pack.js} +20 -20
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +25 -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} +15 -15
- 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} +35 -35
- 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} +40 -40
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +19 -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/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} +61 -61
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +13 -13
- package/src/runtime/{task-state.ts → task-state.js} +15 -15
- 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
|
@@ -12,12 +12,12 @@ const fs = require('node:fs');
|
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const crypto = require('node:crypto');
|
|
14
14
|
|
|
15
|
-
function sha256(buf
|
|
15
|
+
function sha256(buf ) {
|
|
16
16
|
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function walkRelFiles(dir
|
|
20
|
-
const out
|
|
19
|
+
function walkRelFiles(dir , fsImpl ) {
|
|
20
|
+
const out = [];
|
|
21
21
|
for (const entry of fsImpl.readdirSync(dir, { withFileTypes: true })) {
|
|
22
22
|
const abs = path.join(dir, entry.name);
|
|
23
23
|
if (entry.isDirectory()) out.push(...walkRelFiles(abs, fsImpl).map((rel) => path.join(entry.name, rel)));
|
|
@@ -33,29 +33,29 @@ function walkRelFiles(dir: string, fsImpl: any): string[] {
|
|
|
33
33
|
* branched on inside an adapter, so the shape stays visible next to the asset it applies to.
|
|
34
34
|
* @type {Record<string, (sourceRel: string) => string>}
|
|
35
35
|
*/
|
|
36
|
-
const LAYOUTS
|
|
36
|
+
const LAYOUTS = {
|
|
37
37
|
/** `spec-analyst.md` -> `spec-analyst/agent.md` */
|
|
38
|
-
'dir-per-file:agent.md': (sourceRel
|
|
38
|
+
'dir-per-file:agent.md': (sourceRel ) => {
|
|
39
39
|
const ext = path.extname(sourceRel);
|
|
40
40
|
return path.join(sourceRel.slice(0, sourceRel.length - ext.length), `agent${ext || '.md'}`);
|
|
41
41
|
},
|
|
42
42
|
/** `RULE_01_SAFETY.md` -> `RULE_01_SAFETY.instructions.md` — GitHub's path-specific
|
|
43
43
|
* instructions are discovered by their `.instructions.md` suffix. */
|
|
44
|
-
'instructions-md': (sourceRel
|
|
44
|
+
'instructions-md': (sourceRel ) => {
|
|
45
45
|
const ext = path.extname(sourceRel);
|
|
46
46
|
return `${sourceRel.slice(0, sourceRel.length - ext.length)}.instructions.md`;
|
|
47
47
|
},
|
|
48
48
|
/** `MODE_Orchestration.md` -> `doflow-orchestration.md` — Claude's /config picker lists
|
|
49
49
|
* output styles by file name; the doflow- prefix keeps DoFlow's styles grouped and
|
|
50
50
|
* non-colliding with anything a user authors by hand. */
|
|
51
|
-
'doflow-output-style': (sourceRel
|
|
51
|
+
'doflow-output-style': (sourceRel ) => {
|
|
52
52
|
const ext = path.extname(sourceRel);
|
|
53
53
|
const base = path.basename(sourceRel, ext).replace(/^MODE_/i, '').replace(/_/g, '-').toLowerCase();
|
|
54
54
|
return `doflow-${base}${ext || '.md'}`;
|
|
55
55
|
},
|
|
56
56
|
/** `core-implementer.md` -> `core-implementer.json` — kiro-cli discovers custom agents as
|
|
57
57
|
* `.kiro/agents/<name>.json` (2.27.1 `kiro-cli agent list` did not list `.md` agents). */
|
|
58
|
-
'kiro-agent-json': (sourceRel
|
|
58
|
+
'kiro-agent-json': (sourceRel ) => {
|
|
59
59
|
const ext = path.extname(sourceRel);
|
|
60
60
|
return `${sourceRel.slice(0, sourceRel.length - ext.length)}.json`;
|
|
61
61
|
},
|
|
@@ -63,8 +63,8 @@ const LAYOUTS: Record<string, (sourceRel: string) => string> = {
|
|
|
63
63
|
|
|
64
64
|
/** Resolve a declared layout name to its mapper. Unknown names fail loudly rather than silently
|
|
65
65
|
* mirroring, because a typo would otherwise install to the wrong shape and look like it worked. */
|
|
66
|
-
function resolveLayout(name
|
|
67
|
-
if (!name) return (rel
|
|
66
|
+
function resolveLayout(name ) {
|
|
67
|
+
if (!name) return (rel ) => rel;
|
|
68
68
|
const layout = LAYOUTS[name];
|
|
69
69
|
if (!layout) throw new Error(`Unknown copy-tree layout '${name}' (known: ${Object.keys(LAYOUTS).join(', ')})`);
|
|
70
70
|
return layout;
|
|
@@ -79,39 +79,39 @@ function resolveLayout(name: any): (sourceRel: string) => string {
|
|
|
79
79
|
* applyTree can re-derive the written bytes from the untouched source at any later retry.
|
|
80
80
|
* @type {Record<string, (sourceRel: string, content: Buffer) => Buffer>}
|
|
81
81
|
*/
|
|
82
|
-
const TRANSFORMS
|
|
82
|
+
const TRANSFORMS = {
|
|
83
83
|
/** Shared mode doc -> Claude output style with name/description/keep-coding-instructions */
|
|
84
84
|
'claude-output-styles': renderClaudeOutputStyle,
|
|
85
85
|
/** `<rule>.md` -> `<rule>.instructions.md` body under a Copilot applyTo header */
|
|
86
|
-
'copilot-rule-instructions': (sourceRel
|
|
86
|
+
'copilot-rule-instructions': (sourceRel , content ) => {
|
|
87
87
|
void sourceRel;
|
|
88
88
|
return Buffer.from(`---\napplyTo: '**'\n---\n\n${stripFrontmatter(content.toString('utf8'))}`);
|
|
89
89
|
},
|
|
90
90
|
/** Shared agent spec frontmatter -> only the keys Gemini CLI's subagent schema accepts */
|
|
91
|
-
'gemini-agents': (sourceRel
|
|
91
|
+
'gemini-agents': (sourceRel , content ) => {
|
|
92
92
|
void sourceRel;
|
|
93
93
|
return Buffer.from(filterGeminiAgentFrontmatter(content.toString('utf8')));
|
|
94
94
|
},
|
|
95
95
|
/** Shared agent spec frontmatter -> OpenCode's markdown-agent vocabulary */
|
|
96
|
-
'opencode-agents': (sourceRel
|
|
96
|
+
'opencode-agents': (sourceRel , content ) => {
|
|
97
97
|
void sourceRel;
|
|
98
98
|
return Buffer.from(renderOpencodeAgent(content.toString('utf8')));
|
|
99
99
|
},
|
|
100
100
|
/** Shared agent spec -> kiro-cli custom agent JSON (name, description, prompt, tools, hooks) */
|
|
101
|
-
'kiro-agents': (sourceRel
|
|
101
|
+
'kiro-agents': (sourceRel , content ) => {
|
|
102
102
|
void sourceRel;
|
|
103
103
|
return Buffer.from(renderKiroAgent(content.toString('utf8')));
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
function resolveTransform(name
|
|
107
|
+
function resolveTransform(name ) {
|
|
108
108
|
if (!name) return null;
|
|
109
109
|
const transform = TRANSFORMS[name];
|
|
110
110
|
if (!transform) throw new Error(`Unknown copy-tree transform '${name}' (known: ${Object.keys(TRANSFORMS).join(', ')})`);
|
|
111
111
|
return transform;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
function stripFrontmatter(text
|
|
114
|
+
function stripFrontmatter(text ) {
|
|
115
115
|
if (!text.startsWith('---')) return text;
|
|
116
116
|
const end = text.indexOf('\n---', 3);
|
|
117
117
|
if (end === -1) return text;
|
|
@@ -122,7 +122,7 @@ function stripFrontmatter(text: string): string {
|
|
|
122
122
|
* built-in engineering instructions (these styles shape HOW DoFlow works, not WHETHER Claude
|
|
123
123
|
* codes). The description is lifted from the document's first `**Purpose**` line so the /config
|
|
124
124
|
* picker explains each style without opening it. */
|
|
125
|
-
function renderClaudeOutputStyle(sourceRel
|
|
125
|
+
function renderClaudeOutputStyle(sourceRel , content ) {
|
|
126
126
|
const text = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
|
|
127
127
|
const base = path.basename(sourceRel).replace(/\.[^.]+$/, '').replace(/^MODE_/i, '').replace(/_/g, ' ');
|
|
128
128
|
const purpose = text.match(/\*\*Purpose\*\*[:*]*\s*(.+)?/);
|
|
@@ -137,11 +137,11 @@ const GEMINI_AGENT_KEYS = new Set(['name', 'description', 'kind', 'tools', 'mcpS
|
|
|
137
137
|
/** Keep only Gemini's documented top-level frontmatter keys, with their indented continuation
|
|
138
138
|
* lines, in source order. The body after the closing `---` passes through byte for byte, and a
|
|
139
139
|
* file without frontmatter is returned unchanged. */
|
|
140
|
-
function filterGeminiAgentFrontmatter(text
|
|
140
|
+
function filterGeminiAgentFrontmatter(text ) {
|
|
141
141
|
const match = text.match(/^---\r?\n([\s\S]*?\r?\n)---(?=\r?\n|$)/);
|
|
142
142
|
if (!match) return text;
|
|
143
143
|
let keep = false;
|
|
144
|
-
const kept = match[1].split(/(?<=\n)/).filter((line
|
|
144
|
+
const kept = match[1].split(/(?<=\n)/).filter((line ) => {
|
|
145
145
|
const key = line.match(/^([A-Za-z][A-Za-z0-9_-]*):/);
|
|
146
146
|
if (key) keep = GEMINI_AGENT_KEYS.has(key[1]);
|
|
147
147
|
return keep;
|
|
@@ -152,12 +152,12 @@ function filterGeminiAgentFrontmatter(text: string): string {
|
|
|
152
152
|
/** The command a Kiro hook runs for one front-door script: the project copy when the git root
|
|
153
153
|
* holds one, else the user-scope copy under $HOME. One builder, so the static `doflow` agent
|
|
154
154
|
* (core/harnesses/kiro/agents/doflow.json) and every generated specialist share the exact string. */
|
|
155
|
-
function kiroHookCommand(script
|
|
155
|
+
function kiroHookCommand(script ) {
|
|
156
156
|
return `bash "$(root=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$root/.kiro/hooks/${script}" ] && printf '%s/.kiro/hooks/${script}' "$root" || printf '%s/.kiro/hooks/${script}' "$HOME")"`;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** One kiro-cli agent hook entry. */
|
|
160
|
-
function kiroHook({ name, trigger, matcher, script, timeout }
|
|
160
|
+
function kiroHook({ name, trigger, matcher, script, timeout } ) {
|
|
161
161
|
return {
|
|
162
162
|
name, trigger, ...(matcher ? { matcher } : {}),
|
|
163
163
|
action: { type: 'command', command: kiroHookCommand(script) }, timeout,
|
|
@@ -168,7 +168,7 @@ function kiroHook({ name, trigger, matcher, script, timeout }: { name: string, t
|
|
|
168
168
|
* "*" matcher: Kiro documents "@<server>" (every tool of one server) and "*" but no all-MCP
|
|
169
169
|
* matcher, and mcp-tool-guard.sh exits 0 immediately for any non-MCP tool name, so "*" costs one
|
|
170
170
|
* fast no-op for built-in tools and still covers every MCP server. */
|
|
171
|
-
function kiroPreToolUseHooks()
|
|
171
|
+
function kiroPreToolUseHooks() {
|
|
172
172
|
return [
|
|
173
173
|
kiroHook({ name: 'doflow-pre-implementation-gate', trigger: 'preToolUse', matcher: 'write', script: 'pre-implement-gate.sh', timeout: 10 }),
|
|
174
174
|
kiroHook({ name: 'doflow-pre-bash-guard', trigger: 'preToolUse', matcher: 'shell', script: 'pre-bash-guard.sh', timeout: 10 }),
|
|
@@ -178,8 +178,8 @@ function kiroPreToolUseHooks(): any[] {
|
|
|
178
178
|
|
|
179
179
|
/** Map one core/shared agent-spec file onto a kiro-cli custom agent. Spec keys other than
|
|
180
180
|
* name/description (model: inherit, effort) are dropped: Kiro inherits the session model. */
|
|
181
|
-
function renderKiroAgent(text
|
|
182
|
-
const fm
|
|
181
|
+
function renderKiroAgent(text ) {
|
|
182
|
+
const fm = {};
|
|
183
183
|
let body = text;
|
|
184
184
|
if (text.startsWith('---')) {
|
|
185
185
|
const end = text.indexOf('\n---', 3);
|
|
@@ -206,8 +206,8 @@ const OPENCODE_READONLY_AGENTS = new Set(['spec-analyst', 'system-architect', 'q
|
|
|
206
206
|
* (description / mode / permission; model omitted because OpenCode has no 'inherit'). Unknown
|
|
207
207
|
* spec keys (tools, effort) are dropped rather than passed through, so OpenCode never sees
|
|
208
208
|
* vocabulary it does not define. Body below the frontmatter passes through untouched. */
|
|
209
|
-
function renderOpencodeAgent(text
|
|
210
|
-
const fm
|
|
209
|
+
function renderOpencodeAgent(text ) {
|
|
210
|
+
const fm = {};
|
|
211
211
|
let body = text;
|
|
212
212
|
if (text.startsWith('---')) {
|
|
213
213
|
const end = text.indexOf('\n---', 3);
|
|
@@ -233,7 +233,7 @@ function renderOpencodeAgent(text: string): string {
|
|
|
233
233
|
* the destination tree beyond checking existence. `relPath` is destination-relative and doubles as
|
|
234
234
|
* the ledger identity, so a layout change relocates the recorded resource too — planTree's
|
|
235
235
|
* relocation handling then removes the old path rather than orphaning it. */
|
|
236
|
-
function discoverTree({ sourceDir, destDir, fsImpl = fs, layout, transform }
|
|
236
|
+
function discoverTree({ sourceDir, destDir, fsImpl = fs, layout, transform } ) {
|
|
237
237
|
if (!fsImpl.existsSync(sourceDir)) throw new Error(`copy-tree source is missing: ${sourceDir}`);
|
|
238
238
|
const mapRel = typeof layout === 'function' ? layout : resolveLayout(layout);
|
|
239
239
|
const mapContent = typeof transform === 'function' ? transform : resolveTransform(transform);
|
|
@@ -248,7 +248,7 @@ function discoverTree({ sourceDir, destDir, fsImpl = fs, layout, transform }: an
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
/** A file's identity on disk (device + inode), or null when the path does not resolve to one. */
|
|
251
|
-
function fileId(file
|
|
251
|
+
function fileId(file , fsImpl ) {
|
|
252
252
|
try {
|
|
253
253
|
const stat = fsImpl.statSync(file, { bigint: true });
|
|
254
254
|
// An fs stand-in that reports no inode identifies nothing, rather than every file as one.
|
|
@@ -265,9 +265,9 @@ function fileId(file: string, fsImpl: any): string | null {
|
|
|
265
265
|
* a hard link answers the same way. Built on first use, one stat per path; a path that does not
|
|
266
266
|
* exist matches nothing, and `target` never matches itself.
|
|
267
267
|
*/
|
|
268
|
-
function sameFileIndex(paths
|
|
269
|
-
let byId
|
|
270
|
-
return (target
|
|
268
|
+
function sameFileIndex(paths , fsImpl = fs) {
|
|
269
|
+
let byId ;
|
|
270
|
+
return (target ) => {
|
|
271
271
|
if (byId === undefined) {
|
|
272
272
|
byId = new Map();
|
|
273
273
|
for (const candidate of paths) {
|
|
@@ -292,31 +292,31 @@ function sameFileIndex(paths: string[], fsImpl: any = fs): (target: string) => s
|
|
|
292
292
|
* `siblingReplaced` lists the harnesses whose recorded bytes this plan replaces: the file matches
|
|
293
293
|
* neither the source nor this harness's own record, only a sibling's (forced or not).
|
|
294
294
|
*/
|
|
295
|
-
function planTree({ sourceDir, destDir, previousResources = [], operation = 'apply', fsImpl = fs, layout, transform, force = false, keepModified = false, siblingFingerprints = new Map() }
|
|
296
|
-
const prevByPath = new Map
|
|
297
|
-
const changes
|
|
298
|
-
const conflicts
|
|
299
|
-
const kept
|
|
300
|
-
const siblingReplaced = new Set
|
|
295
|
+
function planTree({ sourceDir, destDir, previousResources = [], operation = 'apply', fsImpl = fs, layout, transform, force = false, keepModified = false, siblingFingerprints = new Map() } ) {
|
|
296
|
+
const prevByPath = new Map (previousResources.map((resource ) => [resource.relPath, resource]));
|
|
297
|
+
const changes = [];
|
|
298
|
+
const conflicts = [];
|
|
299
|
+
const kept = [];
|
|
300
|
+
const siblingReplaced = new Set ();
|
|
301
301
|
|
|
302
302
|
// Fingerprints the CURRENT source would write, keyed by destination. Resolved lazily and only
|
|
303
303
|
// when a recorded fingerprint has already failed to match, so the common removal path still
|
|
304
304
|
// never reads the source tree. A missing source directory is not an error here — an asset can be
|
|
305
305
|
// removed after its source moved — it simply leaves the recorded fingerprint as the only signal.
|
|
306
|
-
let sourceByDest
|
|
307
|
-
const sourceFingerprint = (destAbs
|
|
306
|
+
let sourceByDest ;
|
|
307
|
+
const sourceFingerprint = (destAbs ) => {
|
|
308
308
|
if (sourceByDest === undefined) {
|
|
309
309
|
sourceByDest = sourceDir && fsImpl.existsSync(sourceDir)
|
|
310
|
-
? new Map
|
|
310
|
+
? new Map (discoverTree({ sourceDir, destDir, fsImpl, layout, transform }).files.map((file) => [file.destAbs, file.fingerprint]))
|
|
311
311
|
: new Map();
|
|
312
312
|
}
|
|
313
313
|
return sourceByDest.get(destAbs);
|
|
314
314
|
};
|
|
315
315
|
|
|
316
316
|
// Every destination the current source produces, set once the apply path has discovered them.
|
|
317
|
-
let producedSameFile
|
|
317
|
+
let producedSameFile ;
|
|
318
318
|
|
|
319
|
-
const proposeRemoval = (prev
|
|
319
|
+
const proposeRemoval = (prev , { relocated = false } = {}) => {
|
|
320
320
|
// prev's OWN recorded location, not the current destDir — an asset whose nativeDir changed
|
|
321
321
|
// since prev was recorded must be removed from where it actually is, not from where it would
|
|
322
322
|
// land today. Absent (pre-this-fix data, or a caller-constructed previousResources entry with
|
|
@@ -414,9 +414,9 @@ function planTree({ sourceDir, destDir, previousResources = [], operation = 'app
|
|
|
414
414
|
|
|
415
415
|
/** Write every create/update change. Preserves the source file's mode (so a hook script's +x
|
|
416
416
|
* bit survives the copy without a separate chmod pass) and mtime, matching this codebase's
|
|
417
|
-
* general convention for a lifecycle-owned write (see src/adapters/claude/index.
|
|
417
|
+
* general convention for a lifecycle-owned write (see src/adapters/claude/index.js's
|
|
418
418
|
* applySettingsAsset for the same pattern applied to a transformed-content file). */
|
|
419
|
-
function applyTree({ changes = [], fsImpl = fs, transform }
|
|
419
|
+
function applyTree({ changes = [], fsImpl = fs, transform } ) {
|
|
420
420
|
const mapContent = typeof transform === 'function' ? transform : resolveTransform(transform);
|
|
421
421
|
let applied = 0;
|
|
422
422
|
for (const change of changes) {
|
|
@@ -440,15 +440,15 @@ function applyTree({ changes = [], fsImpl = fs, transform }: any): { applied: nu
|
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
/** Why a removal the runtime prune planned may no longer run, or null when it still may: the
|
|
443
|
-
* prune's plan-time proof (src/lifecycle/index.
|
|
443
|
+
* prune's plan-time proof (src/lifecycle/index.js pruneRetiredRuntimeFiles) re-run right before the
|
|
444
444
|
* unlink, so a directory swapped for a symlink, or a file linked to a shipped one, after the plan was
|
|
445
445
|
* made is not deleted through. `guard` is `{ root, source }`: the tree and the source it projects.
|
|
446
446
|
* One walk of the source and one stat per shipped path per tree per call, cached in `cache`. */
|
|
447
|
-
function pruneGuardRefusal(change
|
|
447
|
+
function pruneGuardRefusal(change , cache , fsImpl ) {
|
|
448
448
|
const { root, source } = change.pruneGuard;
|
|
449
449
|
const key = `${root}\0${source}`;
|
|
450
450
|
if (!cache.has(key)) {
|
|
451
|
-
const produced = fsImpl.existsSync(source) ? discoverTree({ sourceDir: source, destDir: root, fsImpl }).files.map((file
|
|
451
|
+
const produced = fsImpl.existsSync(source) ? discoverTree({ sourceDir: source, destDir: root, fsImpl }).files.map((file ) => file.destAbs) : [];
|
|
452
452
|
cache.set(key, { realRoot: fsImpl.realpathSync(root), sameFile: sameFileIndex(produced, fsImpl) });
|
|
453
453
|
}
|
|
454
454
|
const { realRoot, sameFile } = cache.get(key);
|
|
@@ -460,9 +460,9 @@ function pruneGuardRefusal(change: any, cache: Map<string, any>, fsImpl: any): s
|
|
|
460
460
|
/** Delete every remove change, re-checking each file's fingerprint immediately before deletion —
|
|
461
461
|
* refuses (throws) rather than silently deleting a file modified since planTree ran. A removal the
|
|
462
462
|
* runtime prune planned (`pruneGuard`) also re-checks its containment and same-file proof there. */
|
|
463
|
-
function removeTree({ changes = [], fsImpl = fs }
|
|
463
|
+
function removeTree({ changes = [], fsImpl = fs } ) {
|
|
464
464
|
let removed = 0;
|
|
465
|
-
const guards = new Map
|
|
465
|
+
const guards = new Map ();
|
|
466
466
|
for (const change of changes) {
|
|
467
467
|
if (change.operation !== 'remove' || change.kept) continue;
|
|
468
468
|
if (!fsImpl.existsSync(change.target)) continue;
|
|
@@ -486,7 +486,7 @@ const ANCHOR_DIRS = new Set(['.agents', '.claude', '.codex', '.copilot', '.doflo
|
|
|
486
486
|
* which may hold user content elsewhere. The match is by basename, so a root with another name
|
|
487
487
|
* (`~/.gemini/config`, `~/.pi/agent`, `~/.config/opencode`) is pruned once it is empty; no file is
|
|
488
488
|
* ever lost, since rmdir refuses a folder that holds one. */
|
|
489
|
-
function pruneEmptyAncestors(startDir
|
|
489
|
+
function pruneEmptyAncestors(startDir , { fsImpl = fs } = {}) {
|
|
490
490
|
let current = startDir;
|
|
491
491
|
for (;;) {
|
|
492
492
|
const basename = path.basename(current);
|
|
@@ -500,11 +500,11 @@ function pruneEmptyAncestors(startDir: string, { fsImpl = fs }: { fsImpl?: any }
|
|
|
500
500
|
* `plannedWrites` lists the destinations the run being verified wrote. An absent source file is
|
|
501
501
|
* not reported in general, because a user may delete one, but an absent file this run wrote means
|
|
502
502
|
* the run lost it (a case-only rename once deleted the file it had just written) and is a conflict. */
|
|
503
|
-
function verifyTree({ sourceDir, destDir, fsImpl = fs, layout, transform, plannedWrites = [] }
|
|
503
|
+
function verifyTree({ sourceDir, destDir, fsImpl = fs, layout, transform, plannedWrites = [] } ) {
|
|
504
504
|
const { files } = discoverTree({ sourceDir, destDir, fsImpl, layout, transform });
|
|
505
505
|
const written = new Set(plannedWrites);
|
|
506
|
-
const resources
|
|
507
|
-
const conflicts
|
|
506
|
+
const resources = [];
|
|
507
|
+
const conflicts = [];
|
|
508
508
|
for (const file of files) {
|
|
509
509
|
if (!file.exists) {
|
|
510
510
|
if (written.has(file.destAbs)) conflicts.push(`${file.relPath} was written by this run but is missing`);
|
|
@@ -520,14 +520,14 @@ function verifyTree({ sourceDir, destDir, fsImpl = fs, layout, transform, planne
|
|
|
520
520
|
// ---- per-adapter copy-tree glue, shared so codex/claude/gemini don't each carry their own copy ----
|
|
521
521
|
|
|
522
522
|
/** Filter a harness's asset list down to the ones this engine handles. */
|
|
523
|
-
function copyTreeAssets(assets
|
|
524
|
-
return (assets || []).filter((asset
|
|
523
|
+
function copyTreeAssets(assets ) {
|
|
524
|
+
return (assets || []).filter((asset ) => asset?.renderer === 'copy-tree');
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
/** Recursively sort object keys before serializing, so two logically-equal objects with
|
|
528
528
|
* differently-ordered keys (e.g. after a settings file is merged and re-merged) fingerprint
|
|
529
529
|
* identically instead of spuriously registering as changed. */
|
|
530
|
-
function stableSort(value
|
|
530
|
+
function stableSort(value ) {
|
|
531
531
|
if (Array.isArray(value)) return value.map(stableSort);
|
|
532
532
|
if (!value || typeof value !== 'object') return value;
|
|
533
533
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableSort(value[key])]));
|
|
@@ -535,22 +535,22 @@ function stableSort(value: any): any {
|
|
|
535
535
|
|
|
536
536
|
/** Content fingerprint every adapter uses to track what it owns: sha256 of a string as-is, or of
|
|
537
537
|
* a stably key-sorted JSON serialization of anything else. */
|
|
538
|
-
function fingerprint(value
|
|
538
|
+
function fingerprint(value ) {
|
|
539
539
|
return sha256(typeof value === 'string' ? value : JSON.stringify(stableSort(value)));
|
|
540
540
|
}
|
|
541
541
|
|
|
542
542
|
/** Read and parse a native JSON file an adapter merges into. Distinguishes "absent" (safe to
|
|
543
543
|
* create) from "present but unparseable" (a conflict to report, never silently overwritten) —
|
|
544
544
|
* every adapter that merges into a native JSON settings/config file needs exactly this. */
|
|
545
|
-
function readJson(file
|
|
545
|
+
function readJson(file , { fsImpl = fs } = {}) {
|
|
546
546
|
if (!fsImpl.existsSync(file)) return { exists: false, value: {}, error: null };
|
|
547
547
|
try { return { exists: true, value: JSON.parse(fsImpl.readFileSync(file, 'utf8')), error: null }; }
|
|
548
|
-
catch (error
|
|
548
|
+
catch (error ) { return { exists: true, value: null, error: `Invalid JSON in ${file}: ${error.message}` }; }
|
|
549
549
|
}
|
|
550
550
|
|
|
551
551
|
/** Resolve an asset's source directory against the repo root, refusing a path that escapes the
|
|
552
552
|
* repository or does not exist. `harnessName` only shapes the thrown error message. */
|
|
553
|
-
function sourceDirFor(asset
|
|
553
|
+
function sourceDirFor(asset , context = {}, fsImpl = fs, harnessName = 'Adapter') {
|
|
554
554
|
if (!asset || typeof asset.source !== 'string') throw new Error(`${harnessName} asset requires a source path`);
|
|
555
555
|
const repoRoot = context.repoRoot ? path.resolve(context.repoRoot) : process.cwd();
|
|
556
556
|
const source = path.resolve(repoRoot, asset.source);
|
|
@@ -561,14 +561,14 @@ function sourceDirFor(asset: any, context: any = {}, fsImpl: any = fs, harnessNa
|
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
/** Resolve an asset's native destination directory under the harness's already-resolved config dir. */
|
|
564
|
-
function copyTreeDestDir(configDir
|
|
564
|
+
function copyTreeDestDir(configDir , asset ) {
|
|
565
565
|
return path.join(configDir, asset.nativeDir || '');
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
/** Destination of a `../.doflow` shared-tree asset at the scope root (`<project>/.doflow`, or
|
|
569
569
|
* `$HOME/.doflow` globally), for harnesses whose tree root sits more than one level under the scope
|
|
570
570
|
* root. Returns null for any other nativeDir, so the caller falls back to `copyTreeDestDir`. */
|
|
571
|
-
function sharedTreeDestDir(rootDir
|
|
571
|
+
function sharedTreeDestDir(rootDir , nativeDir ) {
|
|
572
572
|
if (nativeDir !== '../.doflow' && !String(nativeDir || '').startsWith('../.doflow/')) return null;
|
|
573
573
|
const sharedRoot = path.join(rootDir, '.doflow');
|
|
574
574
|
const dest = path.join(rootDir, '.doflow', nativeDir.slice('../.doflow'.length));
|
|
@@ -590,13 +590,13 @@ const SHARED_TREE_ASSETS = new Set([
|
|
|
590
590
|
/** For each shared-tree target, the fingerprints other harnesses recorded and which harness
|
|
591
591
|
* recorded each, so an update accepts a runtime or guidance tree a sibling wrote (several harnesses
|
|
592
592
|
* claim one `.doflow` file) while bytes no such row recorded are still refused as a hand edit. */
|
|
593
|
-
function ledgerSiblingFingerprints(resources
|
|
594
|
-
const byTarget = new Map
|
|
593
|
+
function ledgerSiblingFingerprints(resources , harness ) {
|
|
594
|
+
const byTarget = new Map ();
|
|
595
595
|
for (const resource of resources || []) {
|
|
596
596
|
if (resource.kind !== 'copy-tree-file' || resource.harness === harness || !SHARED_TREE_ASSETS.has(resource.assetId)) continue;
|
|
597
597
|
if (typeof resource.target !== 'string' || typeof resource.fingerprint !== 'string') continue;
|
|
598
598
|
if (!byTarget.has(resource.target)) byTarget.set(resource.target, new Map());
|
|
599
|
-
const byFingerprint = byTarget.get(resource.target)
|
|
599
|
+
const byFingerprint = byTarget.get(resource.target) ;
|
|
600
600
|
if (!byFingerprint.has(resource.fingerprint)) byFingerprint.set(resource.fingerprint, resource.harness);
|
|
601
601
|
}
|
|
602
602
|
return byTarget;
|
|
@@ -604,17 +604,17 @@ function ledgerSiblingFingerprints(resources: any, harness: string): Map<string,
|
|
|
604
604
|
|
|
605
605
|
/** The one-line plan notice for a plan whose copy-tree results replaced sibling-written files, or
|
|
606
606
|
* an empty list when none did. */
|
|
607
|
-
function siblingReplacedNotices(results
|
|
607
|
+
function siblingReplacedNotices(results ) {
|
|
608
608
|
const harnesses = [...new Set(results.flatMap((result) => result.siblingReplaced))].sort();
|
|
609
609
|
if (!harnesses.length) return [];
|
|
610
610
|
return [`replaced shared files written by ${harnesses.join(', ')}; reinstall ${harnesses.length > 1 ? 'those harnesses' : 'that harness'} to restore them`];
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
/** Narrow a harness's flat neutral-resource list to one asset's previously-owned copy-tree files. */
|
|
614
|
-
function ledgerFileResources(resources
|
|
614
|
+
function ledgerFileResources(resources , harness , assetId ) {
|
|
615
615
|
return (resources || [])
|
|
616
|
-
.filter((resource
|
|
617
|
-
.map((resource
|
|
616
|
+
.filter((resource ) => resource.harness === harness && resource.assetId === assetId && resource.kind === 'copy-tree-file')
|
|
617
|
+
.map((resource ) => ({ relPath: resource.identity, fingerprint: resource.fingerprint, target: resource.target }));
|
|
618
618
|
}
|
|
619
619
|
|
|
620
620
|
module.exports = { kiroHook, kiroHookCommand, kiroPreToolUseHooks, walkRelFiles, discoverTree, sameFileIndex, planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, resolveLayout, LAYOUTS, resolveTransform, TRANSFORMS, fingerprint, pruneEmptyAncestors, readJson, sourceDirFor };
|
|
@@ -14,8 +14,8 @@
|
|
|
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 { verifyHookCommands } = require('../hook-commands.
|
|
18
|
-
const { mergeHooks } = require('../../helper/settings-merge.
|
|
17
|
+
const { verifyHookCommands } = require('../hook-commands.js');
|
|
18
|
+
const { mergeHooks } = require('../../helper/settings-merge.js');
|
|
19
19
|
|
|
20
20
|
const SUPPORTED_EVENTS = new Set([
|
|
21
21
|
'SessionStart', 'SessionEnd', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'AfterModel',
|
|
@@ -27,7 +27,7 @@ const SUPPORTED_EVENTS = new Set([
|
|
|
27
27
|
// Stop, SubagentStart/Stop, and ConfigChange have no correct Gemini equivalent and are
|
|
28
28
|
// intentionally omitted rather than approximated onto BeforeAgent/AfterAgent (design.md §7/§8 —
|
|
29
29
|
// no false parity).
|
|
30
|
-
const CLAUDE_TO_GEMINI_EVENT
|
|
30
|
+
const CLAUDE_TO_GEMINI_EVENT = {
|
|
31
31
|
SessionStart: 'SessionStart',
|
|
32
32
|
SessionEnd: 'SessionEnd',
|
|
33
33
|
PreToolUse: 'BeforeTool',
|
|
@@ -35,10 +35,10 @@ const CLAUDE_TO_GEMINI_EVENT: Record<string, string> = {
|
|
|
35
35
|
PreCompact: 'PreCompress',
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
function validationError(errors
|
|
38
|
+
function validationError(errors , message ) { errors.push(message); }
|
|
39
39
|
|
|
40
|
-
function validateHooksConfig(config
|
|
41
|
-
const errors
|
|
40
|
+
function validateHooksConfig(config ) {
|
|
41
|
+
const errors = [];
|
|
42
42
|
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
43
43
|
return { ok: false, errors: ['Hook configuration must be an object'], events: [] };
|
|
44
44
|
}
|
|
@@ -54,7 +54,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
54
54
|
validationError(errors, `Hook event '${event}' requires a non-empty array of matcher groups`);
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
|
-
groups.forEach((group
|
|
57
|
+
groups.forEach((group , groupIndex ) => {
|
|
58
58
|
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
59
59
|
validationError(errors, `Hook event '${event}' group ${groupIndex} must be an object`);
|
|
60
60
|
return;
|
|
@@ -66,7 +66,7 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
66
66
|
validationError(errors, `Hook event '${event}' group ${groupIndex} requires a non-empty hooks array`);
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
group.hooks.forEach((handler
|
|
69
|
+
group.hooks.forEach((handler , handlerIndex ) => {
|
|
70
70
|
const label = `Hook event '${event}' group ${groupIndex} handler ${handlerIndex}`;
|
|
71
71
|
if (!handler || typeof handler !== 'object' || Array.isArray(handler)) {
|
|
72
72
|
validationError(errors, `${label} must be an object`); return;
|
|
@@ -85,9 +85,9 @@ function validateHooksConfig(config: any): { ok: boolean, errors: string[], even
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/** Maps Claude guardrail intent, never fabricating a Gemini equivalent for unavailable events. */
|
|
88
|
-
function classifyClaudeGuardrails(claudeHooks
|
|
89
|
-
const supported
|
|
90
|
-
const unsupported
|
|
88
|
+
function classifyClaudeGuardrails(claudeHooks = {}) {
|
|
89
|
+
const supported = [];
|
|
90
|
+
const unsupported = [];
|
|
91
91
|
for (const [event, groups] of Object.entries(claudeHooks || {})) {
|
|
92
92
|
const targetEvent = CLAUDE_TO_GEMINI_EVENT[event] || null;
|
|
93
93
|
const item = { sourceEvent: event, targetEvent, groups: Array.isArray(groups) ? groups.length : 0 };
|
|
@@ -103,22 +103,22 @@ function classifyClaudeGuardrails(claudeHooks: any = {}): { supported: any[], un
|
|
|
103
103
|
* exists but fails to parse is a hard error, not silently treated as empty (this path merges
|
|
104
104
|
* into content DoFlow does not own — a malformed-file work-around here risks losing it).
|
|
105
105
|
*/
|
|
106
|
-
function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, destinationHooksDir: declaredHooksDir, trusted = false, fsImpl = fs }
|
|
106
|
+
function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, destinationHooksDir: declaredHooksDir, trusted = false, fsImpl = fs } = {}) {
|
|
107
107
|
let desired = config;
|
|
108
108
|
try {
|
|
109
109
|
if (!desired && sourceFile) desired = JSON.parse(fsImpl.readFileSync(sourceFile, 'utf8'));
|
|
110
|
-
} catch (error
|
|
110
|
+
} catch (error ) {
|
|
111
111
|
return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${sourceFile}: ${error.message}`] };
|
|
112
112
|
}
|
|
113
113
|
if (!settingsFile) return { ok: false, status: 'invalid-request', changes: [], errors: ['settingsFile is required'] };
|
|
114
114
|
const schema = validateHooksConfig(desired);
|
|
115
115
|
const commands = verifyHookCommands(desired || {}, { scriptsDir: sourceHooksDir, trusted, fsImpl });
|
|
116
|
-
const errors = [...schema.errors, ...commands.checks.filter((check
|
|
116
|
+
const errors = [...schema.errors, ...commands.checks.filter((check ) => !check.ok).map((check ) => `${check.event}: ${check.reason}${check.script ? ` (${check.script})` : ''}`)];
|
|
117
117
|
|
|
118
|
-
let existing
|
|
118
|
+
let existing = {};
|
|
119
119
|
if (fsImpl.existsSync(settingsFile)) {
|
|
120
120
|
try { existing = JSON.parse(fsImpl.readFileSync(settingsFile, 'utf8')); }
|
|
121
|
-
catch (error
|
|
121
|
+
catch (error ) { return { ok: false, status: 'malformed', changes: [], errors: [`Refusing to touch malformed ${settingsFile}: ${error.message}`] }; }
|
|
122
122
|
}
|
|
123
123
|
if (errors.length) return { ok: false, status: 'invalid-request', settingsFile, existing, changes: [], errors, commands };
|
|
124
124
|
|
|
@@ -131,18 +131,18 @@ function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, des
|
|
|
131
131
|
const destinationHooksDir = declaredHooksDir || path.join(path.dirname(settingsFile), 'hooks');
|
|
132
132
|
// The scripts deployGeminiHooks copies beside settings.json, so a backup of this change holds them too.
|
|
133
133
|
const shipped = sourceHooksDir && fsImpl.existsSync(sourceHooksDir)
|
|
134
|
-
? fsImpl.readdirSync(sourceHooksDir).filter((name
|
|
134
|
+
? fsImpl.readdirSync(sourceHooksDir).filter((name ) => !name.endsWith('.json') && fsImpl.statSync(path.join(sourceHooksDir, name)).isFile())
|
|
135
135
|
: [];
|
|
136
136
|
const companionTargets = [...new Set([
|
|
137
|
-
...commands.checks.map((check
|
|
138
|
-
...shipped.map((name
|
|
137
|
+
...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
|
|
138
|
+
...shipped.map((name ) => path.join(destinationHooksDir, name)),
|
|
139
139
|
])].sort();
|
|
140
140
|
return { ok: true, status: changed ? 'change' : 'unchanged', settingsFile, existing, merged,
|
|
141
141
|
changes: changed ? [{ type: fsImpl.existsSync(settingsFile) ? 'update' : 'create', file: settingsFile, key: 'hooks' }] : [],
|
|
142
142
|
errors: [], commands, trust: commands.trust, scriptsDir: sourceHooksDir, destinationHooksDir, companionTargets };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function deployGeminiHooks(plan
|
|
145
|
+
function deployGeminiHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
|
|
146
146
|
if (!plan.ok) return { ...plan, applied: false };
|
|
147
147
|
if (dryRun || plan.status === 'unchanged') return { ...plan, applied: false };
|
|
148
148
|
fsImpl.mkdirSync(path.dirname(plan.settingsFile), { recursive: true });
|
|
@@ -154,7 +154,7 @@ function deployGeminiHooks(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?
|
|
|
154
154
|
} finally {
|
|
155
155
|
if (fsImpl.existsSync(temporary)) fsImpl.unlinkSync(temporary);
|
|
156
156
|
}
|
|
157
|
-
const deployed = new Set
|
|
157
|
+
const deployed = new Set ();
|
|
158
158
|
for (const check of plan.commands.checks) {
|
|
159
159
|
const target = path.join(plan.destinationHooksDir, check.script);
|
|
160
160
|
fsImpl.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -182,18 +182,18 @@ function deployGeminiHooks(plan: any, { dryRun = false, fsImpl = fs }: { dryRun?
|
|
|
182
182
|
* cross-check): if the key isn't present, there's nothing to do; otherwise propose removing only
|
|
183
183
|
* that key, leaving every other key in the shared file untouched (FR-002).
|
|
184
184
|
*/
|
|
185
|
-
function planRemoveGeminiHooks({ settingsFile, fsImpl = fs }
|
|
185
|
+
function planRemoveGeminiHooks({ settingsFile, fsImpl = fs } = {}) {
|
|
186
186
|
if (!settingsFile) return { ok: false, status: 'invalid-request', changes: [], errors: ['settingsFile is required'] };
|
|
187
187
|
if (!fsImpl.existsSync(settingsFile)) return { ok: true, status: 'unchanged', settingsFile, changes: [] };
|
|
188
188
|
let existing;
|
|
189
189
|
try { existing = JSON.parse(fsImpl.readFileSync(settingsFile, 'utf8')); }
|
|
190
|
-
catch (error
|
|
190
|
+
catch (error ) { return { ok: false, status: 'malformed', changes: [], errors: [`Could not parse ${settingsFile}: ${error.message}`] }; }
|
|
191
191
|
if (existing.hooks === undefined) return { ok: true, status: 'unchanged', settingsFile, changes: [] };
|
|
192
192
|
const { hooks: _dropped, ...merged } = existing;
|
|
193
193
|
return { ok: true, status: 'change', settingsFile, merged, changes: [{ type: 'update', file: settingsFile, key: 'hooks', operation: 'remove' }] };
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function deployRemoveGeminiHooks(plan
|
|
196
|
+
function deployRemoveGeminiHooks(plan , { dryRun = false, fsImpl = fs } = {}) {
|
|
197
197
|
if (!plan.ok || plan.status !== 'change' || dryRun) return { ...plan, applied: false };
|
|
198
198
|
fsImpl.mkdirSync(path.dirname(plan.settingsFile), { recursive: true });
|
|
199
199
|
const content = `${JSON.stringify(plan.merged, null, 2)}\n`;
|