@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1061 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +48 -4
- package/src/adapters/antigravity/index.js +631 -0
- package/src/adapters/claude/index.js +548 -0
- package/src/adapters/claude/mcp.js +94 -0
- package/src/adapters/codex/agents.js +208 -0
- package/src/adapters/codex/config.js +272 -0
- package/src/adapters/codex/hooks.js +203 -0
- package/src/adapters/codex/index.js +707 -0
- package/src/adapters/codex/mcp.js +271 -0
- package/src/adapters/codex/native-projection.js +34 -0
- package/src/adapters/copilot/index.js +413 -0
- package/src/adapters/copy-tree.js +620 -0
- package/src/adapters/gemini/hooks.js +210 -0
- package/src/adapters/gemini/index.js +390 -0
- package/src/adapters/hook-commands.js +60 -0
- package/src/adapters/index.js +106 -0
- package/src/adapters/kiro/index.js +310 -0
- package/src/adapters/mcp-entries.js +331 -0
- package/src/adapters/opencode/index.js +466 -0
- package/src/adapters/pi/index.js +279 -0
- package/src/adapters/pi/json-members.js +185 -0
- package/src/adapters/pi/mcp.js +373 -0
- package/src/adapters/resolve-source.js +19 -0
- package/src/cli/commands/install.js +218 -0
- package/src/cli/commands/list-backups.js +21 -0
- package/src/cli/commands/reconcile.js +120 -0
- package/src/cli/commands/remove.js +82 -0
- package/src/cli/commands/rollback.js +172 -0
- package/src/cli/commands/self-update.js +33 -0
- package/src/cli/commands/status.js +138 -0
- package/src/cli/commands/tools.js +92 -0
- package/src/cli/commands/update.js +162 -0
- package/src/cli/index.js +568 -0
- package/src/cli/runtime-commands.js +281 -0
- package/src/cli/shared.js +218 -0
- package/src/helper/bash-helper.js +34 -0
- package/src/helper/git.js +24 -0
- package/src/helper/guardrails.js +49 -0
- package/src/helper/harness-paths.js +140 -0
- package/src/helper/marker-merge.js +116 -0
- package/src/helper/prompt.js +61 -0
- package/src/helper/repo-root.js +29 -0
- package/src/helper/settings-merge.js +151 -0
- package/src/helper/settings-scope.js +33 -0
- package/src/helper/toml.js +244 -0
- package/src/install/backup-set.js +149 -0
- package/src/install/backup.js +724 -0
- package/src/install/context.js +58 -0
- package/src/install/manifest.js +145 -0
- package/src/install/mcp.js +254 -0
- package/src/install/paths.js +84 -0
- package/src/install/targets.js +90 -0
- package/src/install/tool-lifecycle.js +151 -0
- package/src/lifecycle/bash-availability.js +25 -0
- package/src/lifecycle/index.js +812 -0
- package/src/lifecycle/mcp-index.js +28 -0
- package/src/lifecycle/policies.js +73 -0
- package/src/lifecycle/view.js +175 -0
- package/src/registry/index.js +611 -0
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/sync-plugin-versions.js +66 -0
- package/src/runtime/advice/cli.js +283 -0
- package/src/runtime/advice/client.js +324 -0
- package/src/runtime/advice/config.js +189 -0
- package/src/runtime/advice/doctor.js +169 -0
- package/src/runtime/advice/endpoint-guard.js +134 -0
- package/src/runtime/advice/kinds.js +154 -0
- package/src/runtime/advice/providers/ollama-systemone.js +209 -0
- package/src/runtime/advice/record.js +166 -0
- package/src/runtime/advice/registry.js +170 -0
- package/src/runtime/advice/strength.js +66 -0
- package/src/runtime/brief-tools.js +342 -0
- package/src/runtime/c4-project.js +371 -0
- package/src/runtime/capability-router.js +484 -0
- package/src/runtime/checkouts.js +170 -0
- package/src/runtime/claims.js +468 -0
- package/src/runtime/cli-result.js +55 -0
- package/src/runtime/cli.js +678 -0
- package/src/runtime/command-detect.js +516 -0
- package/src/runtime/context-pack.js +492 -0
- package/src/runtime/decision-register.js +493 -0
- package/src/runtime/evidence-ledger.js +338 -0
- package/src/runtime/failure/capture.js +272 -0
- package/src/runtime/failure/classifier.js +68 -0
- package/src/runtime/failure/cli.js +178 -0
- package/src/runtime/failure/home.js +64 -0
- package/src/runtime/failure/store.js +226 -0
- package/src/runtime/feature-resolve.js +79 -0
- package/src/runtime/freshness.js +177 -0
- package/src/runtime/health.js +863 -0
- package/src/runtime/history-compactor.js +237 -0
- package/src/runtime/implementation-gate.js +321 -0
- package/src/runtime/inventory/coverage.js +218 -0
- package/src/runtime/inventory/identity.js +65 -0
- package/src/runtime/inventory/index.js +705 -0
- package/src/runtime/inventory/precedence.js +134 -0
- package/src/runtime/inventory/read-scopes.js +131 -0
- package/src/runtime/inventory/siblings.js +192 -0
- package/src/runtime/knowledge/chunker.js +124 -0
- package/src/runtime/knowledge/hybrid.js +149 -0
- package/src/runtime/knowledge/index-store.js +145 -0
- package/src/runtime/knowledge/retrieval.js +194 -0
- package/src/runtime/leak-scan.js +185 -0
- package/src/runtime/lifecycle/cli.js +276 -0
- package/src/runtime/lifecycle/event-store.js +340 -0
- package/src/runtime/lifecycle/fold.js +353 -0
- package/src/runtime/lifecycle/followup.js +432 -0
- package/src/runtime/lifecycle/goal.js +249 -0
- package/src/runtime/lifecycle/intent-writer.js +86 -0
- package/src/runtime/lifecycle/overview.js +292 -0
- package/src/runtime/lifecycle/release.js +241 -0
- package/src/runtime/lifecycle/report-store.js +221 -0
- package/src/runtime/lifecycle/retention.js +212 -0
- package/src/runtime/lifecycle/root.js +71 -0
- package/src/runtime/lifecycle/status.js +353 -0
- package/src/runtime/lifecycle/store-upkeep.js +393 -0
- package/src/runtime/locator-resolve.js +158 -0
- package/src/runtime/mask.js +210 -0
- package/src/runtime/mcp-consultations.js +412 -0
- package/src/runtime/mcp-reach.js +157 -0
- package/src/runtime/model-router.js +153 -0
- package/src/runtime/outcome.js +467 -0
- package/src/runtime/reach.js +148 -0
- package/src/runtime/readiness-record.js +150 -0
- package/src/runtime/readiness.js +435 -0
- package/src/runtime/recovery.js +282 -0
- package/src/runtime/research-request.js +231 -0
- package/src/runtime/retrieval-plan.js +578 -0
- package/src/runtime/scaffold/artifacts.js +217 -0
- package/src/runtime/scaffold/fingerprint.js +56 -0
- package/src/runtime/scaffold/generate.js +911 -0
- package/src/runtime/scaffold/languages.js +245 -0
- package/src/runtime/storage/cli.js +64 -0
- package/src/runtime/storage/engine.js +249 -0
- package/src/runtime/storage/registry.js +130 -0
- package/src/runtime/storage/roots.js +63 -0
- package/src/runtime/storage/rules.js +282 -0
- package/src/runtime/storage/upkeep.js +243 -0
- package/src/runtime/storage/work-status.js +326 -0
- package/src/runtime/task-attempts.js +415 -0
- package/src/runtime/task-classifier.js +550 -0
- package/src/runtime/task-scope.js +130 -0
- package/src/runtime/task-state.js +176 -0
- package/src/runtime/trace/indicators.js +235 -0
- package/src/runtime/trace/ledger.js +626 -0
- package/src/runtime/trace/render.js +76 -0
- package/src/runtime/trace/views.js +459 -0
- package/src/runtime/verification/contract-runner.js +341 -0
- package/src/runtime/verification/engine.js +1461 -0
- package/src/runtime/verification/registry.js +130 -0
- package/src/runtime/verification/scope-bound.js +418 -0
- package/src/runtime/verification/test-selection.js +172 -0
- package/src/runtime/workflow-engine.js +588 -0
- package/src/runtime/workflow-orchestrator.js +953 -0
- package/src/runtime/worktree.js +340 -0
- package/src/state/index.js +193 -0
- package/src/state/lockfile.js +136 -0
- package/src/state/run-lock.js +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Registry loader and validator. Registry files are plain JSON (core/registry/*.json).
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { validatePathsSection } = require('../helper/harness-paths.js');
|
|
9
|
+
|
|
10
|
+
const REGISTRY_FILES = Object.freeze({
|
|
11
|
+
harnesses: 'harnesses.json',
|
|
12
|
+
assets: 'assets.json',
|
|
13
|
+
mcp: 'mcp.json',
|
|
14
|
+
lifecycle: 'lifecycle.json',
|
|
15
|
+
contracts: 'contracts.json',
|
|
16
|
+
externalTools: 'external-tools.json',
|
|
17
|
+
models: 'models.json',
|
|
18
|
+
});
|
|
19
|
+
const CAPABILITY_STATUS = new Set(['supported', 'different', 'unavailable']);
|
|
20
|
+
/** The orchestration block's own status vocabulary (074 IC-026): whether a harness exposes a
|
|
21
|
+
* primitive, kept apart from CAPABILITY_STATUS because "unknown" is a recorded state here, not a gap. */
|
|
22
|
+
const ORCHESTRATION_STATUS = new Set(['supported', 'unsupported', 'unknown']);
|
|
23
|
+
const ORCHESTRATION_OPS = ['spawn', 'wait', 'status', 'cancel', 'resume'];
|
|
24
|
+
const ORCHESTRATION_FALLBACK = new Set(['sequential', 'manual']);
|
|
25
|
+
// 'core' = must be supported/different (never unavailable) on every currently-integrated harness;
|
|
26
|
+
// 'enhanced' = best-effort, wired wherever a harness's native surface allows. Optional field —
|
|
27
|
+
// a policy without one is neither tier, and test/guards/registry.test.js's core-tier check simply
|
|
28
|
+
// skips it. See agent-docs research backing this (LSP capability negotiation, k8s feature gates,
|
|
29
|
+
// progressive enhancement): reducing every policy to the true cross-harness intersection was
|
|
30
|
+
// rejected as a design in favor of this, since it deletes real working capability instead of just
|
|
31
|
+
// recording where it doesn't reach.
|
|
32
|
+
const POLICY_TIERS = new Set(['core', 'enhanced']);
|
|
33
|
+
const SCOPES = new Set(['project', 'user']);
|
|
34
|
+
const MCP_TRANSPORTS = new Set(['stdio', 'http', 'sse']);
|
|
35
|
+
const EXTERNAL_TOOL_IDS = new Set(['rtk', 'graphify', 'semble', 'mermaid-cli', 'plantuml']);
|
|
36
|
+
const EXTERNAL_TOOL_ACTIONS = new Set(['install', 'update', 'uninstall']);
|
|
37
|
+
const MODEL_KINDS = new Set(['hosted', 'local']);
|
|
38
|
+
const MODEL_SLOT_IDS = new Set(['dense', 'rerank']);
|
|
39
|
+
const MCP_STEP_KEYS = new Set(['skill', 'file', 'expectedWhen']);
|
|
40
|
+
const MCP_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
41
|
+
/** How a capability provider answers: from the current files or commands (`live`), or from a built
|
|
42
|
+
* artifact that can lag the source (`index`). Required on every provider (068). */
|
|
43
|
+
const PROVIDER_SOURCE_MODES = Object.freeze(['live', 'index']);
|
|
44
|
+
|
|
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
|
+
const seen = new Set();
|
|
49
|
+
for (const item of items || []) {
|
|
50
|
+
if (!object(item) || typeof item.id !== 'string' || !item.id) continue;
|
|
51
|
+
if (seen.has(item.id)) issue(errors, label, `duplicate id '${item.id}'`);
|
|
52
|
+
seen.add(item.id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseRegistryFile(file , fsImpl = fs) {
|
|
57
|
+
let text;
|
|
58
|
+
try { text = fsImpl.readFileSync(file, 'utf8'); } catch (error ) {
|
|
59
|
+
throw new Error(`Could not read registry file '${file}': ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
try { return JSON.parse(text); } catch (error ) {
|
|
62
|
+
throw new Error(`Registry file '${file}' is not valid JSON: ${error.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function registryDir(repoRoot ) { return path.join(path.resolve(repoRoot), 'core', 'registry'); }
|
|
67
|
+
|
|
68
|
+
function loadRegistry({ repoRoot, dir, fsImpl = fs } = {}) {
|
|
69
|
+
if (!repoRoot && !dir) throw new Error('loadRegistry requires repoRoot or dir');
|
|
70
|
+
const base = dir ? path.resolve(dir) : registryDir(repoRoot );
|
|
71
|
+
const loaded = {};
|
|
72
|
+
for (const [name, fileName] of Object.entries(REGISTRY_FILES)) {
|
|
73
|
+
loaded[name] = parseRegistryFile(path.join(base, fileName), fsImpl);
|
|
74
|
+
}
|
|
75
|
+
// capabilities.json is read by the capability router, not a REGISTRY_FILES entry; the registry
|
|
76
|
+
// only validates it (provider `mcp` descriptors) when the file is present beside the others.
|
|
77
|
+
const capabilitiesFile = path.join(base, 'capabilities.json');
|
|
78
|
+
const capabilities = fsImpl.existsSync(capabilitiesFile) ? parseRegistryFile(capabilitiesFile, fsImpl).capabilities : undefined;
|
|
79
|
+
const registry = {
|
|
80
|
+
directory: base,
|
|
81
|
+
repoRoot: repoRoot ? path.resolve(repoRoot) : path.resolve(base, '..', '..'),
|
|
82
|
+
harnesses: loaded.harnesses.harnesses,
|
|
83
|
+
assets: loaded.assets.assets,
|
|
84
|
+
mcp: loaded.mcp.servers,
|
|
85
|
+
lifecycle: loaded.lifecycle.policies,
|
|
86
|
+
contracts: loaded.contracts.contracts,
|
|
87
|
+
externalTools: loaded.externalTools.tools,
|
|
88
|
+
modelProviders: loaded.models.providers,
|
|
89
|
+
modelRoles: loaded.models.roles,
|
|
90
|
+
retrievalSlots: Array.isArray(loaded.models.slots) ? loaded.models.slots : [],
|
|
91
|
+
dispatchTiers: loaded.models.dispatchTiers,
|
|
92
|
+
capabilities,
|
|
93
|
+
versions: Object.fromEntries((Object.entries(loaded) ).map(([name, value]) => [name, value.version])),
|
|
94
|
+
};
|
|
95
|
+
const validation = validateRegistry(registry, { repoRoot: registry.repoRoot, fsImpl });
|
|
96
|
+
if (!validation.ok) {
|
|
97
|
+
const error = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
|
|
98
|
+
error.validation = validation;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({ ...registry, validation });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const CONTRACT_COMPLETENESS = new Set(['verified', 'lower-bound']);
|
|
105
|
+
|
|
106
|
+
/** Validates core/registry/contracts.json — what each harness ACCEPTS (legal frontmatter fields,
|
|
107
|
+
* legal hook event names), kept separate from harnesses.json's what-DoFlow-SUPPORTS. The split is
|
|
108
|
+
* what keeps the registry-truth guard from validating the registry against itself.
|
|
109
|
+
*
|
|
110
|
+
* `evidence` is required for the same reason capabilities require it: a contract claim with no
|
|
111
|
+
* citation is folklore, and these lists are copied from vendor documentation that moves. */
|
|
112
|
+
function validateContract(value , location , errors , harnessIds ) {
|
|
113
|
+
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
114
|
+
if (typeof value.harness !== 'string' || !harnessIds.has(value.harness)) {
|
|
115
|
+
issue(errors, location, `harness must be one of: ${[...harnessIds].join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
if (!CONTRACT_COMPLETENESS.has(value.completeness)) {
|
|
118
|
+
issue(errors, location, 'completeness must be verified or lower-bound');
|
|
119
|
+
}
|
|
120
|
+
// skillFields/agentFields are optional: shared skills and agent specs are byte-identical across
|
|
121
|
+
// harnesses, so a per-harness field list is necessarily a subset of the union and cannot change
|
|
122
|
+
// any guard outcome. Declaring one anyway would restate an unverified claim as fact — the exact
|
|
123
|
+
// shape of inert declaration these guards exist to remove. hookEvents is required because those
|
|
124
|
+
// genuinely differ per harness and G5 reads them.
|
|
125
|
+
for (const field of ['skillFields', 'agentFields']) {
|
|
126
|
+
if (value[field] === undefined) continue;
|
|
127
|
+
const list = value[field];
|
|
128
|
+
if (!Array.isArray(list) || list.length === 0 || list.some((item) => typeof item !== 'string' || !item)) {
|
|
129
|
+
issue(errors, `${location}.${field}`, 'must be a non-empty array of non-empty strings when present');
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (new Set(list).size !== list.length) issue(errors, `${location}.${field}`, 'must not contain duplicates');
|
|
133
|
+
}
|
|
134
|
+
// 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.
|
|
140
|
+
const events = value.hookEvents;
|
|
141
|
+
const noted = typeof value.note === 'string' && value.note.trim().length > 0;
|
|
142
|
+
if (!Array.isArray(events) || events.some((item) => typeof item !== 'string' || !item)) {
|
|
143
|
+
issue(errors, `${location}.hookEvents`, 'must be an array of non-empty strings');
|
|
144
|
+
} else if (events.length === 0 && !noted) {
|
|
145
|
+
issue(errors, `${location}.hookEvents`, 'may only be empty when `note` explains why no hook is wired');
|
|
146
|
+
} else if (new Set(events).size !== events.length) {
|
|
147
|
+
issue(errors, `${location}.hookEvents`, 'must not contain duplicates');
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
150
|
+
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
151
|
+
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A harness MCP tool-name template: contains {server} and {tool}, and no other brace text. */
|
|
156
|
+
function validToolNameTemplate(value ) {
|
|
157
|
+
return typeof value === 'string' && value.includes('{server}') && value.includes('{tool}')
|
|
158
|
+
&& !value.replace(/\{server\}|\{tool\}/g, '').match(/[{}]/);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateCapability(value , location , errors ) {
|
|
162
|
+
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
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 ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
165
|
+
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
166
|
+
}
|
|
167
|
+
if (typeof value.verification !== 'string' || !value.verification.trim()) issue(errors, location, 'requires a verification instruction');
|
|
168
|
+
if (value.toolName !== undefined && !validToolNameTemplate(value.toolName)) {
|
|
169
|
+
issue(errors, location, 'toolName must be a string with exactly the placeholders {server} and {tool} and no other braces');
|
|
170
|
+
}
|
|
171
|
+
if (value.prerequisites !== undefined && (!Array.isArray(value.prerequisites) || value.prerequisites.some((item ) => typeof item !== 'string' || !item))) {
|
|
172
|
+
issue(errors, location, 'prerequisites must be an array of non-empty strings');
|
|
173
|
+
}
|
|
174
|
+
// Optional per-event detail, reusing the same three-value status vocabulary. Additive on
|
|
175
|
+
// purpose: a capability without it keeps today's coarse behaviour, so harnesses migrate one at
|
|
176
|
+
// a time instead of all three needing per-event data before any of them can have it.
|
|
177
|
+
if (value.events !== undefined) {
|
|
178
|
+
if (!object(value.events) || Object.keys(value.events).length === 0) {
|
|
179
|
+
issue(errors, `${location}.events`, 'must be a non-empty object when present');
|
|
180
|
+
} else {
|
|
181
|
+
for (const [event, detail] of Object.entries(value.events) ) {
|
|
182
|
+
if (!/^[A-Z][A-Za-z]*$/.test(event)) issue(errors, `${location}.events`, `invalid event name '${event}'`);
|
|
183
|
+
if (!object(detail) || !CAPABILITY_STATUS.has(detail.status)) {
|
|
184
|
+
issue(errors, `${location}.events.${event}`, 'status must be supported, different, or unavailable');
|
|
185
|
+
}
|
|
186
|
+
if (detail?.note !== undefined && (typeof detail.note !== 'string' || !detail.note.trim())) {
|
|
187
|
+
issue(errors, `${location}.events.${event}`, 'note must be a non-empty string when present');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateSource(repoRoot , source , location , errors , fsImpl , expected = 'any') {
|
|
195
|
+
if (typeof source !== 'string' || !source.trim()) { issue(errors, location, 'requires a repository-relative source'); return; }
|
|
196
|
+
const root = path.resolve(repoRoot);
|
|
197
|
+
const resolved = path.resolve(root, source);
|
|
198
|
+
if (!resolved.startsWith(`${root}${path.sep}`) || !fsImpl.existsSync(resolved)) { issue(errors, location, `source does not exist inside repository: ${source}`); return; }
|
|
199
|
+
if (expected === 'file' && !fsImpl.statSync(resolved).isFile()) issue(errors, location, `source must be a file: ${source}`);
|
|
200
|
+
if (expected === 'directory' && !fsImpl.statSync(resolved).isDirectory()) issue(errors, location, `source must be a directory: ${source}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function validateNativeProjection(projection , harness , { repoRoot, fsImpl } , errors ) {
|
|
204
|
+
const at = `harness '${harness.id}' nativeProjection`;
|
|
205
|
+
if (!object(projection)) { issue(errors, at, 'must be an object'); return; }
|
|
206
|
+
const config = projection.config;
|
|
207
|
+
if (!object(config)) issue(errors, at, 'requires config declaration');
|
|
208
|
+
else {
|
|
209
|
+
validateSource(repoRoot, config.source, `${at} config`, errors, fsImpl, 'file');
|
|
210
|
+
if (!Array.isArray(config.resources) || !config.resources.length) issue(errors, `${at} config`, 'requires non-empty resources');
|
|
211
|
+
for (const resource of config.resources || []) {
|
|
212
|
+
if (!object(resource) || resource.kind !== 'configuration-entry' || typeof resource.identity !== 'string' || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/.test(resource.identity) || !Object.hasOwn(resource, 'value')) {
|
|
213
|
+
issue(errors, `${at} config`, 'resources require kind configuration-entry, dotted identity, and value');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const mcp = projection.mcp;
|
|
218
|
+
if (!object(mcp) || mcp.catalog !== 'registry.mcp' || !['optional', 'required'].includes(mcp.selection)) issue(errors, `${at} mcp`, 'requires catalog registry.mcp and optional/required selection');
|
|
219
|
+
const agents = projection.agents;
|
|
220
|
+
if (!object(agents)) issue(errors, `${at} agents`, 'requires agents declaration');
|
|
221
|
+
else validateSource(repoRoot, agents.source, `${at} agents`, errors, fsImpl, 'directory');
|
|
222
|
+
const hooks = projection.hooks;
|
|
223
|
+
if (!object(hooks)) issue(errors, `${at} hooks`, 'requires hooks declaration');
|
|
224
|
+
else {
|
|
225
|
+
validateSource(repoRoot, hooks.configSource, `${at} hooks`, errors, fsImpl, 'file');
|
|
226
|
+
validateSource(repoRoot, hooks.scriptsSource, `${at} hooks`, errors, fsImpl, 'directory');
|
|
227
|
+
if (typeof hooks.trusted !== 'boolean') issue(errors, `${at} hooks`, 'trusted must be boolean');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validateCommand(command , location , errors ) {
|
|
232
|
+
if (!Array.isArray(command) || command.length === 0
|
|
233
|
+
|| command.some((argument) => typeof argument !== 'string' || !argument.trim())) {
|
|
234
|
+
issue(errors, location, 'command must be a non-empty array of non-empty strings');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function validateExternalTool(value , location , errors ) {
|
|
239
|
+
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
240
|
+
const allowed = new Set(['id', 'displayName', 'officialEvidence', 'status', 'install', 'update', 'uninstall', 'prerequisites']);
|
|
241
|
+
for (const key of Object.keys(value)) {
|
|
242
|
+
if (!allowed.has(key)) issue(errors, location, `unsupported field '${key}'`);
|
|
243
|
+
}
|
|
244
|
+
if (typeof value.id !== 'string' || !EXTERNAL_TOOL_IDS.has(value.id)) {
|
|
245
|
+
issue(errors, location, `id must be one of: ${[...EXTERNAL_TOOL_IDS].join(', ')}`);
|
|
246
|
+
}
|
|
247
|
+
if (typeof value.displayName !== 'string' || !value.displayName.trim()) issue(errors, location, 'requires displayName');
|
|
248
|
+
if (!Array.isArray(value.officialEvidence) || value.officialEvidence.length === 0
|
|
249
|
+
|| value.officialEvidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
250
|
+
issue(errors, location, 'must include one or more HTTPS officialEvidence URLs');
|
|
251
|
+
}
|
|
252
|
+
if (value.prerequisites !== undefined && (!Array.isArray(value.prerequisites)
|
|
253
|
+
|| value.prerequisites.some((item ) => typeof item !== 'string' || !item.trim())
|
|
254
|
+
|| new Set(value.prerequisites).size !== value.prerequisites.length)) {
|
|
255
|
+
issue(errors, location, 'prerequisites must be an array of unique non-empty strings when present');
|
|
256
|
+
}
|
|
257
|
+
if (!object(value.status) || !Array.isArray(value.status.commands) || value.status.commands.length === 0) {
|
|
258
|
+
issue(errors, location, 'requires status.commands as a non-empty array of command arrays');
|
|
259
|
+
} else {
|
|
260
|
+
const allowedStatus = new Set(['commands']);
|
|
261
|
+
for (const key of Object.keys(value.status)) {
|
|
262
|
+
if (!allowedStatus.has(key)) issue(errors, `${location}.status`, `unsupported field '${key}'`);
|
|
263
|
+
}
|
|
264
|
+
value.status.commands.forEach((command , index ) => validateCommand(command, `${location}.status.commands[${index}]`, errors));
|
|
265
|
+
}
|
|
266
|
+
for (const action of EXTERNAL_TOOL_ACTIONS) {
|
|
267
|
+
if (value[action] === undefined) continue;
|
|
268
|
+
if (!object(value[action])) { issue(errors, `${location}.${action}`, 'must be a command contract object'); continue; }
|
|
269
|
+
for (const key of Object.keys(value[action])) {
|
|
270
|
+
if (key !== 'command') issue(errors, `${location}.${action}`, `unsupported field '${key}'`);
|
|
271
|
+
}
|
|
272
|
+
validateCommand(value[action].command, `${location}.${action}.command`, errors);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Validates core/registry/models.json — the provider × role matrix the orchestrator's model
|
|
277
|
+
* router will consume. Shell scope: providers are declared with identity, kind, and
|
|
278
|
+
* evidence (a capability claim without a citation is folklore); roles name routing preferences
|
|
279
|
+
* without binding to concrete model IDs, which stay runtime/user choices resolved at run time. */
|
|
280
|
+
function validateModelProvider(value , location , errors ) {
|
|
281
|
+
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
282
|
+
if (typeof value.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.id)) issue(errors, location, 'id must be a lowercase identifier');
|
|
283
|
+
if (typeof value.displayName !== 'string' || !value.displayName.trim()) issue(errors, location, 'requires displayName');
|
|
284
|
+
if (!MODEL_KINDS.has(value.kind)) issue(errors, location, `kind must be one of: ${[...MODEL_KINDS].join(', ')}`);
|
|
285
|
+
if (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
286
|
+
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
287
|
+
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function validateModelRole(value , location , errors ) {
|
|
292
|
+
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
293
|
+
if (typeof value.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.id)) issue(errors, location, 'id must be a lowercase identifier');
|
|
294
|
+
for (const field of ['prefer', 'fallback', 'require']) {
|
|
295
|
+
if (value[field] !== undefined && (typeof value[field] !== 'string' || !value[field].trim())) {
|
|
296
|
+
issue(errors, location, `${field} must be a non-empty string when present`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Validates an optional retrieval slot in core/registry/models.json — a dense-embedding or
|
|
302
|
+
* cross-encoder rerank stage bound to one declared provider and the concrete model that provider
|
|
303
|
+
* serves. Absent slots are the shipped posture and keep retrieval lexical-only; malformed ones
|
|
304
|
+
* must fail the load loudly, because a typo reading as "no dense provider" would silently skip
|
|
305
|
+
* the feature instead of surfacing it. `enabled` is therefore required rather than defaulted:
|
|
306
|
+
* a misspelled flag defaulting to off is the same PASS-over-no-evidence defect in miniature. */
|
|
307
|
+
function validateModelSlot(value , location , errors , providerIds ) {
|
|
308
|
+
if (!object(value)) { issue(errors, location, 'must be an object'); return; }
|
|
309
|
+
const allowed = new Set(['id', 'provider', 'model', 'enabled', 'evidence']);
|
|
310
|
+
for (const key of Object.keys(value)) {
|
|
311
|
+
if (!allowed.has(key)) issue(errors, location, `unsupported field '${key}'`);
|
|
312
|
+
}
|
|
313
|
+
if (!MODEL_SLOT_IDS.has(value.id)) issue(errors, location, `id must be one of: ${[...MODEL_SLOT_IDS].join(', ')}`);
|
|
314
|
+
if (typeof value.provider !== 'string' || !value.provider.trim()) issue(errors, location, 'requires provider');
|
|
315
|
+
else if (providerIds && !providerIds.has(value.provider)) issue(errors, location, `references unknown model provider '${value.provider}'`);
|
|
316
|
+
if (typeof value.model !== 'string' || !value.model.trim()) issue(errors, location, 'requires model');
|
|
317
|
+
if (typeof value.enabled !== 'boolean') issue(errors, location, 'enabled must be a boolean');
|
|
318
|
+
if (value.evidence !== undefined && (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
319
|
+
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url)))) {
|
|
320
|
+
issue(errors, location, 'must include one or more HTTPS evidence URLs when evidence is present');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** `trigger` and `steps` on an MCP server (062): when a stage should reach for the server, and
|
|
325
|
+
* which skill files name it. Registry-only; adapters pick fields and never render these keys. */
|
|
326
|
+
function validateMcpStageUse(server , at , errors ) {
|
|
327
|
+
if (typeof server.trigger !== 'string' || server.trigger.length < 20 || server.trigger.length > 300 || /[\r\n]/.test(server.trigger)) {
|
|
328
|
+
issue(errors, at, 'trigger must be one line of 20 to 300 characters');
|
|
329
|
+
}
|
|
330
|
+
if (!Array.isArray(server.steps)) { issue(errors, at, 'steps must be an array'); return; }
|
|
331
|
+
const seen = new Set();
|
|
332
|
+
server.steps.forEach((step , index ) => {
|
|
333
|
+
const where = `steps[${index}]`;
|
|
334
|
+
if (!object(step)) { issue(errors, at, `${where} must be an object`); return; }
|
|
335
|
+
for (const key of Object.keys(step)) if (!MCP_STEP_KEYS.has(key)) issue(errors, at, `${where} has unknown key '${key}'`);
|
|
336
|
+
if (typeof step.skill !== 'string' || !/^[a-z][a-z0-9-]*$/.test(step.skill)) issue(errors, at, `${where}.skill must be a skill directory name`);
|
|
337
|
+
if (typeof step.file !== 'string' || !step.file.trim() || path.isAbsolute(step.file) || step.file.split(/[\\/]/).includes('..')) {
|
|
338
|
+
issue(errors, at, `${where}.file must be a path relative to the skill directory`);
|
|
339
|
+
}
|
|
340
|
+
if (step.expectedWhen !== undefined && step.expectedWhen !== 'research-request') issue(errors, at, `${where}.expectedWhen must be 'research-request' when present`);
|
|
341
|
+
const key = `${step.skill}\u0000${step.file}`;
|
|
342
|
+
if (seen.has(key)) issue(errors, at, `${where} duplicates skill '${step.skill}' and file '${step.file}'`);
|
|
343
|
+
seen.add(key);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Provider checks for capabilities.json: required `sourceMode` (068) and optional `mcp: {server, tool}` (062). */
|
|
348
|
+
function validateCapabilityProviders(capabilities , errors ) {
|
|
349
|
+
if (capabilities === undefined) return;
|
|
350
|
+
if (!object(capabilities)) { issue(errors, 'capabilities', 'must be an object'); return; }
|
|
351
|
+
for (const [capabilityId, capability] of Object.entries(capabilities) ) {
|
|
352
|
+
for (const provider of object(capability) && Array.isArray(capability.providers) ? capability.providers : []) {
|
|
353
|
+
if (!object(provider)) continue;
|
|
354
|
+
const at = `capability '${capabilityId}' provider '${provider.id ?? '?'}'`;
|
|
355
|
+
if (!PROVIDER_SOURCE_MODES.includes(provider.sourceMode)) {
|
|
356
|
+
issue(errors, at, `sourceMode is required and must be one of: ${PROVIDER_SOURCE_MODES.join(', ')} (got ${provider.sourceMode === undefined ? 'nothing' : JSON.stringify(provider.sourceMode)})`);
|
|
357
|
+
}
|
|
358
|
+
if (provider.mcp === undefined) continue;
|
|
359
|
+
if (!object(provider.mcp)) { issue(errors, at, 'mcp must be an object with server and tool'); continue; }
|
|
360
|
+
for (const field of ['server', 'tool']) {
|
|
361
|
+
if (typeof provider.mcp[field] !== 'string' || !MCP_NAME.test(provider.mcp[field])) issue(errors, at, `mcp.${field} must match ${MCP_NAME}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** `dispatchTiers` in models.json (062): the tier vocabulary and the per-owner default a brief names. */
|
|
368
|
+
function validateDispatchTiers(value , errors ) {
|
|
369
|
+
if (value === undefined) return;
|
|
370
|
+
const at = 'models.dispatchTiers';
|
|
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 ) => typeof tier === 'string' && tier) ? value.tiers : null;
|
|
373
|
+
if (!tiers) { issue(errors, at, 'tiers must be a non-empty array of strings'); return; }
|
|
374
|
+
if (new Set(tiers).size !== tiers.length) issue(errors, at, 'tiers must be unique');
|
|
375
|
+
if (!tiers.includes(value.default)) issue(errors, at, `default must be one of: ${tiers.join(', ')}`);
|
|
376
|
+
if (!object(value.byOwner)) { issue(errors, at, 'byOwner must be an object'); return; }
|
|
377
|
+
for (const [owner, tier] of Object.entries(value.byOwner)) {
|
|
378
|
+
if (!tiers.includes(tier)) issue(errors, at, `byOwner '${owner}' must be one of: ${tiers.join(', ')}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Validates one harness's `orchestration` block (074 IC-026): the five subagent primitives a
|
|
383
|
+
* harness exposes (spawn, wait, status, cancel, resume), each with evidence and a re-check
|
|
384
|
+
* instruction. It reads no other harness and no model name; a value is declared, never inferred. */
|
|
385
|
+
function validateOrchestration(value , location , errors ) {
|
|
386
|
+
if (!object(value)) { issue(errors, location, 'orchestration block is required and must be an object'); return; }
|
|
387
|
+
for (const op of ORCHESTRATION_OPS) {
|
|
388
|
+
if (!Object.hasOwn(value, op)) issue(errors, location, `orchestration is missing operation '${op}'`);
|
|
389
|
+
}
|
|
390
|
+
for (const op of Object.keys(value)) {
|
|
391
|
+
if (!ORCHESTRATION_OPS.includes(op)) issue(errors, location, `orchestration has unknown operation '${op}'`);
|
|
392
|
+
}
|
|
393
|
+
for (const op of ORCHESTRATION_OPS) {
|
|
394
|
+
if (!Object.hasOwn(value, op)) continue;
|
|
395
|
+
const at = `${location} orchestration '${op}'`;
|
|
396
|
+
const entry = value[op];
|
|
397
|
+
if (!object(entry)) { issue(errors, at, 'must be an object'); continue; }
|
|
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 ) => typeof item !== 'string')) {
|
|
400
|
+
issue(errors, at, 'evidence must be an array of strings');
|
|
401
|
+
} else if (entry.evidence.length === 0 && (entry.status === 'supported' || entry.status === 'unsupported')) {
|
|
402
|
+
issue(errors, at, `evidence must not be empty when status is ${entry.status}`);
|
|
403
|
+
}
|
|
404
|
+
if (typeof entry.verification !== 'string' || !entry.verification.trim()) issue(errors, at, 'requires a verification instruction');
|
|
405
|
+
if (typeof entry.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.verifiedOn)) issue(errors, at, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
|
|
406
|
+
if (entry.status !== 'supported' && !ORCHESTRATION_FALLBACK.has(entry.fallback)) {
|
|
407
|
+
issue(errors, at, `requires fallback (sequential or manual) when status is ${entry.status}`);
|
|
408
|
+
}
|
|
409
|
+
if (entry.status === 'unknown' && (typeof entry.reason !== 'string' || !entry.reason.trim())) {
|
|
410
|
+
issue(errors, at, 'requires a reason when status is unknown');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function validateRegistry(registry , { repoRoot, fsImpl = fs } = {}) {
|
|
416
|
+
const errors = [];
|
|
417
|
+
const harnesses = Array.isArray(registry?.harnesses) ? registry.harnesses : null;
|
|
418
|
+
const assets = Array.isArray(registry?.assets) ? registry.assets : null;
|
|
419
|
+
const mcp = Array.isArray(registry?.mcp) ? registry.mcp : null;
|
|
420
|
+
const lifecycle = Array.isArray(registry?.lifecycle) ? registry.lifecycle : null;
|
|
421
|
+
const externalTools = Array.isArray(registry?.externalTools) ? registry.externalTools : null;
|
|
422
|
+
if (!harnesses) issue(errors, 'harnesses', 'must be an array');
|
|
423
|
+
if (!assets) issue(errors, 'assets', 'must be an array');
|
|
424
|
+
if (!mcp) issue(errors, 'mcp', 'must be an array');
|
|
425
|
+
if (!lifecycle) issue(errors, 'lifecycle', 'must be an array');
|
|
426
|
+
if (!externalTools) issue(errors, 'externalTools', 'must be an array');
|
|
427
|
+
const contracts = Array.isArray(registry?.contracts) ? registry.contracts : null;
|
|
428
|
+
if (!contracts) issue(errors, 'contracts', 'must be an array');
|
|
429
|
+
idsUnique(harnesses, 'harnesses', errors); idsUnique(assets, 'assets', errors);
|
|
430
|
+
idsUnique(mcp, 'mcp', errors); idsUnique(lifecycle, 'lifecycle', errors);
|
|
431
|
+
idsUnique(externalTools, 'externalTools', errors);
|
|
432
|
+
|
|
433
|
+
const harnessIds = new Set((harnesses || []).filter(object).map((item ) => item.id));
|
|
434
|
+
for (const harness of harnesses || []) {
|
|
435
|
+
const at = `harness '${harness?.id ?? '?'}'`;
|
|
436
|
+
if (!object(harness)) { issue(errors, 'harnesses', 'entries must be objects'); continue; }
|
|
437
|
+
if (typeof harness.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(harness.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
438
|
+
if (typeof harness.displayName !== 'string' || !harness.displayName.trim()) issue(errors, at, 'requires displayName');
|
|
439
|
+
if (typeof harness.adapter !== 'string' || !harness.adapter.trim()) issue(errors, at, 'requires adapter');
|
|
440
|
+
if (!Array.isArray(harness.scopes) || harness.scopes.length === 0 || harness.scopes.some((scope ) => !SCOPES.has(scope))) issue(errors, at, 'scopes must contain project and/or user');
|
|
441
|
+
if (!object(harness.nativeTargets)) issue(errors, at, 'nativeTargets must be an object');
|
|
442
|
+
// Declared native paths (Stage 3): every surface a harness declares must parse into the
|
|
443
|
+
// minimal {base, segments} shape — unknown keys or malformed rules fail the load loudly so a
|
|
444
|
+
// typo can never silently fall back to an adapter's previous hardcoded literal.
|
|
445
|
+
if (harness.paths !== undefined) {
|
|
446
|
+
errors.push(...validatePathsSection(harness.paths, `${at} paths`));
|
|
447
|
+
}
|
|
448
|
+
if (!object(harness.capabilities) || Object.keys(harness.capabilities || {}).length === 0) issue(errors, at, 'capabilities must be a non-empty object');
|
|
449
|
+
for (const [capability, declaration] of Object.entries(harness.capabilities || {})) {
|
|
450
|
+
if (!/^[a-z][a-z0-9-]*$/.test(capability)) issue(errors, at, `invalid capability '${capability}'`);
|
|
451
|
+
validateCapability(declaration, `${at} capability '${capability}'`, errors);
|
|
452
|
+
}
|
|
453
|
+
validateOrchestration(harness.orchestration, at, errors);
|
|
454
|
+
if (harness.id === 'codex') validateNativeProjection(harness.nativeProjection, harness, { repoRoot, fsImpl }, errors);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const contractHarnesses = new Set();
|
|
458
|
+
for (const contract of contracts || []) {
|
|
459
|
+
validateContract(contract, `contract '${contract?.harness ?? '?'}'`, errors, harnessIds);
|
|
460
|
+
if (object(contract) && typeof contract.harness === 'string') {
|
|
461
|
+
if (contractHarnesses.has(contract.harness)) issue(errors, 'contracts', `duplicate contract for '${contract.harness}'`);
|
|
462
|
+
contractHarnesses.add(contract.harness);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Every harness needs a contract: without one there is nothing to validate its shipped
|
|
466
|
+
// frontmatter and hook events against, so drift in that harness would go unnoticed.
|
|
467
|
+
for (const id of harnessIds) {
|
|
468
|
+
if (!contractHarnesses.has(id)) issue(errors, 'contracts', `no contract declared for harness '${id}'`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const asset of assets || []) {
|
|
472
|
+
const at = `asset '${asset?.id ?? '?'}'`;
|
|
473
|
+
if (!object(asset)) { issue(errors, 'assets', 'entries must be objects'); continue; }
|
|
474
|
+
for (const field of ['id', 'kind', 'source', 'ownership']) if (typeof asset[field] !== 'string' || !asset[field].trim()) issue(errors, at, `requires ${field}`);
|
|
475
|
+
if (typeof asset.source === 'string' && repoRoot) {
|
|
476
|
+
const source = path.resolve(repoRoot, asset.source);
|
|
477
|
+
if (!source.startsWith(`${path.resolve(repoRoot)}${path.sep}`) || !fsImpl.existsSync(source)) issue(errors, at, `source does not exist inside repository: ${asset.source}`);
|
|
478
|
+
}
|
|
479
|
+
if (!Array.isArray(asset.appliesTo) || asset.appliesTo.length === 0) issue(errors, at, 'appliesTo must be a non-empty array');
|
|
480
|
+
if (!object(asset.projection)) issue(errors, at, 'projection must be an object');
|
|
481
|
+
for (const harnessId of asset.appliesTo || []) {
|
|
482
|
+
if (!harnessIds.has(harnessId)) issue(errors, at, `references unknown harness '${harnessId}'`);
|
|
483
|
+
const projection = asset.projection?.[harnessId];
|
|
484
|
+
if (!object(projection) || typeof projection.renderer !== 'string' || !projection.renderer.trim() || typeof projection.capability !== 'string' || !projection.capability.trim()) {
|
|
485
|
+
issue(errors, at, `requires renderer and capability projection for '${harnessId}'`); continue;
|
|
486
|
+
}
|
|
487
|
+
const harness = (harnesses || []).find((item ) => item?.id === harnessId);
|
|
488
|
+
const capability = harness?.capabilities?.[projection.capability];
|
|
489
|
+
if (!capability) issue(errors, at, `projection '${harnessId}' references unknown capability '${projection.capability}'`);
|
|
490
|
+
else if (capability.status === 'unavailable') issue(errors, at, `projection '${harnessId}' cannot render unavailable capability '${projection.capability}'`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
for (const server of mcp || []) {
|
|
495
|
+
const at = `MCP server '${server?.id ?? '?'}'`;
|
|
496
|
+
if (!object(server)) { issue(errors, 'mcp', 'entries must be objects'); continue; }
|
|
497
|
+
if (typeof server.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(server.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
498
|
+
if (!MCP_TRANSPORTS.has(server.transport)) issue(errors, at, 'transport must be stdio, http, or sse');
|
|
499
|
+
if (server.transport === 'stdio' && (typeof server.command !== 'string' || !server.command.trim())) issue(errors, at, 'stdio transport requires command');
|
|
500
|
+
if (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg ) => typeof arg !== 'string'))) issue(errors, at, 'args must be an array of strings');
|
|
501
|
+
if (!['optional', 'required'].includes(server.selection)) issue(errors, at, 'selection must be optional or required');
|
|
502
|
+
validateMcpStageUse(server, at, errors);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
validateCapabilityProviders(registry?.capabilities, errors);
|
|
506
|
+
validateDispatchTiers(registry?.dispatchTiers, errors);
|
|
507
|
+
|
|
508
|
+
for (const policy of lifecycle || []) {
|
|
509
|
+
const at = `lifecycle policy '${policy?.id ?? '?'}'`;
|
|
510
|
+
if (!object(policy)) { issue(errors, 'lifecycle', 'entries must be objects'); continue; }
|
|
511
|
+
if (typeof policy.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(policy.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
512
|
+
if (typeof policy.intent !== 'string' || !policy.intent.trim()) issue(errors, at, 'requires intent');
|
|
513
|
+
if (policy.tier !== undefined && !POLICY_TIERS.has(policy.tier)) issue(errors, at, `tier must be one of ${[...POLICY_TIERS].join(', ')}`);
|
|
514
|
+
if (!Array.isArray(policy.requires) || policy.requires.some((capability ) => typeof capability !== 'string')) issue(errors, at, 'requires must be an array of capability names');
|
|
515
|
+
if (!object(policy.mappings)) { issue(errors, at, 'mappings must be an object'); continue; }
|
|
516
|
+
for (const [harnessId, mapping] of Object.entries(policy.mappings) ) {
|
|
517
|
+
if (!harnessIds.has(harnessId)) { issue(errors, at, `mapping references unknown harness '${harnessId}'`); continue; }
|
|
518
|
+
if (!object(mapping) || !CAPABILITY_STATUS.has(mapping.status)) { issue(errors, at, `mapping '${harnessId}' needs a valid status`); continue; }
|
|
519
|
+
if (mapping.status !== 'unavailable' && (typeof mapping.event !== 'string' || !mapping.event.trim())) issue(errors, at, `mapping '${harnessId}' requires event for a renderable status`);
|
|
520
|
+
if (mapping.status === 'unavailable' && typeof mapping.fallback !== 'string') issue(errors, at, `unavailable mapping '${harnessId}' requires fallback guidance`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const tool of externalTools || []) validateExternalTool(tool, `external tool '${tool?.id ?? '?'}'`, errors);
|
|
524
|
+
|
|
525
|
+
const modelProviders = Array.isArray(registry?.modelProviders) ? registry.modelProviders : null;
|
|
526
|
+
const modelRoles = Array.isArray(registry?.modelRoles) ? registry.modelRoles : null;
|
|
527
|
+
if (!modelProviders) issue(errors, 'models.providers', 'must be an array');
|
|
528
|
+
if (!modelRoles) issue(errors, 'models.roles', 'must be an array');
|
|
529
|
+
idsUnique(modelProviders, 'models.providers', errors);
|
|
530
|
+
idsUnique(modelRoles, 'models.roles', errors);
|
|
531
|
+
for (const provider of modelProviders || []) validateModelProvider(provider, `model provider '${provider?.id ?? '?'}'`, errors);
|
|
532
|
+
for (const role of modelRoles || []) validateModelRole(role, `model role '${role?.id ?? '?'}'`, errors);
|
|
533
|
+
// Slots are optional: undefined means the registry does not use them at all and routing stays
|
|
534
|
+
// exactly as it was. Anything present-but-wrong (null, a bare object, malformed entries) fails.
|
|
535
|
+
const modelSlots = registry?.retrievalSlots;
|
|
536
|
+
if (modelSlots === undefined) {
|
|
537
|
+
// default posture — no dense/rerank binding declared
|
|
538
|
+
} else if (!Array.isArray(modelSlots)) {
|
|
539
|
+
issue(errors, 'models.slots', 'must be an array when present');
|
|
540
|
+
} else {
|
|
541
|
+
idsUnique(modelSlots, 'models.slots', errors);
|
|
542
|
+
const providerIds = new Set((modelProviders || []).filter(object).map((item ) => item.id));
|
|
543
|
+
for (const slot of modelSlots) validateModelSlot(slot, `model slot '${slot?.id ?? '?'}'`, errors, providerIds);
|
|
544
|
+
}
|
|
545
|
+
return { ok: errors.length === 0, errors };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function harnessFor(registry , id ) {
|
|
549
|
+
const harness = registry.harnesses.find((item ) => item.id === id);
|
|
550
|
+
if (!harness) throw new Error(`Unknown registry harness '${id}'`);
|
|
551
|
+
return harness;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function selectAssets(registry , { harness, capability, kind } = {}) {
|
|
555
|
+
const declaration = harness ? harnessFor(registry, harness) : null;
|
|
556
|
+
return registry.assets.filter((asset ) => {
|
|
557
|
+
if (kind && asset.kind !== kind) return false;
|
|
558
|
+
if (!harness) return true;
|
|
559
|
+
if (!asset.appliesTo.includes(harness)) return false;
|
|
560
|
+
const selectedCapability = asset.projection[harness].capability;
|
|
561
|
+
return (!capability || selectedCapability === capability) && declaration.capabilities[selectedCapability].status !== 'unavailable';
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function selectMcpServers(registry , ids ) {
|
|
566
|
+
if (ids === undefined || ids === null) return [...registry.mcp];
|
|
567
|
+
if (!Array.isArray(ids)) throw new Error('MCP selection must be an array of server ids');
|
|
568
|
+
const wanted = new Set(ids);
|
|
569
|
+
const unknown = [...wanted].filter((id) => !registry.mcp.some((server ) => server.id === id));
|
|
570
|
+
if (unknown.length) throw new Error(`Unknown registry MCP server(s): ${unknown.join(', ')}`);
|
|
571
|
+
return registry.mcp.filter((server ) => wanted.has(server.id));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** True when the harness takes servers from the shared MCP catalog; the lifecycle and the selection
|
|
575
|
+
* resolver read this one predicate. */
|
|
576
|
+
function mcpCapable(registry , harnessId ) {
|
|
577
|
+
return harnessFor(registry, harnessId).capabilities.mcp?.status === 'supported';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Reshape registry MCP server declarations into the {allServers, serverDefs} catalog shape every
|
|
581
|
+
* harness's native MCP reconciliation (Codex's config.toml writer, Claude's ~/.claude.json/
|
|
582
|
+
* .mcp.json writer) expects — the one place this transform is derived, reused rather than
|
|
583
|
+
* re-implemented per consumer. */
|
|
584
|
+
function nativeMcpCatalog(servers = []) {
|
|
585
|
+
const allServers = servers.map((server) => server.id);
|
|
586
|
+
const serverDefs = Object.fromEntries(servers.map((server) => [server.id, {
|
|
587
|
+
command: server.command,
|
|
588
|
+
...(server.args?.length ? { args: server.args } : {}),
|
|
589
|
+
...(server.url ? { url: server.url } : {}),
|
|
590
|
+
}]));
|
|
591
|
+
return { allServers, serverDefs };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function capabilityMapData(registry ) {
|
|
595
|
+
return registry.harnesses.flatMap((harness ) => (Object.entries(harness.capabilities) ).map(([capability, declaration]) => ({
|
|
596
|
+
harness: harness.id,
|
|
597
|
+
displayName: harness.displayName,
|
|
598
|
+
capability,
|
|
599
|
+
status: declaration.status,
|
|
600
|
+
nativeTarget: harness.nativeTargets[capability] ?? null,
|
|
601
|
+
evidence: [...declaration.evidence],
|
|
602
|
+
prerequisites: [...(declaration.prerequisites || [])],
|
|
603
|
+
verification: declaration.verification,
|
|
604
|
+
})));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
module.exports = {
|
|
608
|
+
REGISTRY_FILES, CAPABILITY_STATUS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS,
|
|
609
|
+
parseRegistryFile, registryDir, loadRegistry, validateRegistry,
|
|
610
|
+
harnessFor, selectAssets, selectMcpServers, mcpCapable, nativeMcpCatalog, capabilityMapData,
|
|
611
|
+
};
|