@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
|
@@ -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
|
}
|
|
@@ -83,6 +83,7 @@ function loadRegistry({ repoRoot, dir, fsImpl = fs }: { repoRoot?: string, dir?:
|
|
|
83
83
|
assets: loaded.assets.assets,
|
|
84
84
|
mcp: loaded.mcp.servers,
|
|
85
85
|
lifecycle: loaded.lifecycle.policies,
|
|
86
|
+
coverage: loaded.lifecycle.coverage ?? null,
|
|
86
87
|
contracts: loaded.contracts.contracts,
|
|
87
88
|
externalTools: loaded.externalTools.tools,
|
|
88
89
|
modelProviders: loaded.models.providers,
|
|
@@ -90,11 +91,11 @@ function loadRegistry({ repoRoot, dir, fsImpl = fs }: { repoRoot?: string, dir?:
|
|
|
90
91
|
retrievalSlots: Array.isArray(loaded.models.slots) ? loaded.models.slots : [],
|
|
91
92
|
dispatchTiers: loaded.models.dispatchTiers,
|
|
92
93
|
capabilities,
|
|
93
|
-
versions: Object.fromEntries((Object.entries(loaded)
|
|
94
|
+
versions: Object.fromEntries((Object.entries(loaded) ).map(([name, value]) => [name, value.version])),
|
|
94
95
|
};
|
|
95
96
|
const validation = validateRegistry(registry, { repoRoot: registry.repoRoot, fsImpl });
|
|
96
97
|
if (!validation.ok) {
|
|
97
|
-
const error
|
|
98
|
+
const error = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
|
|
98
99
|
error.validation = validation;
|
|
99
100
|
throw error;
|
|
100
101
|
}
|
|
@@ -109,7 +110,7 @@ const CONTRACT_COMPLETENESS = new Set(['verified', 'lower-bound']);
|
|
|
109
110
|
*
|
|
110
111
|
* `evidence` is required for the same reason capabilities require it: a contract claim with no
|
|
111
112
|
* citation is folklore, and these lists are copied from vendor documentation that moves. */
|
|
112
|
-
function validateContract(value
|
|
113
|
+
function validateContract(value , location , errors , harnessIds ) {
|
|
113
114
|
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
114
115
|
if (typeof value.harness !== 'string' || !harnessIds.has(value.harness)) {
|
|
115
116
|
issue(errors, location, `harness must be one of: ${[...harnessIds].join(', ')}`);
|
|
@@ -132,11 +133,10 @@ function validateContract(value: any, location: string, errors: string[], harnes
|
|
|
132
133
|
if (new Set(list).size !== list.length) issue(errors, `${location}.${field}`, 'must not contain duplicates');
|
|
133
134
|
}
|
|
134
135
|
// An empty hookEvents used to be rejected outright, which assumed every harness wires at least one
|
|
135
|
-
// hook. OpenCode
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// between "wires nothing, deliberately" and "nobody filled this in" stays visible.
|
|
136
|
+
// hook. OpenCode has no DoFlow hook projection; Pi uses a code extension only for tool_call.
|
|
137
|
+
// Forcing a non-empty list for an unsupported event would declare a hook DoFlow does not install —
|
|
138
|
+
// an inert claim of exactly the kind these guards exist to remove. Empty is therefore allowed, but
|
|
139
|
+
// only with a note, so deliberate gaps stay distinct from missing registry data.
|
|
140
140
|
const events = value.hookEvents;
|
|
141
141
|
const noted = typeof value.note === 'string' && value.note.trim().length > 0;
|
|
142
142
|
if (!Array.isArray(events) || events.some((item) => typeof item !== 'string' || !item)) {
|
|
@@ -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,96 @@ function validateOrchestration(value: any, location: string, errors: string[]):
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
const CONTEXT_PROFILE_FIELDS = Object.freeze([
|
|
416
|
+
'capacityTokens',
|
|
417
|
+
'reserveTokens',
|
|
418
|
+
'tokenTelemetry',
|
|
419
|
+
'toolResultLimitBytes',
|
|
420
|
+
'retention',
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
/** Validates one harness's `contextProfile` block (075 IC-013): capacity, reserve, telemetry,
|
|
424
|
+
* toolResultLimitBytes and retention. Each is a Fact<T> with {value, source, verifiedOn} or
|
|
425
|
+
* {unverified, verifiedOn}. */
|
|
426
|
+
function validateContextProfile(value , location , errors ) {
|
|
427
|
+
if (!object(value)) {
|
|
428
|
+
issue(errors, location, 'contextProfile block is required and must be an object');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
for (const field of CONTEXT_PROFILE_FIELDS) {
|
|
432
|
+
if (!Object.hasOwn(value, field)) {
|
|
433
|
+
issue(errors, location, `contextProfile is missing field '${field}'`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const key of Object.keys(value)) {
|
|
437
|
+
if (!CONTEXT_PROFILE_FIELDS.includes(key)) {
|
|
438
|
+
issue(errors, location, `contextProfile has unknown key '${key}'`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const field of CONTEXT_PROFILE_FIELDS) {
|
|
442
|
+
if (!Object.hasOwn(value, field)) continue;
|
|
443
|
+
const at = `${location} contextProfile '${field}'`;
|
|
444
|
+
const fact = value[field];
|
|
445
|
+
if (!object(fact)) {
|
|
446
|
+
issue(errors, at, 'must be a Fact object');
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const hasUnverified = Object.hasOwn(fact, 'unverified');
|
|
450
|
+
const hasValue = Object.hasOwn(fact, 'value');
|
|
451
|
+
|
|
452
|
+
if (hasUnverified) {
|
|
453
|
+
if (typeof fact.unverified !== 'string' || !fact.unverified.trim()) {
|
|
454
|
+
issue(errors, at, 'unverified must be a non-empty string');
|
|
455
|
+
}
|
|
456
|
+
if (hasValue) {
|
|
457
|
+
issue(errors, at, 'cannot declare both value and unverified');
|
|
458
|
+
}
|
|
459
|
+
} else if (hasValue) {
|
|
460
|
+
if (typeof fact.source !== 'string' || !fact.source.trim()) {
|
|
461
|
+
issue(errors, at, 'a declared value requires a non-empty source');
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
issue(errors, at, 'requires either value with source or a non-empty unverified');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (
|
|
468
|
+
typeof fact.verifiedOn !== 'string' ||
|
|
469
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(fact.verifiedOn) ||
|
|
470
|
+
Number.isNaN(Date.parse(fact.verifiedOn)) ||
|
|
471
|
+
new Date(fact.verifiedOn).toISOString().slice(0, 10) !== fact.verifiedOn
|
|
472
|
+
) {
|
|
473
|
+
issue(errors, at, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (hasValue && fact.value !== undefined) {
|
|
477
|
+
const val = fact.value;
|
|
478
|
+
if (field === 'capacityTokens') {
|
|
479
|
+
if (typeof val !== 'number' || !Number.isInteger(val) || val <= 0) {
|
|
480
|
+
issue(errors, at, 'value must be a positive integer');
|
|
481
|
+
}
|
|
482
|
+
} else if (field === 'reserveTokens') {
|
|
483
|
+
if (typeof val !== 'number' || !Number.isInteger(val) || val < 0) {
|
|
484
|
+
issue(errors, at, 'value must be a non-negative integer');
|
|
485
|
+
}
|
|
486
|
+
} else if (field === 'tokenTelemetry') {
|
|
487
|
+
if (val !== 'harness-reported' && val !== 'none') {
|
|
488
|
+
issue(errors, at, "value must be 'harness-reported' or 'none'");
|
|
489
|
+
}
|
|
490
|
+
} else if (field === 'toolResultLimitBytes') {
|
|
491
|
+
if (typeof val !== 'number' || !Number.isInteger(val) || val <= 0) {
|
|
492
|
+
issue(errors, at, 'value must be a positive integer');
|
|
493
|
+
}
|
|
494
|
+
} else if (field === 'retention') {
|
|
495
|
+
if (val !== 'supported' && val !== 'unsupported') {
|
|
496
|
+
issue(errors, at, "value must be 'supported' or 'unsupported'");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function validateRegistry(registry , { repoRoot, fsImpl = fs } = {}) {
|
|
504
|
+
const errors = [];
|
|
417
505
|
const harnesses = Array.isArray(registry?.harnesses) ? registry.harnesses : null;
|
|
418
506
|
const assets = Array.isArray(registry?.assets) ? registry.assets : null;
|
|
419
507
|
const mcp = Array.isArray(registry?.mcp) ? registry.mcp : null;
|
|
@@ -430,14 +518,14 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
430
518
|
idsUnique(mcp, 'mcp', errors); idsUnique(lifecycle, 'lifecycle', errors);
|
|
431
519
|
idsUnique(externalTools, 'externalTools', errors);
|
|
432
520
|
|
|
433
|
-
const harnessIds = new Set((harnesses || []).filter(object).map((item
|
|
521
|
+
const harnessIds = new Set ((harnesses || []).filter(object).map((item ) => item.id ));
|
|
434
522
|
for (const harness of harnesses || []) {
|
|
435
523
|
const at = `harness '${harness?.id ?? '?'}'`;
|
|
436
524
|
if (!object(harness)) { issue(errors, 'harnesses', 'entries must be objects'); continue; }
|
|
437
525
|
if (typeof harness.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(harness.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
438
526
|
if (typeof harness.displayName !== 'string' || !harness.displayName.trim()) issue(errors, at, 'requires displayName');
|
|
439
527
|
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
|
|
528
|
+
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
529
|
if (!object(harness.nativeTargets)) issue(errors, at, 'nativeTargets must be an object');
|
|
442
530
|
// Declared native paths (Stage 3): every surface a harness declares must parse into the
|
|
443
531
|
// minimal {base, segments} shape — unknown keys or malformed rules fail the load loudly so a
|
|
@@ -451,6 +539,7 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
451
539
|
validateCapability(declaration, `${at} capability '${capability}'`, errors);
|
|
452
540
|
}
|
|
453
541
|
validateOrchestration(harness.orchestration, at, errors);
|
|
542
|
+
validateContextProfile(harness.contextProfile, at, errors);
|
|
454
543
|
if (harness.id === 'codex') validateNativeProjection(harness.nativeProjection, harness, { repoRoot, fsImpl }, errors);
|
|
455
544
|
}
|
|
456
545
|
|
|
@@ -484,7 +573,7 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
484
573
|
if (!object(projection) || typeof projection.renderer !== 'string' || !projection.renderer.trim() || typeof projection.capability !== 'string' || !projection.capability.trim()) {
|
|
485
574
|
issue(errors, at, `requires renderer and capability projection for '${harnessId}'`); continue;
|
|
486
575
|
}
|
|
487
|
-
const harness = (harnesses || []).find((item
|
|
576
|
+
const harness = (harnesses || []).find((item ) => item?.id === harnessId);
|
|
488
577
|
const capability = harness?.capabilities?.[projection.capability];
|
|
489
578
|
if (!capability) issue(errors, at, `projection '${harnessId}' references unknown capability '${projection.capability}'`);
|
|
490
579
|
else if (capability.status === 'unavailable') issue(errors, at, `projection '${harnessId}' cannot render unavailable capability '${projection.capability}'`);
|
|
@@ -497,7 +586,7 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
497
586
|
if (typeof server.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(server.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
498
587
|
if (!MCP_TRANSPORTS.has(server.transport)) issue(errors, at, 'transport must be stdio, http, or sse');
|
|
499
588
|
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
|
|
589
|
+
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
590
|
if (!['optional', 'required'].includes(server.selection)) issue(errors, at, 'selection must be optional or required');
|
|
502
591
|
validateMcpStageUse(server, at, errors);
|
|
503
592
|
}
|
|
@@ -511,15 +600,17 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
511
600
|
if (typeof policy.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(policy.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
512
601
|
if (typeof policy.intent !== 'string' || !policy.intent.trim()) issue(errors, at, 'requires intent');
|
|
513
602
|
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
|
|
603
|
+
if (!Array.isArray(policy.requires) || policy.requires.some((capability ) => typeof capability !== 'string')) issue(errors, at, 'requires must be an array of capability names');
|
|
515
604
|
if (!object(policy.mappings)) { issue(errors, at, 'mappings must be an object'); continue; }
|
|
516
|
-
for (const [harnessId, mapping] of Object.entries(policy.mappings)
|
|
605
|
+
for (const [harnessId, mapping] of Object.entries(policy.mappings) ) {
|
|
517
606
|
if (!harnessIds.has(harnessId)) { issue(errors, at, `mapping references unknown harness '${harnessId}'`); continue; }
|
|
518
607
|
if (!object(mapping) || !CAPABILITY_STATUS.has(mapping.status)) { issue(errors, at, `mapping '${harnessId}' needs a valid status`); continue; }
|
|
519
608
|
if (mapping.status !== 'unavailable' && (typeof mapping.event !== 'string' || !mapping.event.trim())) issue(errors, at, `mapping '${harnessId}' requires event for a renderable status`);
|
|
520
609
|
if (mapping.status === 'unavailable' && typeof mapping.fallback !== 'string') issue(errors, at, `unavailable mapping '${harnessId}' requires fallback guidance`);
|
|
521
610
|
}
|
|
522
611
|
}
|
|
612
|
+
|
|
613
|
+
validateCoverage(registry?.coverage, registry?.lifecycle, harnessIds, errors);
|
|
523
614
|
for (const tool of externalTools || []) validateExternalTool(tool, `external tool '${tool?.id ?? '?'}'`, errors);
|
|
524
615
|
|
|
525
616
|
const modelProviders = Array.isArray(registry?.modelProviders) ? registry.modelProviders : null;
|
|
@@ -539,21 +630,59 @@ function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?:
|
|
|
539
630
|
issue(errors, 'models.slots', 'must be an array when present');
|
|
540
631
|
} else {
|
|
541
632
|
idsUnique(modelSlots, 'models.slots', errors);
|
|
542
|
-
const providerIds = new Set((modelProviders || []).filter(object).map((item
|
|
633
|
+
const providerIds = new Set((modelProviders || []).filter(object).map((item ) => item.id));
|
|
543
634
|
for (const slot of modelSlots) validateModelSlot(slot, `model slot '${slot?.id ?? '?'}'`, errors, providerIds);
|
|
544
635
|
}
|
|
545
636
|
return { ok: errors.length === 0, errors };
|
|
546
637
|
}
|
|
547
638
|
|
|
548
|
-
|
|
549
|
-
|
|
639
|
+
const COVERAGE_CLASSES = new Set(['blocking', 'observation', 'advisory', 'unsupported', 'unknown']);
|
|
640
|
+
const COVERAGE_TIMINGS = new Set(['pre-tool', 'post-tool', 'session', 'prompt', 'stop', 'compact', 'subagent']);
|
|
641
|
+
const COVERAGE_PROFILES = new Set(['any', 'user-interactive', 'user-headless', 'project-interactive', 'project-headless', 'cli-extension', 'no-extensions', 'ide', 'cli-v3', 'cli-v3-agent', 'cli-2x-agent']);
|
|
642
|
+
const COVERAGE_KINDS = new Set(['doc', 'types', 'fixture', 'live']);
|
|
643
|
+
|
|
644
|
+
function validateCoverage(value , lifecycle , harnessIds , errors ) {
|
|
645
|
+
if (value === undefined || value === null) return;
|
|
646
|
+
const at = 'lifecycle.coverage';
|
|
647
|
+
if (!object(value)) { issue(errors, at, 'must be an object'); return; }
|
|
648
|
+
if (value.schemaVersion !== 1) issue(errors, `${at}.schemaVersion`, 'must be 1');
|
|
649
|
+
if (!Number.isInteger(value.revision) || value.revision < 1) issue(errors, `${at}.revision`, 'must be a positive integer');
|
|
650
|
+
if (typeof value.rowsDigest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value.rowsDigest)) issue(errors, `${at}.rowsDigest`, 'must be sha256 followed by 64 lowercase hex characters');
|
|
651
|
+
if (!object(value.refresh) || typeof value.refresh.trigger !== 'string' || !value.refresh.trigger.trim() || typeof value.refresh.owner !== 'string' || !value.refresh.owner.trim()) issue(errors, `${at}.refresh`, 'requires non-empty trigger and owner');
|
|
652
|
+
if (!object(value.profiles) || [...COVERAGE_PROFILES].some((profile) => typeof value.profiles?.[profile] !== 'string' || !value.profiles[profile].trim())) issue(errors, `${at}.profiles`, 'must define every supported profile with a non-empty meaning');
|
|
653
|
+
if (!Array.isArray(value.rows)) { issue(errors, `${at}.rows`, 'must be an array'); return; }
|
|
654
|
+
const keys = new Set ();
|
|
655
|
+
for (const [index, row] of value.rows.entries()) {
|
|
656
|
+
const rowAt = `${at}.rows[${index}]`;
|
|
657
|
+
if (!object(row)) { issue(errors, rowAt, 'must be an object'); continue; }
|
|
658
|
+
for (const field of ['policy', 'harness', 'event']) if (typeof row[field] !== 'string' || !row[field]) issue(errors, `${rowAt}.${field}`, 'must be a non-empty string');
|
|
659
|
+
if (row.policy !== '*' && !(lifecycle || []).some((policy ) => policy.id === row.policy)) issue(errors, `${rowAt}.policy`, 'must name a lifecycle policy or *');
|
|
660
|
+
if (!harnessIds.has(row.harness)) issue(errors, `${rowAt}.harness`, 'must name a registry harness');
|
|
661
|
+
if (!COVERAGE_TIMINGS.has(row.timing)) issue(errors, `${rowAt}.timing`, 'has an invalid timing');
|
|
662
|
+
if (!COVERAGE_PROFILES.has(row.profile)) issue(errors, `${rowAt}.profile`, 'has an invalid profile');
|
|
663
|
+
if (typeof row.wired !== 'boolean') issue(errors, `${rowAt}.wired`, 'must be a boolean');
|
|
664
|
+
if (row.configSource !== null && (typeof row.configSource !== 'string' || !row.configSource)) issue(errors, `${rowAt}.configSource`, 'must be a path:line or null');
|
|
665
|
+
if (row.wired && typeof row.configSource !== 'string') issue(errors, `${rowAt}.configSource`, 'is required when wired');
|
|
666
|
+
if (!object(row.harnessVersion) || typeof row.harnessVersion.tested !== 'string' || !row.harnessVersion.tested || (row.harnessVersion.range !== undefined && typeof row.harnessVersion.range !== 'string')) issue(errors, `${rowAt}.harnessVersion`, 'requires tested and optional range strings');
|
|
667
|
+
if (!COVERAGE_CLASSES.has(row.class)) issue(errors, `${rowAt}.class`, 'has an invalid class');
|
|
668
|
+
if (!['verified', 'unverified'].includes(row.verification)) issue(errors, `${rowAt}.verification`, 'must be verified or unverified');
|
|
669
|
+
for (const field of ['prerequisites', 'covers', 'doesNotCover', 'caveats']) if (!Array.isArray(row[field]) || row[field].some((item ) => typeof item !== 'string')) issue(errors, `${rowAt}.${field}`, 'must be an array of strings');
|
|
670
|
+
if (!Array.isArray(row.provenance) || row.provenance.some((item ) => !object(item) || !COVERAGE_KINDS.has(item.kind) || typeof item.locator !== 'string' || !item.locator || (['doc', 'types', 'live'].includes(item.kind) && (typeof item.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(item.date))))) issue(errors, `${rowAt}.provenance`, 'contains malformed provenance');
|
|
671
|
+
const key = JSON.stringify([row.policy, row.harness, row.event, row.profile]);
|
|
672
|
+
if (keys.has(key)) issue(errors, rowAt, 'duplicates a policy/harness/event/profile tuple');
|
|
673
|
+
keys.add(key);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function harnessFor(registry , id ) {
|
|
678
|
+
const harness = registry.harnesses.find((item ) => item.id === id);
|
|
550
679
|
if (!harness) throw new Error(`Unknown registry harness '${id}'`);
|
|
551
680
|
return harness;
|
|
552
681
|
}
|
|
553
682
|
|
|
554
|
-
function selectAssets(registry
|
|
683
|
+
function selectAssets(registry , { harness, capability, kind } = {}) {
|
|
555
684
|
const declaration = harness ? harnessFor(registry, harness) : null;
|
|
556
|
-
return registry.assets.filter((asset
|
|
685
|
+
return registry.assets.filter((asset ) => {
|
|
557
686
|
if (kind && asset.kind !== kind) return false;
|
|
558
687
|
if (!harness) return true;
|
|
559
688
|
if (!asset.appliesTo.includes(harness)) return false;
|
|
@@ -562,18 +691,18 @@ function selectAssets(registry: any, { harness, capability, kind }: { harness?:
|
|
|
562
691
|
});
|
|
563
692
|
}
|
|
564
693
|
|
|
565
|
-
function selectMcpServers(registry
|
|
694
|
+
function selectMcpServers(registry , ids ) {
|
|
566
695
|
if (ids === undefined || ids === null) return [...registry.mcp];
|
|
567
696
|
if (!Array.isArray(ids)) throw new Error('MCP selection must be an array of server ids');
|
|
568
697
|
const wanted = new Set(ids);
|
|
569
|
-
const unknown = [...wanted].filter((id) => !registry.mcp.some((server
|
|
698
|
+
const unknown = [...wanted].filter((id) => !registry.mcp.some((server ) => server.id === id));
|
|
570
699
|
if (unknown.length) throw new Error(`Unknown registry MCP server(s): ${unknown.join(', ')}`);
|
|
571
|
-
return registry.mcp.filter((server
|
|
700
|
+
return registry.mcp.filter((server ) => wanted.has(server.id));
|
|
572
701
|
}
|
|
573
702
|
|
|
574
703
|
/** True when the harness takes servers from the shared MCP catalog; the lifecycle and the selection
|
|
575
704
|
* resolver read this one predicate. */
|
|
576
|
-
function mcpCapable(registry
|
|
705
|
+
function mcpCapable(registry , harnessId ) {
|
|
577
706
|
return harnessFor(registry, harnessId).capabilities.mcp?.status === 'supported';
|
|
578
707
|
}
|
|
579
708
|
|
|
@@ -581,7 +710,7 @@ function mcpCapable(registry: any, harnessId: string): boolean {
|
|
|
581
710
|
* harness's native MCP reconciliation (Codex's config.toml writer, Claude's ~/.claude.json/
|
|
582
711
|
* .mcp.json writer) expects — the one place this transform is derived, reused rather than
|
|
583
712
|
* re-implemented per consumer. */
|
|
584
|
-
function nativeMcpCatalog(servers
|
|
713
|
+
function nativeMcpCatalog(servers = []) {
|
|
585
714
|
const allServers = servers.map((server) => server.id);
|
|
586
715
|
const serverDefs = Object.fromEntries(servers.map((server) => [server.id, {
|
|
587
716
|
command: server.command,
|
|
@@ -591,8 +720,8 @@ function nativeMcpCatalog(servers: any[] = []): { allServers: string[], serverDe
|
|
|
591
720
|
return { allServers, serverDefs };
|
|
592
721
|
}
|
|
593
722
|
|
|
594
|
-
function capabilityMapData(registry
|
|
595
|
-
return registry.harnesses.flatMap((harness
|
|
723
|
+
function capabilityMapData(registry ) {
|
|
724
|
+
return registry.harnesses.flatMap((harness ) => (Object.entries(harness.capabilities) ).map(([capability, declaration]) => ({
|
|
596
725
|
harness: harness.id,
|
|
597
726
|
displayName: harness.displayName,
|
|
598
727
|
capability,
|
|
@@ -605,7 +734,7 @@ function capabilityMapData(registry: any): any[] {
|
|
|
605
734
|
}
|
|
606
735
|
|
|
607
736
|
module.exports = {
|
|
608
|
-
REGISTRY_FILES, CAPABILITY_STATUS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS,
|
|
737
|
+
REGISTRY_FILES, CAPABILITY_STATUS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, CONTEXT_PROFILE_FIELDS, validateContextProfile, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS, validateCoverage,
|
|
609
738
|
parseRegistryFile, registryDir, loadRegistry, validateRegistry,
|
|
610
739
|
harnessFor, selectAssets, selectMcpServers, mcpCapable, nativeMcpCatalog, capabilityMapData,
|
|
611
740
|
};
|