@khoavu882/doflow 2.0.0-beta.4 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +24 -1
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +1 -1
- package/src/adapters/antigravity/index.js +83 -47
- package/src/adapters/claude/index.js +15 -1
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +52 -9
- package/src/adapters/copy-tree.js +4 -4
- package/src/adapters/gemini/hooks.js +57 -16
- package/src/adapters/gemini/index.js +57 -18
- package/src/adapters/kiro/index.js +15 -1
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/index.js +34 -9
- package/src/cli/commands/install.js +1 -1
- package/src/cli/commands/update.js +1 -1
- package/src/cli/index.js +17 -5
- package/src/cli/runtime-commands.js +51 -12
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/index.js +28 -14
- package/src/registry/index.js +136 -7
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/claims.js +14 -5
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +35 -268
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1029 -244
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +7 -0
- package/src/runtime/freshness.js +44 -7
- package/src/runtime/outcome.js +1 -1
- package/src/runtime/readiness.js +83 -13
- package/src/runtime/retrieval-plan.js +422 -73
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/artifacts.js +3 -0
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/task-attempts.js +1 -1
- package/src/runtime/task-scope.js +28 -3
- package/src/runtime/task-state.js +72 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
|
@@ -578,17 +578,17 @@ function sharedTreeDestDir(rootDir , nativeDir ) {
|
|
|
578
578
|
return dest;
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
-
/** The assets that project a tree several harnesses claim at one scope root: the runtime
|
|
582
|
-
*
|
|
581
|
+
/** The assets that project a tree several harnesses claim at one scope root: the runtime, guidance,
|
|
582
|
+
* and shared hooks trees. A harness whose
|
|
583
583
|
* install predates its own rows for a shared tree (Pi gained guidance rows in v1.15.0) can only
|
|
584
584
|
* recognise the bytes a sibling recorded. */
|
|
585
585
|
const SHARED_TREE_ASSETS = new Set([
|
|
586
586
|
'scripts.doflow', 'runtime.cli', 'runtime.lib', 'runtime.registry',
|
|
587
|
-
'guidance.context-layer', 'kiro.guidance-tree',
|
|
587
|
+
'guidance.context-layer', 'kiro.guidance-tree', 'hooks.shared',
|
|
588
588
|
]);
|
|
589
589
|
|
|
590
590
|
/** For each shared-tree target, the fingerprints other harnesses recorded and which harness
|
|
591
|
-
* recorded each, so an update accepts a runtime or
|
|
591
|
+
* recorded each, so an update accepts a runtime, guidance, or shared hooks 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
593
|
function ledgerSiblingFingerprints(resources , harness ) {
|
|
594
594
|
const byTarget = new Map ();
|
|
@@ -14,6 +14,7 @@
|
|
|
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 crypto = require('node:crypto');
|
|
17
18
|
const { verifyHookCommands } = require('../hook-commands.js');
|
|
18
19
|
const { mergeHooks } = require('../../helper/settings-merge.js');
|
|
19
20
|
|
|
@@ -103,7 +104,39 @@ function classifyClaudeGuardrails(claudeHooks = {})
|
|
|
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'));
|
|
@@ -124,20 +157,26 @@ function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, des
|
|
|
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 ) => !name.endsWith('.json') && fsImpl.statSync(path.join(sourceHooksDir, name)).isFile())
|
|
135
|
-
: [];
|
|
136
175
|
const companionTargets = [...new Set([
|
|
137
176
|
...commands.checks.map((check ) => path.join(destinationHooksDir, check.script)),
|
|
138
|
-
...
|
|
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
|
}
|
|
@@ -145,14 +184,16 @@ function planGeminiHooks({ config, sourceFile, sourceHooksDir, settingsFile, des
|
|
|
145
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
198
|
const deployed = new Set ();
|
|
158
199
|
for (const check of plan.commands.checks) {
|
|
@@ -207,4 +248,4 @@ function deployRemoveGeminiHooks(plan , { dryRun = false, fsImpl = fs }
|
|
|
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 };
|
|
@@ -9,7 +9,7 @@ const fs = require('node:fs');
|
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.js');
|
|
11
11
|
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
|
|
12
|
-
const { planGeminiHooks, deployGeminiHooks, planRemoveGeminiHooks, deployRemoveGeminiHooks } = require('./hooks.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 -->';
|
|
@@ -229,17 +229,32 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
229
229
|
return assets.find((asset ) => asset.id === 'guidance.core')?.id ?? assets[0]?.id;
|
|
230
230
|
}
|
|
231
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));
|
|
235
|
+
}
|
|
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: [] };
|
|
@@ -270,7 +285,7 @@ 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,
|
|
@@ -331,6 +346,37 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
331
346
|
removeCopyTreeAssets(changes, { fsImpl });
|
|
332
347
|
}
|
|
333
348
|
|
|
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
|
+
|
|
334
380
|
function verify({ scope, scopeRoot, assets = [], context = {}, fsImpl = fs } ) {
|
|
335
381
|
const found = discover({ scope, scopeRoot, context, fsImpl });
|
|
336
382
|
const resources = [];
|
|
@@ -339,23 +385,16 @@ function createGeminiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] }
|
|
|
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();
|
|
@@ -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
|
};
|
|
@@ -258,6 +258,19 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
|
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/** The hook front doors (IC-037): the scripts and the agent that wires them. */
|
|
262
|
+
function hookDoorAssets(assets ) { return (assets || []).filter((asset ) => asset.id === 'kiro.hooks-scripts' || asset.id === 'kiro.doflow-agent'); }
|
|
263
|
+
|
|
264
|
+
function planHookDoors({ scope, scopeRoot, assets = [], context = {}, ledger, fsImpl = fs } ) {
|
|
265
|
+
const doors = planCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context, ledger, removing: false, fsImpl });
|
|
266
|
+
return { changes: doors.changes, conflicts: doors.conflicts, prerequisites: [], notices: doors.notices };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function verifyHookDoors({ scope, scopeRoot, assets = [], context = {}, fsImpl = fs } ) {
|
|
270
|
+
const doors = verifyCopyTreeAssets({ assets: hookDoorAssets(assets), scope, scopeRoot, context, fsImpl });
|
|
271
|
+
return { ok: doors.conflicts.length === 0, statuses: doors.statuses, resources: doors.resources, conflicts: doors.conflicts };
|
|
272
|
+
}
|
|
273
|
+
|
|
261
274
|
function apply({ changes = [], fsImpl = fs } ) {
|
|
262
275
|
writeMcpEntries(changes, { container: MCP_CONTAINER, fsImpl });
|
|
263
276
|
applyCopyTreeAssets(changes, { fsImpl });
|
|
@@ -297,7 +310,7 @@ function createKiroAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } =
|
|
|
297
310
|
};
|
|
298
311
|
}
|
|
299
312
|
|
|
300
|
-
return { nativePaths, discover, render, plan, apply, remove, verify, buildServerEntry };
|
|
313
|
+
return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors, buildServerEntry };
|
|
301
314
|
}
|
|
302
315
|
|
|
303
316
|
const singleton = createKiroAdapter();
|
|
@@ -305,6 +318,7 @@ const singleton = createKiroAdapter();
|
|
|
305
318
|
module.exports = {
|
|
306
319
|
nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
|
|
307
320
|
plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
|
|
321
|
+
planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
|
|
308
322
|
buildServerEntry: singleton.buildServerEntry,
|
|
309
323
|
createKiroAdapter,
|
|
310
324
|
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { fingerprint } = require('../copy-tree.js');
|
|
8
|
+
|
|
9
|
+
const ASSET_ID = 'pi.guard-extension';
|
|
10
|
+
const EXTENSION_NAME = 'doflow-guard.js';
|
|
11
|
+
const OWNERSHIP = 'pi:guard-extension';
|
|
12
|
+
const TEMPLATE = 'core/harnesses/pi/extensions/doflow-guard.js';
|
|
13
|
+
const BRIDGE_SUFFIX = ['.doflow', 'shared', 'hooks', 'bridge', 'bridge.sh'];
|
|
14
|
+
|
|
15
|
+
function userAgentDir(scopeRoot , env ) {
|
|
16
|
+
const override = env?.PI_CODING_AGENT_DIR;
|
|
17
|
+
return typeof override === 'string' && override.trim() ? path.resolve(override.trim()) : path.join(scopeRoot, '.pi', 'agent');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function targetPath(scopeRoot , env , scope = 'user') {
|
|
21
|
+
return scope === 'project'
|
|
22
|
+
? path.join(scopeRoot, '.pi', 'extensions', EXTENSION_NAME)
|
|
23
|
+
: path.join(userAgentDir(scopeRoot, env), 'extensions', EXTENSION_NAME);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderExtension({ repoRoot, scopeRoot, target, fsImpl = fs } ) {
|
|
27
|
+
const templatePath = path.join(repoRoot, TEMPLATE);
|
|
28
|
+
const template = fsImpl.readFileSync(templatePath, 'utf8');
|
|
29
|
+
const bridgePath = path.join(scopeRoot, ...BRIDGE_SUFFIX);
|
|
30
|
+
const relative = path.relative(path.dirname(target), bridgePath).split(path.sep).join('/');
|
|
31
|
+
const rendered = template.replace("const BRIDGE = new URL('../../shared/hooks/bridge/bridge.sh', import.meta.url);",
|
|
32
|
+
`const BRIDGE = new URL(${JSON.stringify(relative)}, import.meta.url);`);
|
|
33
|
+
if (rendered === template) throw new Error('Pi extension bridge path template was not found');
|
|
34
|
+
return rendered;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function planPiHooks({ scope, scopeRoot, assets = [], ledger, context = {}, adopt = false, env = process.env, fsImpl = fs } ) {
|
|
38
|
+
if (scope !== 'user' && scope !== 'global' && scope !== 'project') return { changes: [], conflicts: [], notices: [] };
|
|
39
|
+
const asset = assets.find((row ) => row.id === ASSET_ID);
|
|
40
|
+
if (!asset) return { changes: [], conflicts: [], notices: [] };
|
|
41
|
+
const computedTarget = targetPath(scopeRoot, env, scope);
|
|
42
|
+
const removing = context.operation === 'remove';
|
|
43
|
+
const ownedRows = (ledger?.resources || []).filter((row ) => row.harness === 'pi' && row.assetId === ASSET_ID
|
|
44
|
+
&& row.ownershipIdentity === OWNERSHIP);
|
|
45
|
+
const exactOwned = ownedRows.find((row ) => row.target === computedTarget);
|
|
46
|
+
const owned = exactOwned || (removing && ownedRows.length === 1 ? ownedRows[0] : undefined);
|
|
47
|
+
const target = removing && owned ? owned.target : computedTarget;
|
|
48
|
+
const exists = fsImpl.existsSync(target);
|
|
49
|
+
const notices = [];
|
|
50
|
+
if (removing) {
|
|
51
|
+
if (!owned) return { changes: [], conflicts: [], notices };
|
|
52
|
+
if (!exists) return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', release: true,
|
|
53
|
+
ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
|
|
54
|
+
const current = fsImpl.readFileSync(target, 'utf8');
|
|
55
|
+
if (fingerprint(current) !== owned.fingerprint) {
|
|
56
|
+
notices.push(`kept hand-edited ${target}; DoFlow no longer manages it`);
|
|
57
|
+
return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', release: true,
|
|
58
|
+
ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
|
|
59
|
+
}
|
|
60
|
+
return { changes: [{ assetId: ASSET_ID, target, operation: 'remove', content: null,
|
|
61
|
+
ownershipIdentity: OWNERSHIP, harness: 'pi', projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const content = renderExtension({ repoRoot: context.repoRoot, scopeRoot, target, fsImpl });
|
|
65
|
+
if (!exists) return { changes: [{ assetId: ASSET_ID, target, operation: 'create', content,
|
|
66
|
+
ownershipIdentity: OWNERSHIP, harness: 'pi', fingerprint: fingerprint(content),
|
|
67
|
+
projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
|
|
68
|
+
if (!owned && !adopt) {
|
|
69
|
+
return { changes: [], conflicts: [`Pi extension already exists and is not owned by DoFlow: ${target}`], notices };
|
|
70
|
+
}
|
|
71
|
+
const current = fsImpl.readFileSync(target, 'utf8');
|
|
72
|
+
if (owned && fingerprint(current) !== owned.fingerprint) {
|
|
73
|
+
return { changes: [], conflicts: [`Pi extension was modified outside DoFlow: ${target}`], notices };
|
|
74
|
+
}
|
|
75
|
+
if (current === content) return { changes: [], conflicts: [], notices };
|
|
76
|
+
return { changes: [{ assetId: ASSET_ID, target, operation: 'update', content,
|
|
77
|
+
ownershipIdentity: OWNERSHIP, harness: 'pi', fingerprint: fingerprint(content),
|
|
78
|
+
projection: { renderer: 'pi-guard-extension' } }], conflicts: [], notices };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function verifyPiHooks({ scope, scopeRoot, assets = [], ledger, context = {}, adopt = false, env = process.env, fsImpl = fs, pathEnv = process.env.PATH } ) {
|
|
82
|
+
if (scope !== 'user' && scope !== 'global' && scope !== 'project') return { statuses: [], resources: [], conflicts: [] };
|
|
83
|
+
const asset = assets.find((row ) => row.id === ASSET_ID);
|
|
84
|
+
if (!asset) return { statuses: [], resources: [], conflicts: [] };
|
|
85
|
+
const target = targetPath(scopeRoot, env, scope);
|
|
86
|
+
const owned = (ledger?.resources || []).find((row ) => row.harness === 'pi' && row.assetId === ASSET_ID
|
|
87
|
+
&& row.ownershipIdentity === OWNERSHIP && row.target === target);
|
|
88
|
+
const bridgePath = path.join(scopeRoot, ...BRIDGE_SUFFIX);
|
|
89
|
+
const present = fsImpl.existsSync(target);
|
|
90
|
+
const content = present ? fsImpl.readFileSync(target, 'utf8') : null;
|
|
91
|
+
const actualFingerprint = content === null ? null : fingerprint(content);
|
|
92
|
+
const fingerprintMatches = !!owned && actualFingerprint === owned.fingerprint;
|
|
93
|
+
const plannedWrite = (context?.plannedWrites || []).includes(target);
|
|
94
|
+
const bridgeMatches = !!content && content.includes(JSON.stringify(path.relative(path.dirname(target), bridgePath).split(path.sep).join('/')));
|
|
95
|
+
const commandOnPath = (command ) => String(pathEnv || '').split(path.delimiter)
|
|
96
|
+
.some((dir) => fsImpl.existsSync(path.join(dir, command)));
|
|
97
|
+
const status = present && !owned && !plannedWrite && !adopt ? 'conflict'
|
|
98
|
+
: (present && (fingerprintMatches || plannedWrite) && bridgeMatches && commandOnPath('bash') && commandOnPath('jq') ? 'managed' : 'absent');
|
|
99
|
+
const resources = status === 'managed' ? [{ assetId: ASSET_ID, target, ownershipIdentity: OWNERSHIP,
|
|
100
|
+
fingerprint: actualFingerprint, sourceVersion: context.sourceVersion ?? 'unknown',
|
|
101
|
+
projection: { renderer: 'pi-guard-extension' } }] : [];
|
|
102
|
+
const statuses = [{ harness: 'pi', assetId: ASSET_ID, capability: 'hooks', status, target,
|
|
103
|
+
ownershipIdentity: OWNERSHIP, fingerprint: actualFingerprint, fingerprintMatches,
|
|
104
|
+
bridgePath, bridgeExists: fsImpl.existsSync(bridgePath), bash: commandOnPath('bash'), jq: commandOnPath('jq'),
|
|
105
|
+
prerequisites: scope === 'project' ? ['project-trust'] : [], piCodingAgentDir: typeof env?.PI_CODING_AGENT_DIR === 'string' && env.PI_CODING_AGENT_DIR.trim() ? env.PI_CODING_AGENT_DIR.trim() : null }];
|
|
106
|
+
const conflicts = status === 'conflict' ? [`Pi extension is not owned by DoFlow: ${target}`] : [];
|
|
107
|
+
return { statuses, resources, conflicts };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function removePiHooks(changes , { fsImpl = fs } = {}) {
|
|
111
|
+
let removed = 0;
|
|
112
|
+
for (const change of changes) {
|
|
113
|
+
if (change.projection?.renderer !== 'pi-guard-extension' || change.operation !== 'remove' || change.release) continue;
|
|
114
|
+
if (fsImpl.existsSync(change.target)) { fsImpl.unlinkSync(change.target); removed += 1; }
|
|
115
|
+
}
|
|
116
|
+
return removed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { ASSET_ID, EXTENSION_NAME, OWNERSHIP, userAgentDir, targetPath, renderExtension, planPiHooks, verifyPiHooks, removePiHooks };
|
package/src/adapters/pi/index.js
CHANGED
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// MCP servers are merged per server into Pi's mcp.json (.pi/mcp.json project; ~/.pi/agent/mcp.json
|
|
15
15
|
// or $PI_CODING_AGENT_DIR/mcp.json user) by ./mcp.ts; Pi reads it through its built-in MCP from
|
|
16
|
-
// 0.99.0.
|
|
17
|
-
// executing with full system permissions, which is a different trust model from the shell scripts
|
|
18
|
-
// DoFlow ships.
|
|
16
|
+
// 0.99.0. The user- and project-scope lifecycle extension is ledger-owned and resolves its shared bridge at install time.
|
|
19
17
|
//
|
|
20
18
|
// Evidence: https://pi.dev/docs/latest/settings, https://pi.dev/docs/latest/skills,
|
|
21
19
|
// https://pi.dev/docs/latest/quickstart, https://pi.dev/docs/latest/extensions,
|
|
@@ -27,9 +25,11 @@ const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
|
|
|
27
25
|
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor } = require('../copy-tree.js');
|
|
28
26
|
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
|
|
29
27
|
const { mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp } = require('./mcp.js');
|
|
28
|
+
const { planPiHooks, verifyPiHooks, removePiHooks } = require('./hooks.js');
|
|
30
29
|
|
|
31
30
|
const HARNESS = 'pi';
|
|
32
|
-
const
|
|
31
|
+
const AGENT_DIR_MCP_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, but DoFlow moves only mcp.json there; skills and AGENTS.md stay in ~/.pi/agent.';
|
|
32
|
+
const AGENT_DIR_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, and DoFlow moves mcp.json and the guard extension there; skills and AGENTS.md stay in ~/.pi/agent.';
|
|
33
33
|
|
|
34
34
|
// Native path facts live in core/registry/harnesses.json under this harness's "paths" section and
|
|
35
35
|
// resolve through the shared harness-paths resolver; nothing below hardcodes them.
|
|
@@ -43,8 +43,8 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
43
43
|
function nativePaths({ scope, scopeRoot } = {}) {
|
|
44
44
|
const paths = resolveHarnessPaths(declaredPaths, { scope, scopeRoot });
|
|
45
45
|
// Pi's user-scope mcp.json follows $PI_CODING_AGENT_DIR when set. It is not declared in the
|
|
46
|
-
// registry because the path schema has no environment base (src/helper/harness-paths.js);
|
|
47
|
-
//
|
|
46
|
+
// registry because the path schema has no environment base (src/helper/harness-paths.js); the
|
|
47
|
+
// guard extension follows it too, while skills and AGENTS.md keep their declared locations.
|
|
48
48
|
if ((scope === 'user' || scope === 'global') && agentDirOverride()) paths.mcp = path.join(path.resolve(agentDirOverride()), 'mcp.json');
|
|
49
49
|
return paths;
|
|
50
50
|
}
|
|
@@ -191,15 +191,32 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
191
191
|
changes.push(...copyTree.changes);
|
|
192
192
|
conflicts.push(...copyTree.conflicts);
|
|
193
193
|
|
|
194
|
+
const piHooks = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
195
|
+
changes.push(...piHooks.changes);
|
|
196
|
+
conflicts.push(...piHooks.conflicts);
|
|
197
|
+
|
|
194
198
|
const piMcp = planPiMcp({ selected: mcp, rows: mcpRows(ledger), file: found.paths.mcp, scope, removing, snapshot: found.mcp, assets });
|
|
195
199
|
changes.push(...piMcp.changes);
|
|
196
200
|
conflicts.push(...piMcp.conflicts);
|
|
197
201
|
|
|
198
|
-
const notices = [...copyTree.notices, ...piMcp.notices];
|
|
199
|
-
if (!removing && scope !== 'project' && agentDirOverride())
|
|
202
|
+
const notices = [...copyTree.notices, ...piMcp.notices, ...piHooks.notices];
|
|
203
|
+
if (!removing && scope !== 'project' && agentDirOverride()) {
|
|
204
|
+
notices.push(assets.some((asset ) => asset.id === 'pi.guard-extension') ? AGENT_DIR_NOTICE : AGENT_DIR_MCP_NOTICE);
|
|
205
|
+
}
|
|
200
206
|
return { changes, conflicts, notices, paths: found.paths };
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
/** The hook front door (IC-037): the guard extension. */
|
|
210
|
+
function planHookDoors({ scope, scopeRoot, assets = [], context = {}, ledger, fsImpl = fs } ) {
|
|
211
|
+
const door = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
212
|
+
return { changes: door.changes, conflicts: door.conflicts, notices: door.notices };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function verifyHookDoors({ scope, scopeRoot, assets = [], ledger, context = {}, fsImpl = fs } ) {
|
|
216
|
+
const door = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
217
|
+
return { ok: door.conflicts.length === 0 && !door.statuses.some((s ) => s.status === 'invalid' || s.status === 'conflict'), ...door };
|
|
218
|
+
}
|
|
219
|
+
|
|
203
220
|
function writeChange(change , fsImpl ) {
|
|
204
221
|
fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
|
|
205
222
|
fsImpl.writeFileSync(change.target, change.content, 'utf8');
|
|
@@ -226,10 +243,12 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
226
243
|
let removed = applyPiMcp(changes.filter(isPiMcp), { fsImpl }).applied;
|
|
227
244
|
for (const change of changes) {
|
|
228
245
|
if (change.operation !== 'remove' || change.projection?.renderer === 'copy-tree' || isPiMcp(change)) continue;
|
|
246
|
+
if (change.projection?.renderer === 'pi-guard-extension') continue;
|
|
229
247
|
if (change.content === null) continue;
|
|
230
248
|
writeChange(change, fsImpl);
|
|
231
249
|
removed += 1;
|
|
232
250
|
}
|
|
251
|
+
removed += removePiHooks(changes, { fsImpl });
|
|
233
252
|
removed += removeCopyTreeAssets(changes, { fsImpl });
|
|
234
253
|
return { removed };
|
|
235
254
|
}
|
|
@@ -263,10 +282,15 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
263
282
|
resources.push(...piMcp.resources);
|
|
264
283
|
conflicts.push(...piMcp.conflicts);
|
|
265
284
|
|
|
285
|
+
const piHooks = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
286
|
+
statuses.push(...piHooks.statuses);
|
|
287
|
+
resources.push(...piHooks.resources);
|
|
288
|
+
conflicts.push(...piHooks.conflicts);
|
|
289
|
+
|
|
266
290
|
return { ok: conflicts.length === 0 && !statuses.some((s) => s.status === 'invalid' || s.status === 'conflict'), resources, statuses, conflicts };
|
|
267
291
|
}
|
|
268
292
|
|
|
269
|
-
return { nativePaths, discover, render, plan, apply, remove, verify };
|
|
293
|
+
return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
const singleton = createPiAdapter();
|
|
@@ -274,6 +298,7 @@ const singleton = createPiAdapter();
|
|
|
274
298
|
module.exports = {
|
|
275
299
|
HARNESS, nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
|
|
276
300
|
plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
|
|
301
|
+
planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
|
|
277
302
|
managedInstruction: (singleton ).managedInstruction, strippedInstruction: (singleton ).strippedInstruction,
|
|
278
303
|
createPiAdapter,
|
|
279
304
|
};
|
|
@@ -55,7 +55,7 @@ function cmdInstall(o ) {
|
|
|
55
55
|
// One lifecycle view across every requested target — computed unconditionally (not only under
|
|
56
56
|
// --dry-run) so its safety gate and its plan are the exact same object the real apply below uses.
|
|
57
57
|
const lifecycleView = registryLifecycleView({ registry, repoRoot: REPO_ROOT, scope, dirs, targets,
|
|
58
|
-
mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds,
|
|
58
|
+
mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds, operation: 'install',
|
|
59
59
|
force: o.force, adopt: o.adopt === true, permissions: o.permissions === true, statusline: o.statusline === true });
|
|
60
60
|
if (!lifecycleView.plan.safe) { assertSafeRegistryPlan(lifecycleView); return; }
|
|
61
61
|
|
|
@@ -51,7 +51,7 @@ function cmdUpdate(o ) {
|
|
|
51
51
|
// One lifecycle view across every requested target — computed unconditionally (not only under
|
|
52
52
|
// --dry-run) so its safety gate and its plan are the exact same object the real apply below uses.
|
|
53
53
|
const lifecycleView = registryLifecycleView({ registry, repoRoot: REPO_ROOT, scope, dirs, targets,
|
|
54
|
-
mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds,
|
|
54
|
+
mcpSelections: selection.selections, mcpAdoptable: selection.adoptable, retainedMcpIds: selection.retainedMcpIds, operation: 'update',
|
|
55
55
|
force: o.force, permissions: o.permissions === true, statusline: o.statusline === true });
|
|
56
56
|
if (!lifecycleView.plan.safe) { assertSafeRegistryPlan(lifecycleView); return; }
|
|
57
57
|
printMcpSelection(lifecycleView, selection.sources, { requested: o.mcp, prefix: o.dryRun ? '[DRY]' : '[INFO]' });
|