@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.
Files changed (236) hide show
  1. package/bin/doflow.js +2 -2
  2. package/core/.antigravity-plugin/plugin.json +0 -1
  3. package/core/.claude-plugin/plugin.json +1 -1
  4. package/core/.codex-plugin/plugin.json +1 -1
  5. package/core/.plugin/plugin.json +1 -1
  6. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  7. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  8. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  9. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  10. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  11. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  12. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  13. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  14. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  15. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  16. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  17. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  18. package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
  19. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  20. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
  21. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
  22. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
  23. package/core/harnesses/gemini/hooks/session-start.sh +5 -3
  24. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
  25. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
  26. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
  27. package/core/harnesses/kiro/hooks/session-start.sh +11 -5
  28. package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
  29. package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
  30. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
  31. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  32. package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
  33. package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
  34. package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
  35. package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
  36. package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
  37. package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
  38. package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
  39. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
  40. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
  41. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
  42. package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
  43. package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
  44. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
  45. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
  46. package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
  47. package/core/registry/assets.json +19 -0
  48. package/core/registry/contracts.json +2 -2
  49. package/core/registry/harnesses.json +196 -10
  50. package/core/registry/lifecycle.json +2626 -7
  51. package/core/registry/storage.json +110 -83
  52. package/core/registry/verification.json +1 -1
  53. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
  54. package/core/shared/scripts/doflow/bin/doflow-run +10 -6
  55. package/core/shared/skills/do-design/SKILL.md +6 -6
  56. package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
  57. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
  58. package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
  59. package/core/shared/skills/do-implement/SKILL.md +3 -2
  60. package/core/shared/skills/do-plan/SKILL.md +7 -8
  61. package/package.json +4 -2
  62. package/src/adapters/antigravity/{index.ts → index.js} +141 -105
  63. package/src/adapters/claude/{index.ts → index.js} +63 -49
  64. package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
  65. package/src/adapters/codex/{agents.ts → agents.js} +24 -24
  66. package/src/adapters/codex/{config.ts → config.js} +49 -49
  67. package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
  68. package/src/adapters/codex/{index.ts → index.js} +148 -105
  69. package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
  70. package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
  71. package/src/adapters/copilot/{index.ts → index.js} +42 -42
  72. package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
  73. package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
  74. package/src/adapters/gemini/{index.ts → index.js} +96 -57
  75. package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
  76. package/src/adapters/{index.ts → index.js} +13 -13
  77. package/src/adapters/kiro/{index.ts → index.js} +49 -35
  78. package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
  79. package/src/adapters/opencode/{index.ts → index.js} +52 -52
  80. package/src/adapters/pi/hooks.js +119 -0
  81. package/src/adapters/pi/{index.ts → index.js} +64 -39
  82. package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
  83. package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
  84. package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
  85. package/src/cli/commands/{install.ts → install.js} +25 -25
  86. package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
  87. package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
  88. package/src/cli/commands/{remove.ts → remove.js} +10 -10
  89. package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
  90. package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
  91. package/src/cli/commands/{status.ts → status.js} +24 -24
  92. package/src/cli/commands/{tools.ts → tools.js} +16 -16
  93. package/src/cli/commands/{update.ts → update.js} +20 -20
  94. package/src/cli/{index.ts → index.js} +43 -31
  95. package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
  96. package/src/cli/{shared.ts → shared.js} +40 -40
  97. package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
  98. package/src/helper/{git.ts → git.js} +2 -2
  99. package/src/helper/{guardrails.ts → guardrails.js} +10 -10
  100. package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
  101. package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
  102. package/src/helper/{prompt.ts → prompt.js} +5 -5
  103. package/src/helper/{repo-root.ts → repo-root.js} +2 -2
  104. package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
  105. package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
  106. package/src/helper/{toml.ts → toml.js} +48 -48
  107. package/src/install/{backup-set.ts → backup-set.js} +16 -16
  108. package/src/install/{backup.ts → backup.js} +65 -65
  109. package/src/install/{context.ts → context.js} +15 -15
  110. package/src/install/{manifest.ts → manifest.js} +27 -27
  111. package/src/install/{mcp.ts → mcp.js} +30 -30
  112. package/src/install/{paths.ts → paths.js} +15 -15
  113. package/src/install/{targets.ts → targets.js} +10 -10
  114. package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
  115. package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
  116. package/src/lifecycle/coverage.js +57 -0
  117. package/src/lifecycle/hook-step.js +61 -0
  118. package/src/lifecycle/{index.ts → index.js} +160 -146
  119. package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
  120. package/src/lifecycle/{policies.ts → policies.js} +11 -11
  121. package/src/lifecycle/{view.ts → view.js} +28 -28
  122. package/src/registry/{index.ts → index.js} +195 -66
  123. package/src/release/build-package.js +91 -0
  124. package/src/release/changelog-section.js +70 -0
  125. package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
  126. package/src/runtime/advice/{cli.ts → cli.js} +31 -31
  127. package/src/runtime/advice/{client.ts → client.js} +44 -44
  128. package/src/runtime/advice/{config.ts → config.js} +16 -16
  129. package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
  130. package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
  131. package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
  132. package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
  133. package/src/runtime/advice/{record.ts → record.js} +19 -19
  134. package/src/runtime/advice/{registry.ts → registry.js} +16 -16
  135. package/src/runtime/advice/{strength.ts → strength.js} +7 -7
  136. package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
  137. package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
  138. package/src/runtime/candidate.js +505 -0
  139. package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
  140. package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
  141. package/src/runtime/{claims.ts → claims.js} +44 -35
  142. package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
  143. package/src/runtime/{cli.ts → cli.js} +59 -292
  144. package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
  145. package/src/runtime/context/budget.js +203 -0
  146. package/src/runtime/context/compose.js +1367 -0
  147. package/src/runtime/context/delivery-store.js +102 -0
  148. package/src/runtime/context/mandatory.js +594 -0
  149. package/src/runtime/context/pointer.js +142 -0
  150. package/src/runtime/context-pack.js +1277 -0
  151. package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
  152. package/src/runtime/evidence-fields.js +39 -0
  153. package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
  154. package/src/runtime/failure/{capture.ts → capture.js} +29 -29
  155. package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
  156. package/src/runtime/failure/{cli.ts → cli.js} +30 -30
  157. package/src/runtime/failure/{home.ts → home.js} +6 -6
  158. package/src/runtime/failure/{store.ts → store.js} +32 -32
  159. package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
  160. package/src/runtime/{freshness.ts → freshness.js} +59 -22
  161. package/src/runtime/{health.ts → health.js} +72 -72
  162. package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
  163. package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
  164. package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
  165. package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
  166. package/src/runtime/inventory/{index.ts → index.js} +36 -36
  167. package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
  168. package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
  169. package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
  170. package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
  171. package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
  172. package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
  173. package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
  174. package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
  175. package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
  176. package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
  177. package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
  178. package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
  179. package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
  180. package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
  181. package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
  182. package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
  183. package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
  184. package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
  185. package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
  186. package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
  187. package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
  188. package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
  189. package/src/runtime/{mask.ts → mask.js} +22 -22
  190. package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
  191. package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
  192. package/src/runtime/{model-router.ts → model-router.js} +20 -20
  193. package/src/runtime/{outcome.ts → outcome.js} +27 -27
  194. package/src/runtime/{reach.ts → reach.js} +21 -21
  195. package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
  196. package/src/runtime/{readiness.ts → readiness.js} +113 -43
  197. package/src/runtime/{recovery.ts → recovery.js} +15 -15
  198. package/src/runtime/{research-request.ts → research-request.js} +34 -34
  199. package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
  200. package/src/runtime/retrieval-record.js +223 -0
  201. package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
  202. package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
  203. package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
  204. package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
  205. package/src/runtime/source-version.js +289 -0
  206. package/src/runtime/storage/{cli.ts → cli.js} +9 -9
  207. package/src/runtime/storage/{engine.ts → engine.js} +37 -37
  208. package/src/runtime/storage/{registry.ts → registry.js} +19 -19
  209. package/src/runtime/storage/{roots.ts → roots.js} +8 -8
  210. package/src/runtime/storage/{rules.ts → rules.js} +26 -26
  211. package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
  212. package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
  213. package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
  214. package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
  215. package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
  216. package/src/runtime/{task-state.ts → task-state.js} +86 -21
  217. package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
  218. package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
  219. package/src/runtime/trace/{render.ts → render.js} +7 -7
  220. package/src/runtime/trace/{views.ts → views.js} +40 -40
  221. package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
  222. package/src/runtime/verification/{engine.ts → engine.js} +143 -143
  223. package/src/runtime/verification/{registry.ts → registry.js} +6 -6
  224. package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
  225. package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
  226. package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
  227. package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
  228. package/src/runtime/{worktree.ts → worktree.js} +31 -31
  229. package/src/state/{index.ts → index.js} +26 -26
  230. package/src/state/{lockfile.ts → lockfile.js} +20 -20
  231. package/src/state/{run-lock.ts → run-lock.js} +44 -44
  232. package/core/.antigravity-plugin/hooks.json +0 -39
  233. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  234. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  235. package/core/harnesses/shared/hooks/hooks.json +0 -47
  236. 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.ts');
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: string[], location: string, message: string): void { errors.push(`${location}: ${message}`); }
46
- function object(value: any): boolean { return value !== null && typeof value === 'object' && !Array.isArray(value); }
47
- function idsUnique(items: any, label: string, errors: string[]): void {
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: string, fsImpl: any = fs): any {
56
+ function parseRegistryFile(file , fsImpl = fs) {
57
57
  let text;
58
- try { text = fsImpl.readFileSync(file, 'utf8'); } catch (error: any) {
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: any) {
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: string): string { return path.join(path.resolve(repoRoot), 'core', 'registry'); }
66
+ function registryDir(repoRoot ) { return path.join(path.resolve(repoRoot), 'core', 'registry'); }
67
67
 
68
- function loadRegistry({ repoRoot, dir, fsImpl = fs }: { repoRoot?: string, dir?: string, fsImpl?: any } = {}): any {
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 as string);
71
- const loaded: any = {};
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) as [string, any][]).map(([name, value]) => [name, value.version])),
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: any = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
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: any, location: string, errors: string[], harnessIds: any): void {
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 and Pi break that: both implement lifecycle handlers as JS/TS code modules, so
136
- // DoFlow's shell hooks have nothing to attach to and it wires none. Forcing a non-empty list there
137
- // would mean declaring a hook DoFlow does not install — an inert claim of exactly the kind these
138
- // guards exist to remove. Empty is therefore allowed, but only with a note, so the distinction
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: any) => typeof url !== 'string' || !/^https:\/\//.test(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: any): boolean {
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: any, location: string, errors: string[]): void {
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: any) => typeof url !== 'string' || !/^https:\/\//.test(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: any) => typeof item !== 'string' || !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) as [string, any][]) {
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: any, source: any, location: string, errors: string[], fsImpl: any, expected: string = 'any'): void {
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: any, harness: any, { repoRoot, fsImpl }: { repoRoot: any, fsImpl: any }, errors: string[]): void {
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: any, location: string, errors: string[]): void {
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: any, location: string, errors: string[]): void {
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: any) => typeof url !== 'string' || !/^https:\/\//.test(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: any) => typeof item !== 'string' || !item.trim())
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: any, index: number) => validateCommand(command, `${location}.status.commands[${index}]`, errors));
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: any, location: string, errors: string[]): void {
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: any) => typeof url !== 'string' || !/^https:\/\//.test(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: any, location: string, errors: string[]): void {
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: any, location: string, errors: string[], providerIds: any): void {
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: any) => typeof url !== 'string' || !/^https:\/\//.test(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: any, at: string, errors: string[]): void {
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: any, index: number) => {
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: any, errors: string[]): void {
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) as [string, any][]) {
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: any, errors: string[]): void {
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: any) => typeof tier === 'string' && tier) ? value.tiers : null;
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: any, location: string, errors: string[]): void {
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: any) => typeof item !== 'string')) {
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
- function validateRegistry(registry: any, { repoRoot, fsImpl = fs }: { repoRoot?: string, fsImpl?: any } = {}): { ok: boolean, errors: string[] } {
416
- const errors: string[] = [];
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: any) => item.id));
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: any) => !SCOPES.has(scope))) issue(errors, at, 'scopes must contain project and/or user');
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: any) => item?.id === harnessId);
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: any) => typeof arg !== 'string'))) issue(errors, at, 'args must be an array of strings');
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: any) => typeof capability !== 'string')) issue(errors, at, 'requires must be an array of capability names');
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) as [string, any][]) {
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: any) => item.id));
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
- function harnessFor(registry: any, id: string): any {
549
- const harness = registry.harnesses.find((item: any) => item.id === id);
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: any, { harness, capability, kind }: { harness?: string, capability?: string, kind?: string } = {}): any[] {
683
+ function selectAssets(registry , { harness, capability, kind } = {}) {
555
684
  const declaration = harness ? harnessFor(registry, harness) : null;
556
- return registry.assets.filter((asset: any) => {
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: any, ids?: string[] | null): any[] {
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: any) => server.id === id));
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: any) => wanted.has(server.id));
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: any, harnessId: string): boolean {
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: any[] = []): { allServers: string[], serverDefs: any } {
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: any): any[] {
595
- return registry.harnesses.flatMap((harness: any) => (Object.entries(harness.capabilities) as [string, any][]).map(([capability, declaration]) => ({
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
  };