@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
//
|
|
7
7
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
8
8
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
9
|
-
// caller's `require('../install/manifest.
|
|
9
|
+
// caller's `require('../install/manifest.js')` keeps working while the tree is still `.js`.
|
|
10
10
|
const fs = require('node:fs');
|
|
11
11
|
const path = require('node:path');
|
|
12
|
-
const { sourceCommit: gitSourceCommit } = require('../helper/git.
|
|
12
|
+
const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
13
13
|
|
|
14
14
|
// Explicit migration bridge: pre-registry installations stored this record beneath .claude.
|
|
15
15
|
// Lifecycle commands use the scope-neutral .doflow location; only the neutral-state importer
|
|
@@ -17,27 +17,27 @@ const { sourceCommit: gitSourceCommit } = require('../helper/git.ts');
|
|
|
17
17
|
const LEGACY_MANIFEST_FILE_NAME = '.install-manifest.json';
|
|
18
18
|
const MANIFEST_FILE_NAME = LEGACY_MANIFEST_FILE_NAME;
|
|
19
19
|
|
|
20
|
-
function manifestPath(claudeDir
|
|
20
|
+
function manifestPath(claudeDir ) {
|
|
21
21
|
return path.join(claudeDir, MANIFEST_FILE_NAME);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function canonicalManifestPath(scopeRoot
|
|
24
|
+
function canonicalManifestPath(scopeRoot ) {
|
|
25
25
|
return path.join(path.resolve(scopeRoot), '.doflow', MANIFEST_FILE_NAME);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* `sourceCommit` lets a caller (bin/doflow.js) pass an already-resolved commit instead of this
|
|
@@ -48,16 +48,16 @@ interface WriteManifestInput {
|
|
|
48
48
|
* for fine-grained Codex resources. Each record uses {target,scope,kind,identity,sourceVersion,
|
|
49
49
|
* fingerprint,selection,recoveryPoint}. Omit it to preserve a ledger written by a newer command.
|
|
50
50
|
*/
|
|
51
|
-
function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoot, backupId = '', tools, date, dryRun = false, sourceCommit, managedResources }
|
|
51
|
+
function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoot, backupId = '', tools, date, dryRun = false, sourceCommit, managedResources } ) {
|
|
52
52
|
if (!scopeRoot && !claudeDir) throw new Error('scopeRoot is required for lifecycle metadata');
|
|
53
|
-
const file = scopeRoot ? canonicalManifestPath(scopeRoot) : manifestPath(claudeDir
|
|
53
|
+
const file = scopeRoot ? canonicalManifestPath(scopeRoot) : manifestPath(claudeDir );
|
|
54
54
|
if (dryRun) return file;
|
|
55
55
|
|
|
56
56
|
// Preserve per-tool last_updated for tools NOT part of this operation (matches sync.sh's
|
|
57
57
|
// incremental jq merge, which only touches the tools passed to write_manifest).
|
|
58
|
-
let existingTools
|
|
59
|
-
let existingMcpServers
|
|
60
|
-
let existingManagedResources
|
|
58
|
+
let existingTools = {};
|
|
59
|
+
let existingMcpServers ;
|
|
60
|
+
let existingManagedResources ;
|
|
61
61
|
const existingFile = file;
|
|
62
62
|
if (existingFile && fs.existsSync(existingFile)) {
|
|
63
63
|
try {
|
|
@@ -72,10 +72,10 @@ function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoo
|
|
|
72
72
|
} catch { /* start fresh */ }
|
|
73
73
|
}
|
|
74
74
|
const ts = date.toISOString().replace(/\.\d+Z$/, 'Z');
|
|
75
|
-
const toolsOut
|
|
75
|
+
const toolsOut = { ...existingTools };
|
|
76
76
|
for (const t of tools) toolsOut[t] = { installed: true, last_updated: ts };
|
|
77
77
|
|
|
78
|
-
const manifest
|
|
78
|
+
const manifest = {
|
|
79
79
|
script_version: scriptVersion,
|
|
80
80
|
last_operation: operation,
|
|
81
81
|
last_run: ts,
|
|
@@ -102,7 +102,7 @@ function writeManifest({ scopeRoot, claudeDir, scriptVersion, operation, repoRoo
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/** null if no manifest yet. */
|
|
105
|
-
function readManifestFile(file
|
|
105
|
+
function readManifestFile(file ) {
|
|
106
106
|
if (!fs.existsSync(file)) return null;
|
|
107
107
|
try {
|
|
108
108
|
const m = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -129,13 +129,13 @@ function readManifestFile(file: string): any {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/** Read only the canonical scope-neutral manifest used by lifecycle commands. */
|
|
132
|
-
function readInstallManifest({ scopeRoot }
|
|
132
|
+
function readInstallManifest({ scopeRoot } ) {
|
|
133
133
|
if (!scopeRoot) throw new Error('scopeRoot is required');
|
|
134
134
|
return readManifestFile(canonicalManifestPath(scopeRoot));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** Read only the legacy path; neutral-state migration uses this deliberately. */
|
|
138
|
-
function readManifest(claudeDir
|
|
138
|
+
function readManifest(claudeDir ) {
|
|
139
139
|
return readManifestFile(manifestPath(claudeDir));
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -5,24 +5,24 @@
|
|
|
5
5
|
//
|
|
6
6
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
7
7
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
8
|
-
// caller's `require('../install/mcp.
|
|
8
|
+
// caller's `require('../install/mcp.js')` keeps working while the tree is still `.js`. The
|
|
9
9
|
// `../registry` and `../state/lockfile` requires stay bare while those modules are still `.js`.
|
|
10
|
-
const { readSyncBlocking } = require('../helper/prompt.
|
|
11
|
-
const { selectMcpServers, nativeMcpCatalog, mcpCapable } = require('../registry/index.
|
|
12
|
-
const { pinnedSelections } = require('../state/lockfile.
|
|
10
|
+
const { readSyncBlocking } = require('../helper/prompt.js');
|
|
11
|
+
const { selectMcpServers, nativeMcpCatalog, mcpCapable } = require('../registry/index.js');
|
|
12
|
+
const { pinnedSelections } = require('../state/lockfile.js');
|
|
13
13
|
|
|
14
14
|
const ESC = String.fromCharCode(27);
|
|
15
15
|
const CTRL_C = String.fromCharCode(3);
|
|
16
16
|
|
|
17
17
|
/** `registry` is a loaded registry (src/registry#loadRegistry); returns server names in registry
|
|
18
18
|
* declaration order. */
|
|
19
|
-
function readAllServers(registry
|
|
19
|
+
function readAllServers(registry ) {
|
|
20
20
|
return nativeMcpCatalog(selectMcpServers(registry)).allServers;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/** Parse `--mcp`: `all` and `none` are keywords that cannot be mixed with names or each other, an
|
|
24
24
|
* empty list and an unknown name are errors, names are deduplicated. `null` when the flag is absent. */
|
|
25
|
-
function parseMcpFlag(requested
|
|
25
|
+
function parseMcpFlag(requested , catalogIds ) {
|
|
26
26
|
if (!requested) return null;
|
|
27
27
|
const keywords = requested.filter((s) => s === 'all' || s === 'none');
|
|
28
28
|
if (keywords.length) {
|
|
@@ -44,27 +44,27 @@ function parseMcpFlag(requested: string[] | undefined, catalogIds: string[]): st
|
|
|
44
44
|
return [...new Set(requested)];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const holdsRows = (ledger
|
|
47
|
+
const holdsRows = (ledger , harness ) => (ledger?.resources ?? []).some((row ) => row.harness === harness);
|
|
48
48
|
|
|
49
49
|
/** Ids of `lists` that the catalog still declares, as one union in registry order. */
|
|
50
|
-
const catalogUnion = (catalogIds
|
|
50
|
+
const catalogUnion = (catalogIds , lists ) => catalogIds.filter((id) => lists.some((ids) => ids.includes(id)));
|
|
51
51
|
|
|
52
52
|
/** 1.18.0 wrote lock rows only for claude and codex, and only non-empty ones. Any other row was
|
|
53
53
|
* written by a later run, which already settled which of that harness's entries are DoFlow's. */
|
|
54
54
|
const LEGACY_LOCK_HARNESSES = ['claude', 'codex'];
|
|
55
55
|
|
|
56
|
-
function settledByLaterRun(rows
|
|
56
|
+
function settledByLaterRun(rows , harness ) {
|
|
57
57
|
return harness in rows && (!LEGACY_LOCK_HARNESSES.includes(harness) || rows[harness].length === 0);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** The servers a 1.18.0 install recorded writing for this harness, from state no later run writes:
|
|
61
61
|
* the manifest's MCP list, Antigravity's rows that carry no fingerprint, and Copilot's whole-map
|
|
62
62
|
* registration row, which covers the manifest's list or, with none, the catalog 1.18.0 wrote. */
|
|
63
|
-
function legacyRecordedIds({ catalogIds, ledger, harness, manifestServers }
|
|
64
|
-
const rows = (ledger?.resources ?? []).filter((row
|
|
63
|
+
function legacyRecordedIds({ catalogIds, ledger, harness, manifestServers } ) {
|
|
64
|
+
const rows = (ledger?.resources ?? []).filter((row ) => row.harness === harness);
|
|
65
65
|
const ids = [...(manifestServers ?? [])];
|
|
66
66
|
for (const row of rows) if (row.kind === 'mcp-server' && (row.fingerprint ?? null) === null) ids.push(row.identity);
|
|
67
|
-
if (!manifestServers && rows.some((row
|
|
67
|
+
if (!manifestServers && rows.some((row ) => row.ownershipIdentity === `${harness}:mcp:registration`)) ids.push(...catalogIds);
|
|
68
68
|
return catalogUnion(catalogIds, [ids]);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -72,7 +72,7 @@ function legacyRecordedIds({ catalogIds, ledger, harness, manifestServers }: { c
|
|
|
72
72
|
* only what a 1.18.0 install recorded for a harness the ledger holds, and only until a later run
|
|
73
73
|
* has recorded the harness's own selection. An entry a user wrote is never adoptable merely because
|
|
74
74
|
* DoFlow installed something else for that harness. Retired ids are dropped. */
|
|
75
|
-
function adoptableMcpIds({ registry, lock, ledger, harnesses, manifestServers = null }
|
|
75
|
+
function adoptableMcpIds({ registry, lock, ledger, harnesses, manifestServers = null } ) {
|
|
76
76
|
const catalogIds = readAllServers(registry);
|
|
77
77
|
const rows = pinnedSelections(lock);
|
|
78
78
|
return Object.fromEntries(harnesses.filter((harness) => mcpCapable(registry, harness)).map((harness) => {
|
|
@@ -83,7 +83,7 @@ function adoptableMcpIds({ registry, lock, ledger, harnesses, manifestServers =
|
|
|
83
83
|
|
|
84
84
|
/** Servers another harness of this scope still has recorded: the union of the lock rows of the lock's
|
|
85
85
|
* harnesses that are not targeted, registry order. */
|
|
86
|
-
function retainedMcpIds(catalogIds
|
|
86
|
+
function retainedMcpIds(catalogIds , rows , targets ) {
|
|
87
87
|
return catalogUnion(catalogIds, Object.entries(rows).filter(([harness]) => !targets.includes(harness)).map(([, ids]) => ids));
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -100,26 +100,26 @@ function retainedMcpIds(catalogIds: string[], rows: Record<string, string[]>, ta
|
|
|
100
100
|
*/
|
|
101
101
|
function resolveMcpSelections({
|
|
102
102
|
cmd, requested, targets, registry, lock, ledger, manifestServers = null, interactive = false, promptFn, onStale,
|
|
103
|
-
}
|
|
103
|
+
} ) {
|
|
104
104
|
const catalogIds = readAllServers(registry);
|
|
105
105
|
const flagged = parseMcpFlag(requested, catalogIds);
|
|
106
106
|
const rows = pinnedSelections(lock);
|
|
107
|
-
const capable = targets.filter((harness
|
|
108
|
-
const retired = new Set
|
|
109
|
-
const known = (ids
|
|
107
|
+
const capable = targets.filter((harness ) => mcpCapable(registry, harness));
|
|
108
|
+
const retired = new Set ();
|
|
109
|
+
const known = (ids ) => {
|
|
110
110
|
for (const id of ids) if (!catalogIds.includes(id)) retired.add(id);
|
|
111
111
|
return catalogUnion(catalogIds, [ids]);
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
let prompted
|
|
114
|
+
let prompted = null;
|
|
115
115
|
if (!flagged && cmd === 'install' && interactive && capable.length) {
|
|
116
|
-
const recorded = capable.filter((harness
|
|
116
|
+
const recorded = capable.filter((harness ) => harness in rows).map((harness ) => rows[harness]);
|
|
117
117
|
const seed = recorded.length ? catalogUnion(catalogIds, recorded.map(known))
|
|
118
118
|
: manifestServers ? known(manifestServers) : [...catalogIds];
|
|
119
119
|
prompted = promptFn(catalogIds, seed);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const pick = (harness
|
|
122
|
+
const pick = (harness ) => {
|
|
123
123
|
if (flagged) return [flagged, 'flag'];
|
|
124
124
|
if (prompted !== null) return [prompted, 'prompt'];
|
|
125
125
|
if (harness in rows) return [known(rows[harness]), 'recorded'];
|
|
@@ -127,10 +127,10 @@ function resolveMcpSelections({
|
|
|
127
127
|
if (manifestServers) return [known(manifestServers), 'manifest'];
|
|
128
128
|
return [[], 'default'];
|
|
129
129
|
};
|
|
130
|
-
const selections
|
|
131
|
-
const sources
|
|
130
|
+
const selections = {};
|
|
131
|
+
const sources = {};
|
|
132
132
|
for (const harness of capable) [selections[harness], sources[harness]] = pick(harness);
|
|
133
|
-
for (const [harness, ids] of Object.entries(rows)) if (!targets.includes(harness)) known(ids
|
|
133
|
+
for (const [harness, ids] of Object.entries(rows)) if (!targets.includes(harness)) known(ids );
|
|
134
134
|
if (retired.size && onStale) onStale([...retired].sort());
|
|
135
135
|
|
|
136
136
|
return {
|
|
@@ -144,12 +144,12 @@ function resolveMcpSelections({
|
|
|
144
144
|
/** The selections a scope has recorded, for readers that never prompt (reconcile, status, inventory):
|
|
145
145
|
* each targeted MCP-capable harness's lock row, or 'keep' when the lock has none. Retired ids are
|
|
146
146
|
* dropped silently. */
|
|
147
|
-
function recordedMcpSelections({ registry, lock, ledger, targets, manifestServers = null }
|
|
147
|
+
function recordedMcpSelections({ registry, lock, ledger, targets, manifestServers = null } ) {
|
|
148
148
|
const catalogIds = readAllServers(registry);
|
|
149
149
|
const rows = pinnedSelections(lock);
|
|
150
|
-
const capable = targets.filter((harness
|
|
150
|
+
const capable = targets.filter((harness ) => mcpCapable(registry, harness));
|
|
151
151
|
return {
|
|
152
|
-
selections: Object.fromEntries(capable.map((harness
|
|
152
|
+
selections: Object.fromEntries(capable.map((harness ) => [harness,
|
|
153
153
|
harness in rows ? catalogUnion(catalogIds, [rows[harness]]) : 'keep'])),
|
|
154
154
|
adoptable: adoptableMcpIds({ registry, lock, ledger, harnesses: capable, manifestServers }),
|
|
155
155
|
retainedMcpIds: retainedMcpIds(catalogIds, rows, targets),
|
|
@@ -171,7 +171,7 @@ const KEY = {
|
|
|
171
171
|
* case, bounded by a deadline for a genuinely unusable fd). Returns the decoded chunk, or null if
|
|
172
172
|
* the fd is unusable.
|
|
173
173
|
*/
|
|
174
|
-
function readKeypress(buf
|
|
174
|
+
function readKeypress(buf ) {
|
|
175
175
|
try {
|
|
176
176
|
const n = readSyncBlocking(0, buf);
|
|
177
177
|
return buf.toString('utf8', 0, n);
|
|
@@ -187,7 +187,7 @@ function readKeypress(buf: Buffer): string | null {
|
|
|
187
187
|
* the same primitive confirm()/promptLine() already use.
|
|
188
188
|
* Returns selected server names, or null if no usable TTY (caller falls back).
|
|
189
189
|
*/
|
|
190
|
-
function promptMcpCheckbox(servers
|
|
190
|
+
function promptMcpCheckbox(servers , initialSelected , message = 'Select MCP servers to install:') {
|
|
191
191
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
192
192
|
if (servers.length === 0) return [];
|
|
193
193
|
|
|
@@ -195,7 +195,7 @@ function promptMcpCheckbox(servers: string[], initialSelected: string[], message
|
|
|
195
195
|
let cursor = 0;
|
|
196
196
|
const help = ' (up/down or j/k move, space toggle, a toggle-all, enter confirm)';
|
|
197
197
|
|
|
198
|
-
const render = (first
|
|
198
|
+
const render = (first ) => {
|
|
199
199
|
if (!first) process.stdout.write(`${ESC}[${servers.length + 2}A`);
|
|
200
200
|
console.log(message);
|
|
201
201
|
console.log(help);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
4
4
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
5
|
-
// caller's `require('../install/paths.
|
|
5
|
+
// caller's `require('../install/paths.js')` keeps working while the rest of the tree is still `.js`.
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
|
|
8
8
|
const DOFLOW_DIR_NAME = '.doflow';
|
|
@@ -19,26 +19,26 @@ const INSTALL_MANIFEST_FILE_NAME = '.install-manifest.json';
|
|
|
19
19
|
// material and DoFlow no longer owns their lifecycle.
|
|
20
20
|
const LEGACY_BACKUP_ANCHOR_DIR_NAME = '.claude';
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
33
|
|
|
34
34
|
/** Read-only legacy backup location for a scope. Never a write or prune target — see the note above. */
|
|
35
|
-
function legacyBackupReadRoot({ scopeRoot }
|
|
35
|
+
function legacyBackupReadRoot({ scopeRoot } ) {
|
|
36
36
|
if (typeof scopeRoot !== 'string' || !scopeRoot) throw new Error('scopeRoot is required');
|
|
37
37
|
return path.join(path.resolve(scopeRoot), LEGACY_BACKUP_ANCHOR_DIR_NAME, BACKUP_DIR_NAME);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** True for `<anything>/.claude/backups` — used to refuse writes/prunes against the legacy root. */
|
|
41
|
-
function isLegacyBackupReadRoot(candidate
|
|
41
|
+
function isLegacyBackupReadRoot(candidate ) {
|
|
42
42
|
if (typeof candidate !== 'string' || !candidate) return false;
|
|
43
43
|
const resolved = path.resolve(candidate);
|
|
44
44
|
return path.basename(resolved) === BACKUP_DIR_NAME
|
|
@@ -49,7 +49,7 @@ function isLegacyBackupReadRoot(candidate: unknown): boolean {
|
|
|
49
49
|
* read path handed only that root can still derive the legacy one. Returns null for any path that
|
|
50
50
|
* is not shaped `<scopeRoot>/.doflow/backups` (a test fixture, a hand-passed directory) — the
|
|
51
51
|
* bridge then stays inert rather than inventing a sibling `.claude` next to an arbitrary path. */
|
|
52
|
-
function scopeRootFromCanonicalBackupRoot(backupRoot
|
|
52
|
+
function scopeRootFromCanonicalBackupRoot(backupRoot ) {
|
|
53
53
|
if (typeof backupRoot !== 'string' || !backupRoot) return null;
|
|
54
54
|
const resolved = path.resolve(backupRoot);
|
|
55
55
|
if (path.basename(resolved) !== BACKUP_DIR_NAME) return null;
|
|
@@ -59,7 +59,7 @@ function scopeRootFromCanonicalBackupRoot(backupRoot: unknown): string | null {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** Resolve DoFlow-owned lifecycle metadata without anchoring it to a harness directory. */
|
|
62
|
-
function doflowPaths({ scopeRoot }
|
|
62
|
+
function doflowPaths({ scopeRoot } ) {
|
|
63
63
|
if (typeof scopeRoot !== 'string' || !scopeRoot) throw new Error('scopeRoot is required');
|
|
64
64
|
const root = path.resolve(scopeRoot);
|
|
65
65
|
const doflowRoot = path.join(root, DOFLOW_DIR_NAME);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
13
13
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
14
|
-
// caller's `require('../install/targets.
|
|
14
|
+
// caller's `require('../install/targets.js')` keeps working while the rest of the tree is still `.js`.
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
|
|
@@ -24,12 +24,12 @@ const VALID = ['claude', 'codex', 'gemini', 'copilot', 'kiro', 'opencode', 'pi',
|
|
|
24
24
|
* rest explicitly with `--target claude,codex,gemini`. */
|
|
25
25
|
const DEFAULT_TARGETS = ['claude'];
|
|
26
26
|
|
|
27
|
-
const TARGET_ALIASES
|
|
27
|
+
const TARGET_ALIASES = {
|
|
28
28
|
agy: 'antigravity',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/** Default to DEFAULT_TARGETS; validate any explicitly requested. */
|
|
32
|
-
function resolveTargets(requested
|
|
32
|
+
function resolveTargets(requested ) {
|
|
33
33
|
const rawTargets = requested && requested.length ? requested : [...DEFAULT_TARGETS];
|
|
34
34
|
const targets = rawTargets.map((t) => TARGET_ALIASES[t] || t);
|
|
35
35
|
for (const t of targets) {
|
|
@@ -40,10 +40,10 @@ function resolveTargets(requested: string[] | undefined): string[] {
|
|
|
40
40
|
return [...new Set(targets)];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Map tool -> install root.
|
|
@@ -66,14 +66,14 @@ interface ToolDirsOptions {
|
|
|
66
66
|
* reads (steering, agents, hooks, settings/mcp.json), so — unlike copilot — this is
|
|
67
67
|
* the clean, symmetric case.
|
|
68
68
|
* opencode -> Global config is `~/.config/opencode`, NOT `~/.opencode/` — a path DoFlow's own
|
|
69
|
-
* docs asserted incorrectly for several releases (see src/adapters/opencode/index.
|
|
69
|
+
* docs asserted incorrectly for several releases (see src/adapters/opencode/index.js).
|
|
70
70
|
* Project scope is `.opencode`, asymmetric with the global path the same way gemini's
|
|
71
71
|
* is.
|
|
72
72
|
* pi -> Global config is `~/.pi/agent` — note the `agent/` segment; `~/.pi/settings.json`
|
|
73
73
|
* is not a location Pi reads. Project scope is `.pi` (see
|
|
74
|
-
* src/adapters/pi/index.
|
|
74
|
+
* src/adapters/pi/index.js's own `nativePaths()`, which this mirrors exactly).
|
|
75
75
|
*/
|
|
76
|
-
function toolDirs(opts
|
|
76
|
+
function toolDirs(opts = {}) {
|
|
77
77
|
const { global = false, projectRoot = '.' } = opts;
|
|
78
78
|
const root = global ? os.homedir() : path.resolve(projectRoot);
|
|
79
79
|
return {
|
|
@@ -6,30 +6,30 @@
|
|
|
6
6
|
//
|
|
7
7
|
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
8
8
|
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
9
|
-
// caller's `require('../install/tool-lifecycle.
|
|
9
|
+
// caller's `require('../install/tool-lifecycle.js')` keeps working while the tree is still `.js`.
|
|
10
10
|
const { execFileSync } = require('node:child_process');
|
|
11
11
|
|
|
12
12
|
const SUPPORTED_PLATFORMS = new Set(['darwin', 'linux']);
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
|
|
16
|
-
function isSupportedPlatform(platform
|
|
16
|
+
function isSupportedPlatform(platform = process.platform) {
|
|
17
17
|
return SUPPORTED_PLATFORMS.has(platform);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function commandText(command
|
|
20
|
+
function commandText(command ) {
|
|
21
21
|
return command.map((part) => JSON.stringify(part)).join(' ');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function failureKind(error
|
|
24
|
+
function failureKind(error ) {
|
|
25
25
|
return error?.code === 'ENOENT' ? 'absent' : 'failed';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function runInspection(command
|
|
28
|
+
function runInspection(command , execFileSyncImpl ) {
|
|
29
29
|
try {
|
|
30
30
|
const output = execFileSyncImpl(command[0], command.slice(1), { encoding: 'utf8', stdio: 'pipe' });
|
|
31
31
|
return { command: [...command], result: 'succeeded', output: output == null ? '' : String(output) };
|
|
32
|
-
} catch (error
|
|
32
|
+
} catch (error ) {
|
|
33
33
|
return {
|
|
34
34
|
command: [...command], result: failureKind(error),
|
|
35
35
|
error: error?.message || String(error),
|
|
@@ -38,26 +38,26 @@ function runInspection(command: string[], execFileSyncImpl: ExecFileSyncImpl): a
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Reduce every status command result to a safe tool state. */
|
|
41
|
-
function normalizeToolState(inspections
|
|
41
|
+
function normalizeToolState(inspections ) {
|
|
42
42
|
if (inspections.every((inspection) => inspection.result === 'succeeded')) return 'installed';
|
|
43
43
|
if (inspections.every((inspection) => inspection.result === 'absent')) return 'absent';
|
|
44
44
|
if (inspections.some((inspection) => inspection.result === 'succeeded')) return 'unknown';
|
|
45
45
|
return 'inspection-failed';
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function inspectTool(tool
|
|
49
|
-
const inspections = tool.status.commands.map((command
|
|
48
|
+
function inspectTool(tool , { execFileSyncImpl = execFileSync } = {}) {
|
|
49
|
+
const inspections = tool.status.commands.map((command ) => runInspection(command, execFileSyncImpl));
|
|
50
50
|
return { state: normalizeToolState(inspections), inspections };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function inspectPrerequisites(tool
|
|
54
|
-
return (tool.prerequisites || []).map((name
|
|
53
|
+
function inspectPrerequisites(tool , { execFileSyncImpl = execFileSync } = {}) {
|
|
54
|
+
return (tool.prerequisites || []).map((name ) => {
|
|
55
55
|
const inspection = runInspection([name, '--version'], execFileSyncImpl);
|
|
56
56
|
return { name, available: inspection.result === 'succeeded', inspection };
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function actionEligibility(tool
|
|
60
|
+
function actionEligibility(tool , state , action , prerequisites ) {
|
|
61
61
|
if (action === 'status') return { applicable: false, reason: 'status inspection only' };
|
|
62
62
|
const contract = tool[action];
|
|
63
63
|
if (!contract) return { applicable: false, reason: `no verified ${action} command is registered` };
|
|
@@ -79,12 +79,12 @@ function actionEligibility(tool: any, state: string, action: string, prerequisit
|
|
|
79
79
|
* Inspect all status command vectors, then create a non-mutating action plan.
|
|
80
80
|
* The registry can be the loaded registry or an { externalTools } fixture.
|
|
81
81
|
*/
|
|
82
|
-
function planToolLifecycle({ registry, action = 'status', platform = process.platform, execFileSyncImpl = execFileSync }
|
|
82
|
+
function planToolLifecycle({ registry, action = 'status', platform = process.platform, execFileSyncImpl = execFileSync } = {}) {
|
|
83
83
|
if (!['install', 'update', 'uninstall', 'status'].includes(action)) {
|
|
84
84
|
throw new Error(`Unsupported external-tool action '${action}'`);
|
|
85
85
|
}
|
|
86
86
|
const supported = isSupportedPlatform(platform);
|
|
87
|
-
const tools = (registry?.externalTools || []).map((tool
|
|
87
|
+
const tools = (registry?.externalTools || []).map((tool ) => {
|
|
88
88
|
if (!supported) {
|
|
89
89
|
return {
|
|
90
90
|
tool, state: 'unknown', inspections: [], prerequisites: [],
|
|
@@ -98,19 +98,19 @@ function planToolLifecycle({ registry, action = 'status', platform = process.pla
|
|
|
98
98
|
return { action, platform, supported, tools };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function defaultDisplay(command
|
|
101
|
+
function defaultDisplay(command ) { console.error(`[INFO] ${commandText(command)}`); }
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Display and separately confirm every planned mutation. A failed command never prevents later
|
|
105
105
|
* commands from being independently offered and executed.
|
|
106
106
|
*/
|
|
107
|
-
function executeToolLifecycle({ plan, execFileSyncImpl = execFileSync, displayCommand = defaultDisplay, confirmCommand }
|
|
107
|
+
function executeToolLifecycle({ plan, execFileSyncImpl = execFileSync, displayCommand = defaultDisplay, confirmCommand } = {}) {
|
|
108
108
|
if (!plan) throw new Error('executeToolLifecycle requires a plan');
|
|
109
109
|
if (typeof confirmCommand !== 'function') throw new Error('executeToolLifecycle requires confirmCommand');
|
|
110
110
|
|
|
111
111
|
const notAttempted = plan.tools
|
|
112
|
-
.filter((item
|
|
113
|
-
.map((item
|
|
112
|
+
.filter((item ) => item.action.applicable)
|
|
113
|
+
.map((item ) => ({ tool: item.tool.id, status: 'not-attempted', command: [...item.action.command] }));
|
|
114
114
|
const results = [];
|
|
115
115
|
for (const item of plan.tools) {
|
|
116
116
|
const action = item.action;
|
|
@@ -127,7 +127,7 @@ function executeToolLifecycle({ plan, execFileSyncImpl = execFileSync, displayCo
|
|
|
127
127
|
try {
|
|
128
128
|
const output = execFileSyncImpl(command[0], command.slice(1), { encoding: 'utf8', stdio: 'pipe' });
|
|
129
129
|
results.push({ tool: item.tool.id, status: 'succeeded', command: [...command], output: output == null ? '' : String(output) });
|
|
130
|
-
} catch (error
|
|
130
|
+
} catch (error ) {
|
|
131
131
|
results.push({ tool: item.tool.id, status: 'failed', command: [...command], error: error?.message || String(error) });
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -13,7 +13,7 @@ const { execFileSync } = require('node:child_process');
|
|
|
13
13
|
* unrelated binary could satisfy a mere PATH check, but only a real bash exits cleanly
|
|
14
14
|
* on `--version`. ENOENT, a non-zero exit, or any other spawn error are all treated as
|
|
15
15
|
* "not available"; the caller does not need to distinguish the reason. */
|
|
16
|
-
function hasBashCapableShell(execFileSyncImpl
|
|
16
|
+
function hasBashCapableShell(execFileSyncImpl = execFileSync) {
|
|
17
17
|
try {
|
|
18
18
|
execFileSyncImpl('bash', ['--version'], { stdio: 'ignore' });
|
|
19
19
|
return true;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
|
|
5
|
+
const POLICY_ORDER = Object.freeze(['pre-implementation-gate', 'pre-bash-guard', 'session-context', 'mcp-tool-guard', 'stop-check']);
|
|
6
|
+
const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'gemini', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
|
|
7
|
+
const NEVER_BLOCKING_EVENTS = new Set([
|
|
8
|
+
'pi\u0000tool_execution_start', 'antigravity\u0000PostToolUse', 'antigravity\u0000PreInvocation',
|
|
9
|
+
'antigravity\u0000PostInvocation', 'gemini\u0000AfterTool', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
|
|
10
|
+
]);
|
|
11
|
+
function stable(value ) {
|
|
12
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
13
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
function rowsDigest(rows ) { return `sha256:${crypto.createHash('sha256').update(stable(rows)).digest('hex')}`; }
|
|
17
|
+
class UnknownEnforcementTarget extends Error {
|
|
18
|
+
code ;
|
|
19
|
+
constructor(code , detail ) { super(detail); this.name = 'UnknownEnforcementTarget'; this.code = code; }
|
|
20
|
+
}
|
|
21
|
+
function enforcementFor(coverage , query ) {
|
|
22
|
+
const rows = coverage?.rows || [];
|
|
23
|
+
const profiles = coverage?.profiles || {};
|
|
24
|
+
if (query.policy === '*') throw new UnknownEnforcementTarget('unknown-policy', "Unknown policy '*'");
|
|
25
|
+
if (!rows.some((row ) => row.harness === query.harness)) throw new UnknownEnforcementTarget('unknown-harness', `Unknown harness '${query.harness}'`);
|
|
26
|
+
if (!rows.some((row ) => row.policy === query.policy)) throw new UnknownEnforcementTarget('unknown-policy', `Unknown policy '${query.policy}'`);
|
|
27
|
+
if (query.profile !== undefined && !Object.hasOwn(profiles, query.profile)) throw new UnknownEnforcementTarget('unknown-profile', `Unknown profile '${query.profile}'`);
|
|
28
|
+
const pairRows = rows.filter((row ) => row.harness === query.harness && (row.policy === query.policy || row.policy === '*'));
|
|
29
|
+
const timing = query.policy === 'stop-check' ? 'stop' : 'pre-tool';
|
|
30
|
+
const applicable = pairRows.filter((row ) => row.timing === timing && (query.profile === undefined || row.profile === query.profile || row.profile === 'any'));
|
|
31
|
+
const enforced = applicable.length > 0 && applicable.every((row ) => row.wired && row.class === 'blocking' && row.verification === 'verified');
|
|
32
|
+
const rank = (row ) => CLASS_ORDER.indexOf(row.class);
|
|
33
|
+
const weakest = [...applicable].sort((a , b ) => rank(a) - rank(b))[0];
|
|
34
|
+
const intersection = (field ) => applicable.length ? [...new Set (applicable[0][field] || [])].filter((item) => applicable.every((row ) => (row[field] || []).includes(item))) : [];
|
|
35
|
+
const union = (field ) => [...new Set(applicable.flatMap((row ) => row[field] || []))];
|
|
36
|
+
const parity = enforced && pairRows.length > 0 && pairRows.every((row ) => row.class === 'blocking');
|
|
37
|
+
return {
|
|
38
|
+
harness: query.harness, policy: query.policy, profile: query.profile ?? null, enforced, parity,
|
|
39
|
+
class: weakest?.class || 'unknown', verification: weakest?.verification || 'unverified',
|
|
40
|
+
prerequisites: union('prerequisites'), covers: intersection('covers'), doesNotCover: union('doesNotCover'), caveats: union('caveats'),
|
|
41
|
+
rows: applicable, matrix: { schemaVersion: coverage.schemaVersion, revision: coverage.revision, rowsDigest: coverage.rowsDigest },
|
|
42
|
+
reason: weakest ? `${weakest.class} at (${weakest.policy}, ${weakest.harness}, ${weakest.event}, ${weakest.profile})` : `unknown at (${query.policy}, ${query.harness}, *, ${query.profile ?? '*'})`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function renderEnforcementMatrix(coverage ) {
|
|
46
|
+
const order = (items , value ) => {
|
|
47
|
+
const index = items.indexOf(value);
|
|
48
|
+
return index < 0 ? items.length : index;
|
|
49
|
+
};
|
|
50
|
+
const rows = [...coverage.rows].sort((a , b ) => order(POLICY_ORDER, a.policy) - order(POLICY_ORDER, b.policy)
|
|
51
|
+
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
52
|
+
|| a.event.localeCompare(b.event) || a.profile.localeCompare(b.profile));
|
|
53
|
+
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}; ${coverage.rowsDigest}`, '| Policy | Harness | Native event | Profile | Class | Verification | Prerequisites | Does not cover |', '|---|---|---|---|---|---|---|---|'];
|
|
54
|
+
for (const row of rows) lines.push(`| ${row.policy} | ${row.harness} | ${row.event} | ${row.profile} | ${row.class} | ${row.verification} | ${(row.prerequisites || []).join(', ')} | ${(row.doesNotCover || []).join(', ')} |`);
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
module.exports = { enforcementFor, renderEnforcementMatrix, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|