@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doflow.js +2 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/registry/storage.json +87 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +60 -60
- package/src/adapters/claude/{index.ts → index.js} +48 -48
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +23 -23
- package/src/adapters/codex/{index.ts → index.js} +98 -98
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +70 -70
- package/src/adapters/gemini/{hooks.ts → hooks.js} +24 -24
- package/src/adapters/gemini/{index.ts → index.js} +41 -41
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +34 -34
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/{index.ts → index.js} +30 -30
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +24 -24
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +19 -19
- package/src/cli/{index.ts → index.js} +26 -26
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +48 -48
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/{index.ts → index.js} +140 -140
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +60 -60
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +30 -30
- package/src/runtime/{cli-result.ts → cli-result.js} +4 -4
- package/src/runtime/{cli.ts → cli.js} +37 -37
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/{context-pack.ts → context-pack.js} +20 -20
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +25 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +15 -15
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +35 -35
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +40 -40
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +19 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +61 -61
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +13 -13
- package/src/runtime/{task-state.ts → task-state.js} +15 -15
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Only SELECTED servers are imported, so the cost is proportional to what is installed: a
|
|
17
17
|
// one-server install pays for one doc, and an install with none produces no file at all.
|
|
18
|
-
function renderMcpIndex(selectedServers
|
|
18
|
+
function renderMcpIndex(selectedServers ) {
|
|
19
19
|
if (!selectedServers || selectedServers.length === 0) return null;
|
|
20
20
|
const lines = ['# MCP short flags — this install'];
|
|
21
21
|
for (const server of selectedServers) {
|
|
@@ -5,30 +5,30 @@
|
|
|
5
5
|
// Registry lifecycle policies are logical intent. This projection reports what a
|
|
6
6
|
// harness may render and any human prerequisite; native event serialization stays
|
|
7
7
|
// in the owning harness adapter.
|
|
8
|
-
const { harnessFor } = require('../registry/index.
|
|
8
|
+
const { harnessFor } = require('../registry/index.js');
|
|
9
9
|
|
|
10
10
|
const POLICY_STATUSES = new Set(['supported', 'different', 'unavailable', 'prerequisite']);
|
|
11
11
|
|
|
12
|
-
function unique(values
|
|
12
|
+
function unique(values ) { return [...new Set(values.filter(Boolean))]; }
|
|
13
13
|
|
|
14
|
-
function policyFor(registry
|
|
15
|
-
const policy = registry?.lifecycle?.find((item
|
|
14
|
+
function policyFor(registry , id ) {
|
|
15
|
+
const policy = registry?.lifecycle?.find((item ) => item.id === id);
|
|
16
16
|
if (!policy) throw new Error(`Unknown lifecycle policy '${id}'`);
|
|
17
17
|
return policy;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function renderPolicy(policy
|
|
20
|
+
function renderPolicy(policy , harness ) {
|
|
21
21
|
const mapping = policy?.mappings?.[harness.id];
|
|
22
22
|
if (!mapping) throw new Error(`Lifecycle policy '${policy.id}' has no mapping for '${harness.id}'`);
|
|
23
|
-
const requiredCapabilities = (policy.requires || []).map((capability
|
|
23
|
+
const requiredCapabilities = (policy.requires || []).map((capability ) => {
|
|
24
24
|
const declaration = harness.capabilities?.[capability];
|
|
25
25
|
if (!declaration) throw new Error(`Lifecycle policy '${policy.id}' requires unknown '${harness.id}' capability '${capability}'`);
|
|
26
26
|
return [capability, declaration];
|
|
27
27
|
});
|
|
28
|
-
const unavailableCapability = requiredCapabilities.find(([, declaration]
|
|
28
|
+
const unavailableCapability = requiredCapabilities.find(([, declaration] ) => declaration.status === 'unavailable');
|
|
29
29
|
const prerequisites = unique([
|
|
30
30
|
...(mapping.prerequisites || []),
|
|
31
|
-
...requiredCapabilities.flatMap(([, declaration]
|
|
31
|
+
...requiredCapabilities.flatMap(([, declaration] ) => declaration.prerequisites || []),
|
|
32
32
|
]);
|
|
33
33
|
const capabilityStatus = unavailableCapability ? 'unavailable' : mapping.status;
|
|
34
34
|
const unavailable = capabilityStatus === 'unavailable';
|
|
@@ -60,14 +60,14 @@ function renderPolicy(policy: any, harness: any): any {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Render policy status data for one harness without calling an adapter or writing state. */
|
|
63
|
-
function renderPolicies(registry
|
|
63
|
+
function renderPolicies(registry , { harness: harnessId, policyIds } = {}) {
|
|
64
64
|
if (!harnessId) throw new Error('A harness id is required to render lifecycle policies');
|
|
65
65
|
const harness = harnessFor(registry, harnessId);
|
|
66
66
|
const policies = policyIds === undefined || policyIds === null
|
|
67
67
|
? registry.lifecycle
|
|
68
|
-
: policyIds.map((id
|
|
68
|
+
: policyIds.map((id ) => policyFor(registry, id));
|
|
69
69
|
if (!Array.isArray(policies)) throw new Error('Lifecycle policies must be an array');
|
|
70
|
-
return Object.freeze(policies.map((policy
|
|
70
|
+
return Object.freeze(policies.map((policy ) => renderPolicy(policy, harness)));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
module.exports = { POLICY_STATUSES, policyFor, renderPolicy, renderPolicies };
|
|
@@ -8,21 +8,21 @@
|
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
11
|
-
const { parseToml } = require('../helper/toml.
|
|
12
|
-
const { createAdapterRegistry } = require('../adapters/index.
|
|
13
|
-
const { declaredHarnessPaths } = require('../helper/harness-paths.
|
|
14
|
-
const claudeAdapter = require('../adapters/claude/index.
|
|
15
|
-
const codexAdapter = require('../adapters/codex/index.
|
|
16
|
-
const { createGeminiAdapter } = require('../adapters/gemini/index.
|
|
17
|
-
const { createOpenCodeAdapter } = require('../adapters/opencode/index.
|
|
18
|
-
const { createPiAdapter } = require('../adapters/pi/index.
|
|
19
|
-
const { createCopilotAdapter } = require('../adapters/copilot/index.
|
|
20
|
-
const { createKiroAdapter } = require('../adapters/kiro/index.
|
|
21
|
-
const { createAntigravityAdapter } = require('../adapters/antigravity/index.
|
|
22
|
-
const { planLifecycle } = require('./index.
|
|
23
|
-
const { stateRoot, readLedger, defaultLedger } = require('../state/index.
|
|
24
|
-
const { selectAssets } = require('../registry/index.
|
|
25
|
-
const { defaultLock, readLock, writeLock, diffLocks, removeLock, pinnedSelections } = require('../state/lockfile.
|
|
11
|
+
const { parseToml } = require('../helper/toml.js');
|
|
12
|
+
const { createAdapterRegistry } = require('../adapters/index.js');
|
|
13
|
+
const { declaredHarnessPaths } = require('../helper/harness-paths.js');
|
|
14
|
+
const claudeAdapter = require('../adapters/claude/index.js');
|
|
15
|
+
const codexAdapter = require('../adapters/codex/index.js');
|
|
16
|
+
const { createGeminiAdapter } = require('../adapters/gemini/index.js');
|
|
17
|
+
const { createOpenCodeAdapter } = require('../adapters/opencode/index.js');
|
|
18
|
+
const { createPiAdapter } = require('../adapters/pi/index.js');
|
|
19
|
+
const { createCopilotAdapter } = require('../adapters/copilot/index.js');
|
|
20
|
+
const { createKiroAdapter } = require('../adapters/kiro/index.js');
|
|
21
|
+
const { createAntigravityAdapter } = require('../adapters/antigravity/index.js');
|
|
22
|
+
const { planLifecycle } = require('./index.js');
|
|
23
|
+
const { stateRoot, readLedger, defaultLedger } = require('../state/index.js');
|
|
24
|
+
const { selectAssets } = require('../registry/index.js');
|
|
25
|
+
const { defaultLock, readLock, writeLock, diffLocks, removeLock, pinnedSelections } = require('../state/lockfile.js');
|
|
26
26
|
// Tolerant because the projected runtime under `.doflow/runtime/` ships bin/, src/ and
|
|
27
27
|
// core/registry/ but no package.json — see the `runtime.*` assets in core/registry/assets.json.
|
|
28
28
|
// A hard require here would make every Node-backed verb fail in an install, which is the exact
|
|
@@ -32,9 +32,9 @@ function loadPkg() {
|
|
|
32
32
|
}
|
|
33
33
|
const pkg = loadPkg();
|
|
34
34
|
|
|
35
|
-
function codexScope(scope
|
|
35
|
+
function codexScope(scope ) { return scope.global ? 'global' : 'project'; }
|
|
36
36
|
|
|
37
|
-
function codexConfigResources(repoRoot
|
|
37
|
+
function codexConfigResources(repoRoot , fsImpl ) {
|
|
38
38
|
const configSrc = path.join(repoRoot, 'core', 'harnesses', 'codex', 'config', 'config.toml');
|
|
39
39
|
const parsed = parseToml(fsImpl.readFileSync(configSrc, 'utf8'));
|
|
40
40
|
return [...parsed.entries.entries()].map(([identity, entry]) => ({
|
|
@@ -48,7 +48,7 @@ function codexConfigResources(repoRoot: any, fsImpl: any) {
|
|
|
48
48
|
* mutation path until every native adapter has CLI-level parity.
|
|
49
49
|
* `registry` is loaded once per command (by the caller) and threaded through here rather than
|
|
50
50
|
* reloaded — the same registry also resolves every harness's MCP selection for that command. */
|
|
51
|
-
function registryLifecycleView({ registry, scope, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], operation, repoRoot, force = false, adopt = false, permissions = false, statusline = false, fsImpl = fs }
|
|
51
|
+
function registryLifecycleView({ registry, scope, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], operation, repoRoot, force = false, adopt = false, permissions = false, statusline = false, fsImpl = fs } ) {
|
|
52
52
|
const lifecycleScope = codexScope(scope);
|
|
53
53
|
const scopeRoot = scope.global ? os.homedir() : path.resolve(scope.projectRoot);
|
|
54
54
|
const neutralStateRoot = stateRoot({ scope: lifecycleScope, projectRoot: scopeRoot, homeDir: scopeRoot });
|
|
@@ -79,11 +79,11 @@ function registryLifecycleView({ registry, scope, targets, mcpSelections = {}, m
|
|
|
79
79
|
return { registry, stateRoot: neutralStateRoot, ledger, plan, adapters };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function printRegistryLifecycle(view
|
|
82
|
+
function printRegistryLifecycle(view , prefix = '[PLAN]') {
|
|
83
83
|
console.log(`${prefix} Registry lifecycle: ${view.plan.changes.length} native change(s), ${view.plan.conflicts.length} conflict(s), ${view.plan.prerequisites.length} prerequisite(s)`);
|
|
84
84
|
for (const target of view.plan.targets) {
|
|
85
85
|
console.log(`${prefix} ${target.harness}: ${target.changes.length} change(s)${target.conflicts.length ? `; conflicts: ${target.conflicts.join('; ')}` : ''}`);
|
|
86
|
-
const hookChange = target.changes.find((change
|
|
86
|
+
const hookChange = target.changes.find((change ) => change.nativeComponent === 'hooks');
|
|
87
87
|
if (hookChange?.nativePlan?.trust?.required) {
|
|
88
88
|
console.log(`${prefix} ${target.harness} hooks trust: ${hookChange.nativePlan.trust.status} (review required in ${target.harness})`);
|
|
89
89
|
}
|
|
@@ -94,7 +94,7 @@ function printRegistryLifecycle(view: any, prefix = '[PLAN]') {
|
|
|
94
94
|
|
|
95
95
|
/** Prints the plan's notices after a real run. Independent of whether anything changed: a no-op
|
|
96
96
|
* reinstall of a harness that has a gap must still say so. */
|
|
97
|
-
function printPlanNotices(view
|
|
97
|
+
function printPlanNotices(view ) {
|
|
98
98
|
for (const { harness, notice } of view.plan.notices) console.log(`[INFO] ${harness}: ${notice}`);
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -103,7 +103,7 @@ function printPlanNotices(view: any) {
|
|
|
103
103
|
* src/targets.js — so a future non-lifecycle target doesn't silently gain lifecycle behavior. */
|
|
104
104
|
const LIFECYCLE_HARNESSES = ['claude', 'codex', 'gemini', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity'];
|
|
105
105
|
|
|
106
|
-
function assertSafeRegistryPlan(view
|
|
106
|
+
function assertSafeRegistryPlan(view ) {
|
|
107
107
|
if (view.plan.safe) return;
|
|
108
108
|
for (const conflict of view.plan.conflicts) console.error(`[ERROR] ${conflict.harness} lifecycle refused: ${conflict.reason}`);
|
|
109
109
|
for (const prerequisite of view.plan.prerequisites) console.error(`[ERROR] ${prerequisite.harness} lifecycle prerequisite: ${prerequisite.prerequisite}`);
|
|
@@ -119,9 +119,9 @@ function assertSafeRegistryPlan(view: any) {
|
|
|
119
119
|
* selections carry over for pinned harnesses and are replaced by the rows this run resolved, `[]`
|
|
120
120
|
* included. Assets are recomputed from the registry for every pinned harness. `null` when no harness
|
|
121
121
|
* stays pinned. */
|
|
122
|
-
function lockDocument({ registry, scope, scopeRoot, previous = null, ledger, plannedTargets = [], mcpSelections = {}, sourceVersion = pkg.version, now = new Date() }
|
|
123
|
-
const held = new Set((ledger?.resources ?? []).map((resource
|
|
124
|
-
const pinned = [...new Set([...(previous?.targets ?? []).map((entry
|
|
122
|
+
function lockDocument({ registry, scope, scopeRoot, previous = null, ledger, plannedTargets = [], mcpSelections = {}, sourceVersion = pkg.version, now = new Date() } ) {
|
|
123
|
+
const held = new Set((ledger?.resources ?? []).map((resource ) => resource?.harness));
|
|
124
|
+
const pinned = [...new Set([...(previous?.targets ?? []).map((entry ) => entry.harness), ...plannedTargets])]
|
|
125
125
|
.filter((harness) => held.has(harness)).sort();
|
|
126
126
|
if (!pinned.length) return null;
|
|
127
127
|
const assets = [];
|
|
@@ -145,13 +145,13 @@ function lockDocument({ registry, scope, scopeRoot, previous = null, ledger, pla
|
|
|
145
145
|
/** Describe the reviewable delta between the pinned lock and the next one: "created" for a first
|
|
146
146
|
* pin, "unchanged" when nothing pinned differs, "cleared" when nothing remains pinned, else a count
|
|
147
147
|
* of the changed rows. */
|
|
148
|
-
function lockDelta(previous
|
|
148
|
+
function lockDelta(previous , next ) {
|
|
149
149
|
if (!next) return previous ? { changed: true, summary: 'cleared' } : { changed: false, summary: 'unchanged' };
|
|
150
150
|
if (!previous) return { changed: true, summary: 'created' };
|
|
151
151
|
const diff = diffLocks(previous, next);
|
|
152
152
|
if (diff.clean) return { changed: false, summary: 'unchanged' };
|
|
153
153
|
const count = ['targets', 'assets']
|
|
154
|
-
.flatMap((section) => Object.values
|
|
154
|
+
.flatMap((section) => Object.values (diff[section]).map((list ) => list.length))
|
|
155
155
|
.reduce((sum, n) => sum + n, 0)
|
|
156
156
|
+ diff.mcpSelections.changed.length + (diff.meta.sourceVersion ? 1 : 0);
|
|
157
157
|
return { changed: true, summary: `${count} change(s)` };
|
|
@@ -160,7 +160,7 @@ function lockDelta(previous: any, next: any) {
|
|
|
160
160
|
/** Persist the lock for a completed command and return its delta against what was pinned before.
|
|
161
161
|
* An unchanged lock is left byte for byte, so routine re-runs neither rewrite the file nor
|
|
162
162
|
* manufacture noise; a `null` lock removes the file. */
|
|
163
|
-
function recordLock(scopeArgs
|
|
163
|
+
function recordLock(scopeArgs , next , { fsImpl = fs } = {}) {
|
|
164
164
|
const delta = lockDelta(readLock(scopeArgs, { fsImpl }), next);
|
|
165
165
|
if (delta.changed) {
|
|
166
166
|
if (next) writeLock(scopeArgs, next, { fsImpl });
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Registry loader and validator. Registry files are plain JSON (core/registry/*.json).
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
-
const { validatePathsSection } = require('../helper/harness-paths.
|
|
8
|
+
const { validatePathsSection } = require('../helper/harness-paths.js');
|
|
9
9
|
|
|
10
10
|
const REGISTRY_FILES = Object.freeze({
|
|
11
11
|
harnesses: 'harnesses.json',
|
|
@@ -42,9 +42,9 @@ const MCP_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
|
42
42
|
* artifact that can lag the source (`index`). Required on every provider (068). */
|
|
43
43
|
const PROVIDER_SOURCE_MODES = Object.freeze(['live', 'index']);
|
|
44
44
|
|
|
45
|
-
function issue(errors
|
|
46
|
-
function object(value
|
|
47
|
-
function idsUnique(items
|
|
45
|
+
function issue(errors , location , message ) { errors.push(`${location}: ${message}`); }
|
|
46
|
+
function object(value ) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
47
|
+
function idsUnique(items , label , errors ) {
|
|
48
48
|
const seen = new Set();
|
|
49
49
|
for (const item of items || []) {
|
|
50
50
|
if (!object(item) || typeof item.id !== 'string' || !item.id) continue;
|
|
@@ -53,22 +53,22 @@ function idsUnique(items: any, label: string, errors: string[]): void {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function parseRegistryFile(file
|
|
56
|
+
function parseRegistryFile(file , fsImpl = fs) {
|
|
57
57
|
let text;
|
|
58
|
-
try { text = fsImpl.readFileSync(file, 'utf8'); } catch (error
|
|
58
|
+
try { text = fsImpl.readFileSync(file, 'utf8'); } catch (error ) {
|
|
59
59
|
throw new Error(`Could not read registry file '${file}': ${error.message}`);
|
|
60
60
|
}
|
|
61
|
-
try { return JSON.parse(text); } catch (error
|
|
61
|
+
try { return JSON.parse(text); } catch (error ) {
|
|
62
62
|
throw new Error(`Registry file '${file}' is not valid JSON: ${error.message}`);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function registryDir(repoRoot
|
|
66
|
+
function registryDir(repoRoot ) { return path.join(path.resolve(repoRoot), 'core', 'registry'); }
|
|
67
67
|
|
|
68
|
-
function loadRegistry({ repoRoot, dir, fsImpl = fs }
|
|
68
|
+
function loadRegistry({ repoRoot, dir, fsImpl = fs } = {}) {
|
|
69
69
|
if (!repoRoot && !dir) throw new Error('loadRegistry requires repoRoot or dir');
|
|
70
|
-
const base = dir ? path.resolve(dir) : registryDir(repoRoot
|
|
71
|
-
const loaded
|
|
70
|
+
const base = dir ? path.resolve(dir) : registryDir(repoRoot );
|
|
71
|
+
const loaded = {};
|
|
72
72
|
for (const [name, fileName] of Object.entries(REGISTRY_FILES)) {
|
|
73
73
|
loaded[name] = parseRegistryFile(path.join(base, fileName), fsImpl);
|
|
74
74
|
}
|
|
@@ -90,11 +90,11 @@ function loadRegistry({ repoRoot, dir, fsImpl = fs }: { repoRoot?: string, dir?:
|
|
|
90
90
|
retrievalSlots: Array.isArray(loaded.models.slots) ? loaded.models.slots : [],
|
|
91
91
|
dispatchTiers: loaded.models.dispatchTiers,
|
|
92
92
|
capabilities,
|
|
93
|
-
versions: Object.fromEntries((Object.entries(loaded)
|
|
93
|
+
versions: Object.fromEntries((Object.entries(loaded) ).map(([name, value]) => [name, value.version])),
|
|
94
94
|
};
|
|
95
95
|
const validation = validateRegistry(registry, { repoRoot: registry.repoRoot, fsImpl });
|
|
96
96
|
if (!validation.ok) {
|
|
97
|
-
const error
|
|
97
|
+
const error = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
|
|
98
98
|
error.validation = validation;
|
|
99
99
|
throw error;
|
|
100
100
|
}
|
|
@@ -109,7 +109,7 @@ const CONTRACT_COMPLETENESS = new Set(['verified', 'lower-bound']);
|
|
|
109
109
|
*
|
|
110
110
|
* `evidence` is required for the same reason capabilities require it: a contract claim with no
|
|
111
111
|
* citation is folklore, and these lists are copied from vendor documentation that moves. */
|
|
112
|
-
function validateContract(value
|
|
112
|
+
function validateContract(value , location , errors , harnessIds ) {
|
|
113
113
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
114
114
|
if (typeof value.harness !== 'string' || !harnessIds.has(value.harness)) {
|
|
115
115
|
issue(errors, location, `harness must be one of: ${[...harnessIds].join(', ')}`);
|
|
@@ -147,28 +147,28 @@ function validateContract(value: any, location: string, errors: string[], harnes
|
|
|
147
147
|
issue(errors, `${location}.hookEvents`, 'must not contain duplicates');
|
|
148
148
|
}
|
|
149
149
|
if (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
150
|
-
|| value.evidence.some((url
|
|
150
|
+
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
151
151
|
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/** A harness MCP tool-name template: contains {server} and {tool}, and no other brace text. */
|
|
156
|
-
function validToolNameTemplate(value
|
|
156
|
+
function validToolNameTemplate(value ) {
|
|
157
157
|
return typeof value === 'string' && value.includes('{server}') && value.includes('{tool}')
|
|
158
158
|
&& !value.replace(/\{server\}|\{tool\}/g, '').match(/[{}]/);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function validateCapability(value
|
|
161
|
+
function validateCapability(value , location , errors ) {
|
|
162
162
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
163
163
|
if (!CAPABILITY_STATUS.has(value.status)) issue(errors, location, 'status must be supported, different, or unavailable');
|
|
164
|
-
if (!Array.isArray(value.evidence) || value.evidence.length === 0 || value.evidence.some((url
|
|
164
|
+
if (!Array.isArray(value.evidence) || value.evidence.length === 0 || value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
165
165
|
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
166
166
|
}
|
|
167
167
|
if (typeof value.verification !== 'string' || !value.verification.trim()) issue(errors, location, 'requires a verification instruction');
|
|
168
168
|
if (value.toolName !== undefined && !validToolNameTemplate(value.toolName)) {
|
|
169
169
|
issue(errors, location, 'toolName must be a string with exactly the placeholders {server} and {tool} and no other braces');
|
|
170
170
|
}
|
|
171
|
-
if (value.prerequisites !== undefined && (!Array.isArray(value.prerequisites) || value.prerequisites.some((item
|
|
171
|
+
if (value.prerequisites !== undefined && (!Array.isArray(value.prerequisites) || value.prerequisites.some((item ) => typeof item !== 'string' || !item))) {
|
|
172
172
|
issue(errors, location, 'prerequisites must be an array of non-empty strings');
|
|
173
173
|
}
|
|
174
174
|
// Optional per-event detail, reusing the same three-value status vocabulary. Additive on
|
|
@@ -178,7 +178,7 @@ function validateCapability(value: any, location: string, errors: string[]): voi
|
|
|
178
178
|
if (!object(value.events) || Object.keys(value.events).length === 0) {
|
|
179
179
|
issue(errors, `${location}.events`, 'must be a non-empty object when present');
|
|
180
180
|
} else {
|
|
181
|
-
for (const [event, detail] of Object.entries(value.events)
|
|
181
|
+
for (const [event, detail] of Object.entries(value.events) ) {
|
|
182
182
|
if (!/^[A-Z][A-Za-z]*$/.test(event)) issue(errors, `${location}.events`, `invalid event name '${event}'`);
|
|
183
183
|
if (!object(detail) || !CAPABILITY_STATUS.has(detail.status)) {
|
|
184
184
|
issue(errors, `${location}.events.${event}`, 'status must be supported, different, or unavailable');
|
|
@@ -191,7 +191,7 @@ function validateCapability(value: any, location: string, errors: string[]): voi
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
function validateSource(repoRoot
|
|
194
|
+
function validateSource(repoRoot , source , location , errors , fsImpl , expected = 'any') {
|
|
195
195
|
if (typeof source !== 'string' || !source.trim()) { issue(errors, location, 'requires a repository-relative source'); return; }
|
|
196
196
|
const root = path.resolve(repoRoot);
|
|
197
197
|
const resolved = path.resolve(root, source);
|
|
@@ -200,7 +200,7 @@ function validateSource(repoRoot: any, source: any, location: string, errors: st
|
|
|
200
200
|
if (expected === 'directory' && !fsImpl.statSync(resolved).isDirectory()) issue(errors, location, `source must be a directory: ${source}`);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
function validateNativeProjection(projection
|
|
203
|
+
function validateNativeProjection(projection , harness , { repoRoot, fsImpl } , errors ) {
|
|
204
204
|
const at = `harness '${harness.id}' nativeProjection`;
|
|
205
205
|
if (!object(projection)) { issue(errors, at, 'must be an object'); return; }
|
|
206
206
|
const config = projection.config;
|
|
@@ -228,14 +228,14 @@ function validateNativeProjection(projection: any, harness: any, { repoRoot, fsI
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
function validateCommand(command
|
|
231
|
+
function validateCommand(command , location , errors ) {
|
|
232
232
|
if (!Array.isArray(command) || command.length === 0
|
|
233
233
|
|| command.some((argument) => typeof argument !== 'string' || !argument.trim())) {
|
|
234
234
|
issue(errors, location, 'command must be a non-empty array of non-empty strings');
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
function validateExternalTool(value
|
|
238
|
+
function validateExternalTool(value , location , errors ) {
|
|
239
239
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
240
240
|
const allowed = new Set(['id', 'displayName', 'officialEvidence', 'status', 'install', 'update', 'uninstall', 'prerequisites']);
|
|
241
241
|
for (const key of Object.keys(value)) {
|
|
@@ -246,11 +246,11 @@ function validateExternalTool(value: any, location: string, errors: string[]): v
|
|
|
246
246
|
}
|
|
247
247
|
if (typeof value.displayName !== 'string' || !value.displayName.trim()) issue(errors, location, 'requires displayName');
|
|
248
248
|
if (!Array.isArray(value.officialEvidence) || value.officialEvidence.length === 0
|
|
249
|
-
|| value.officialEvidence.some((url
|
|
249
|
+
|| value.officialEvidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
250
250
|
issue(errors, location, 'must include one or more HTTPS officialEvidence URLs');
|
|
251
251
|
}
|
|
252
252
|
if (value.prerequisites !== undefined && (!Array.isArray(value.prerequisites)
|
|
253
|
-
|| value.prerequisites.some((item
|
|
253
|
+
|| value.prerequisites.some((item ) => typeof item !== 'string' || !item.trim())
|
|
254
254
|
|| new Set(value.prerequisites).size !== value.prerequisites.length)) {
|
|
255
255
|
issue(errors, location, 'prerequisites must be an array of unique non-empty strings when present');
|
|
256
256
|
}
|
|
@@ -261,7 +261,7 @@ function validateExternalTool(value: any, location: string, errors: string[]): v
|
|
|
261
261
|
for (const key of Object.keys(value.status)) {
|
|
262
262
|
if (!allowedStatus.has(key)) issue(errors, `${location}.status`, `unsupported field '${key}'`);
|
|
263
263
|
}
|
|
264
|
-
value.status.commands.forEach((command
|
|
264
|
+
value.status.commands.forEach((command , index ) => validateCommand(command, `${location}.status.commands[${index}]`, errors));
|
|
265
265
|
}
|
|
266
266
|
for (const action of EXTERNAL_TOOL_ACTIONS) {
|
|
267
267
|
if (value[action] === undefined) continue;
|
|
@@ -277,18 +277,18 @@ function validateExternalTool(value: any, location: string, errors: string[]): v
|
|
|
277
277
|
* router will consume. Shell scope: providers are declared with identity, kind, and
|
|
278
278
|
* evidence (a capability claim without a citation is folklore); roles name routing preferences
|
|
279
279
|
* without binding to concrete model IDs, which stay runtime/user choices resolved at run time. */
|
|
280
|
-
function validateModelProvider(value
|
|
280
|
+
function validateModelProvider(value , location , errors ) {
|
|
281
281
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
282
282
|
if (typeof value.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.id)) issue(errors, location, 'id must be a lowercase identifier');
|
|
283
283
|
if (typeof value.displayName !== 'string' || !value.displayName.trim()) issue(errors, location, 'requires displayName');
|
|
284
284
|
if (!MODEL_KINDS.has(value.kind)) issue(errors, location, `kind must be one of: ${[...MODEL_KINDS].join(', ')}`);
|
|
285
285
|
if (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
286
|
-
|| value.evidence.some((url
|
|
286
|
+
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
287
287
|
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
function validateModelRole(value
|
|
291
|
+
function validateModelRole(value , location , errors ) {
|
|
292
292
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
293
293
|
if (typeof value.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.id)) issue(errors, location, 'id must be a lowercase identifier');
|
|
294
294
|
for (const field of ['prefer', 'fallback', 'require']) {
|
|
@@ -304,7 +304,7 @@ function validateModelRole(value: any, location: string, errors: string[]): void
|
|
|
304
304
|
* must fail the load loudly, because a typo reading as "no dense provider" would silently skip
|
|
305
305
|
* the feature instead of surfacing it. `enabled` is therefore required rather than defaulted:
|
|
306
306
|
* a misspelled flag defaulting to off is the same PASS-over-no-evidence defect in miniature. */
|
|
307
|
-
function validateModelSlot(value
|
|
307
|
+
function validateModelSlot(value , location , errors , providerIds ) {
|
|
308
308
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
309
309
|
const allowed = new Set(['id', 'provider', 'model', 'enabled', 'evidence']);
|
|
310
310
|
for (const key of Object.keys(value)) {
|
|
@@ -316,20 +316,20 @@ function validateModelSlot(value: any, location: string, errors: string[], provi
|
|
|
316
316
|
if (typeof value.model !== 'string' || !value.model.trim()) issue(errors, location, 'requires model');
|
|
317
317
|
if (typeof value.enabled !== 'boolean') issue(errors, location, 'enabled must be a boolean');
|
|
318
318
|
if (value.evidence !== undefined && (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
319
|
-
|| value.evidence.some((url
|
|
319
|
+
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url)))) {
|
|
320
320
|
issue(errors, location, 'must include one or more HTTPS evidence URLs when evidence is present');
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
/** `trigger` and `steps` on an MCP server (062): when a stage should reach for the server, and
|
|
325
325
|
* which skill files name it. Registry-only; adapters pick fields and never render these keys. */
|
|
326
|
-
function validateMcpStageUse(server
|
|
326
|
+
function validateMcpStageUse(server , at , errors ) {
|
|
327
327
|
if (typeof server.trigger !== 'string' || server.trigger.length < 20 || server.trigger.length > 300 || /[\r\n]/.test(server.trigger)) {
|
|
328
328
|
issue(errors, at, 'trigger must be one line of 20 to 300 characters');
|
|
329
329
|
}
|
|
330
330
|
if (!Array.isArray(server.steps)) { issue(errors, at, 'steps must be an array'); return; }
|
|
331
331
|
const seen = new Set();
|
|
332
|
-
server.steps.forEach((step
|
|
332
|
+
server.steps.forEach((step , index ) => {
|
|
333
333
|
const where = `steps[${index}]`;
|
|
334
334
|
if (!object(step)) { issue(errors, at, `${where} must be an object`); return; }
|
|
335
335
|
for (const key of Object.keys(step)) if (!MCP_STEP_KEYS.has(key)) issue(errors, at, `${where} has unknown key '${key}'`);
|
|
@@ -345,10 +345,10 @@ function validateMcpStageUse(server: any, at: string, errors: string[]): void {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/** Provider checks for capabilities.json: required `sourceMode` (068) and optional `mcp: {server, tool}` (062). */
|
|
348
|
-
function validateCapabilityProviders(capabilities
|
|
348
|
+
function validateCapabilityProviders(capabilities , errors ) {
|
|
349
349
|
if (capabilities === undefined) return;
|
|
350
350
|
if (!object(capabilities)) { issue(errors, 'capabilities', 'must be an object'); return; }
|
|
351
|
-
for (const [capabilityId, capability] of Object.entries(capabilities)
|
|
351
|
+
for (const [capabilityId, capability] of Object.entries(capabilities) ) {
|
|
352
352
|
for (const provider of object(capability) && Array.isArray(capability.providers) ? capability.providers : []) {
|
|
353
353
|
if (!object(provider)) continue;
|
|
354
354
|
const at = `capability '${capabilityId}' provider '${provider.id ?? '?'}'`;
|
|
@@ -365,11 +365,11 @@ function validateCapabilityProviders(capabilities: any, errors: string[]): void
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
/** `dispatchTiers` in models.json (062): the tier vocabulary and the per-owner default a brief names. */
|
|
368
|
-
function validateDispatchTiers(value
|
|
368
|
+
function validateDispatchTiers(value , errors ) {
|
|
369
369
|
if (value === undefined) return;
|
|
370
370
|
const at = 'models.dispatchTiers';
|
|
371
371
|
if (!object(value)) { issue(errors, at, 'must be an object'); return; }
|
|
372
|
-
const tiers = Array.isArray(value.tiers) && value.tiers.length > 0 && value.tiers.every((tier
|
|
372
|
+
const tiers = Array.isArray(value.tiers) && value.tiers.length > 0 && value.tiers.every((tier ) => typeof tier === 'string' && tier) ? value.tiers : null;
|
|
373
373
|
if (!tiers) { issue(errors, at, 'tiers must be a non-empty array of strings'); return; }
|
|
374
374
|
if (new Set(tiers).size !== tiers.length) issue(errors, at, 'tiers must be unique');
|
|
375
375
|
if (!tiers.includes(value.default)) issue(errors, at, `default must be one of: ${tiers.join(', ')}`);
|
|
@@ -382,7 +382,7 @@ function validateDispatchTiers(value: any, errors: string[]): void {
|
|
|
382
382
|
/** Validates one harness's `orchestration` block (074 IC-026): the five subagent primitives a
|
|
383
383
|
* harness exposes (spawn, wait, status, cancel, resume), each with evidence and a re-check
|
|
384
384
|
* instruction. It reads no other harness and no model name; a value is declared, never inferred. */
|
|
385
|
-
function validateOrchestration(value
|
|
385
|
+
function validateOrchestration(value , location , errors ) {
|
|
386
386
|
if (!object(value)) { issue(errors, location, 'orchestration block is required and must be an object'); return; }
|
|
387
387
|
for (const op of ORCHESTRATION_OPS) {
|
|
388
388
|
if (!Object.hasOwn(value, op)) issue(errors, location, `orchestration is missing operation '${op}'`);
|
|
@@ -396,7 +396,7 @@ function validateOrchestration(value: any, location: string, errors: string[]):
|
|
|
396
396
|
const entry = value[op];
|
|
397
397
|
if (!object(entry)) { issue(errors, at, 'must be an object'); continue; }
|
|
398
398
|
if (!ORCHESTRATION_STATUS.has(entry.status)) issue(errors, at, 'status must be supported, unsupported, or unknown');
|
|
399
|
-
if (!Array.isArray(entry.evidence) || entry.evidence.some((item
|
|
399
|
+
if (!Array.isArray(entry.evidence) || entry.evidence.some((item ) => typeof item !== 'string')) {
|
|
400
400
|
issue(errors, at, 'evidence must be an array of strings');
|
|
401
401
|
} else if (entry.evidence.length === 0 && (entry.status === 'supported' || entry.status === 'unsupported')) {
|
|
402
402
|
issue(errors, at, `evidence must not be empty when status is ${entry.status}`);
|
|
@@ -412,8 +412,8 @@ function validateOrchestration(value: any, location: string, errors: string[]):
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
function validateRegistry(registry
|
|
416
|
-
const errors
|
|
415
|
+
function validateRegistry(registry , { repoRoot, fsImpl = fs } = {}) {
|
|
416
|
+
const errors = [];
|
|
417
417
|
const harnesses = Array.isArray(registry?.harnesses) ? registry.harnesses : null;
|
|
418
418
|
const assets = Array.isArray(registry?.assets) ? registry.assets : null;
|
|
419
419
|
const mcp = Array.isArray(registry?.mcp) ? registry.mcp : null;
|
|
@@ -430,14 +430,14 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
430
430
|
idsUnique(mcp, 'mcp', errors); idsUnique(lifecycle, 'lifecycle', errors);
|
|
431
431
|
idsUnique(externalTools, 'externalTools', errors);
|
|
432
432
|
|
|
433
|
-
const harnessIds = new Set((harnesses || []).filter(object).map((item
|
|
433
|
+
const harnessIds = new Set((harnesses || []).filter(object).map((item ) => item.id));
|
|
434
434
|
for (const harness of harnesses || []) {
|
|
435
435
|
const at = `harness '${harness?.id ?? '?'}'`;
|
|
436
436
|
if (!object(harness)) { issue(errors, 'harnesses', 'entries must be objects'); continue; }
|
|
437
437
|
if (typeof harness.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(harness.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
438
438
|
if (typeof harness.displayName !== 'string' || !harness.displayName.trim()) issue(errors, at, 'requires displayName');
|
|
439
439
|
if (typeof harness.adapter !== 'string' || !harness.adapter.trim()) issue(errors, at, 'requires adapter');
|
|
440
|
-
if (!Array.isArray(harness.scopes) || harness.scopes.length === 0 || harness.scopes.some((scope
|
|
440
|
+
if (!Array.isArray(harness.scopes) || harness.scopes.length === 0 || harness.scopes.some((scope ) => !SCOPES.has(scope))) issue(errors, at, 'scopes must contain project and/or user');
|
|
441
441
|
if (!object(harness.nativeTargets)) issue(errors, at, 'nativeTargets must be an object');
|
|
442
442
|
// Declared native paths (Stage 3): every surface a harness declares must parse into the
|
|
443
443
|
// minimal {base, segments} shape — unknown keys or malformed rules fail the load loudly so a
|
|
@@ -484,7 +484,7 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
484
484
|
if (!object(projection) || typeof projection.renderer !== 'string' || !projection.renderer.trim() || typeof projection.capability !== 'string' || !projection.capability.trim()) {
|
|
485
485
|
issue(errors, at, `requires renderer and capability projection for '${harnessId}'`); continue;
|
|
486
486
|
}
|
|
487
|
-
const harness = (harnesses || []).find((item
|
|
487
|
+
const harness = (harnesses || []).find((item ) => item?.id === harnessId);
|
|
488
488
|
const capability = harness?.capabilities?.[projection.capability];
|
|
489
489
|
if (!capability) issue(errors, at, `projection '${harnessId}' references unknown capability '${projection.capability}'`);
|
|
490
490
|
else if (capability.status === 'unavailable') issue(errors, at, `projection '${harnessId}' cannot render unavailable capability '${projection.capability}'`);
|
|
@@ -497,7 +497,7 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
497
497
|
if (typeof server.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(server.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
498
498
|
if (!MCP_TRANSPORTS.has(server.transport)) issue(errors, at, 'transport must be stdio, http, or sse');
|
|
499
499
|
if (server.transport === 'stdio' && (typeof server.command !== 'string' || !server.command.trim())) issue(errors, at, 'stdio transport requires command');
|
|
500
|
-
if (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg
|
|
500
|
+
if (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg ) => typeof arg !== 'string'))) issue(errors, at, 'args must be an array of strings');
|
|
501
501
|
if (!['optional', 'required'].includes(server.selection)) issue(errors, at, 'selection must be optional or required');
|
|
502
502
|
validateMcpStageUse(server, at, errors);
|
|
503
503
|
}
|
|
@@ -511,9 +511,9 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
511
511
|
if (typeof policy.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(policy.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
512
512
|
if (typeof policy.intent !== 'string' || !policy.intent.trim()) issue(errors, at, 'requires intent');
|
|
513
513
|
if (policy.tier !== undefined && !POLICY_TIERS.has(policy.tier)) issue(errors, at, `tier must be one of ${[...POLICY_TIERS].join(', ')}`);
|
|
514
|
-
if (!Array.isArray(policy.requires) || policy.requires.some((capability
|
|
514
|
+
if (!Array.isArray(policy.requires) || policy.requires.some((capability ) => typeof capability !== 'string')) issue(errors, at, 'requires must be an array of capability names');
|
|
515
515
|
if (!object(policy.mappings)) { issue(errors, at, 'mappings must be an object'); continue; }
|
|
516
|
-
for (const [harnessId, mapping] of Object.entries(policy.mappings)
|
|
516
|
+
for (const [harnessId, mapping] of Object.entries(policy.mappings) ) {
|
|
517
517
|
if (!harnessIds.has(harnessId)) { issue(errors, at, `mapping references unknown harness '${harnessId}'`); continue; }
|
|
518
518
|
if (!object(mapping) || !CAPABILITY_STATUS.has(mapping.status)) { issue(errors, at, `mapping '${harnessId}' needs a valid status`); continue; }
|
|
519
519
|
if (mapping.status !== 'unavailable' && (typeof mapping.event !== 'string' || !mapping.event.trim())) issue(errors, at, `mapping '${harnessId}' requires event for a renderable status`);
|
|
@@ -539,21 +539,21 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
539
539
|
issue(errors, 'models.slots', 'must be an array when present');
|
|
540
540
|
} else {
|
|
541
541
|
idsUnique(modelSlots, 'models.slots', errors);
|
|
542
|
-
const providerIds = new Set((modelProviders || []).filter(object).map((item
|
|
542
|
+
const providerIds = new Set((modelProviders || []).filter(object).map((item ) => item.id));
|
|
543
543
|
for (const slot of modelSlots) validateModelSlot(slot, `model slot '${slot?.id ?? '?'}'`, errors, providerIds);
|
|
544
544
|
}
|
|
545
545
|
return { ok: errors.length === 0, errors };
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
function harnessFor(registry
|
|
549
|
-
const harness = registry.harnesses.find((item
|
|
548
|
+
function harnessFor(registry , id ) {
|
|
549
|
+
const harness = registry.harnesses.find((item ) => item.id === id);
|
|
550
550
|
if (!harness) throw new Error(`Unknown registry harness '${id}'`);
|
|
551
551
|
return harness;
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
function selectAssets(registry
|
|
554
|
+
function selectAssets(registry , { harness, capability, kind } = {}) {
|
|
555
555
|
const declaration = harness ? harnessFor(registry, harness) : null;
|
|
556
|
-
return registry.assets.filter((asset
|
|
556
|
+
return registry.assets.filter((asset ) => {
|
|
557
557
|
if (kind && asset.kind !== kind) return false;
|
|
558
558
|
if (!harness) return true;
|
|
559
559
|
if (!asset.appliesTo.includes(harness)) return false;
|
|
@@ -562,18 +562,18 @@ function selectAssets(registry: any, { harness, capability, kind }: { harness?:
|
|
|
562
562
|
});
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
function selectMcpServers(registry
|
|
565
|
+
function selectMcpServers(registry , ids ) {
|
|
566
566
|
if (ids === undefined || ids === null) return [...registry.mcp];
|
|
567
567
|
if (!Array.isArray(ids)) throw new Error('MCP selection must be an array of server ids');
|
|
568
568
|
const wanted = new Set(ids);
|
|
569
|
-
const unknown = [...wanted].filter((id) => !registry.mcp.some((server
|
|
569
|
+
const unknown = [...wanted].filter((id) => !registry.mcp.some((server ) => server.id === id));
|
|
570
570
|
if (unknown.length) throw new Error(`Unknown registry MCP server(s): ${unknown.join(', ')}`);
|
|
571
|
-
return registry.mcp.filter((server
|
|
571
|
+
return registry.mcp.filter((server ) => wanted.has(server.id));
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
/** True when the harness takes servers from the shared MCP catalog; the lifecycle and the selection
|
|
575
575
|
* resolver read this one predicate. */
|
|
576
|
-
function mcpCapable(registry
|
|
576
|
+
function mcpCapable(registry , harnessId ) {
|
|
577
577
|
return harnessFor(registry, harnessId).capabilities.mcp?.status === 'supported';
|
|
578
578
|
}
|
|
579
579
|
|
|
@@ -581,7 +581,7 @@ function mcpCapable(registry: any, harnessId: string): boolean {
|
|
|
581
581
|
* harness's native MCP reconciliation (Codex's config.toml writer, Claude's ~/.claude.json/
|
|
582
582
|
* .mcp.json writer) expects — the one place this transform is derived, reused rather than
|
|
583
583
|
* re-implemented per consumer. */
|
|
584
|
-
function nativeMcpCatalog(servers
|
|
584
|
+
function nativeMcpCatalog(servers = []) {
|
|
585
585
|
const allServers = servers.map((server) => server.id);
|
|
586
586
|
const serverDefs = Object.fromEntries(servers.map((server) => [server.id, {
|
|
587
587
|
command: server.command,
|
|
@@ -591,8 +591,8 @@ function nativeMcpCatalog(servers: any[] = []): { allServers: string[], serverDe
|
|
|
591
591
|
return { allServers, serverDefs };
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
function capabilityMapData(registry
|
|
595
|
-
return registry.harnesses.flatMap((harness
|
|
594
|
+
function capabilityMapData(registry ) {
|
|
595
|
+
return registry.harnesses.flatMap((harness ) => (Object.entries(harness.capabilities) ).map(([capability, declaration]) => ({
|
|
596
596
|
harness: harness.id,
|
|
597
597
|
displayName: harness.displayName,
|
|
598
598
|
capability,
|