@khoavu882/doflow 2.0.0-beta.4 → 2.0.0
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/README.md +12 -11
- package/bin/doflow.js +21 -11
- package/core/.antigravity-plugin/plugin.json +1 -3
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/claude/settings/settings.json +18 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +57 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -1
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
- package/core/harnesses/kiro/hooks/session-start.sh +12 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
- package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
- package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +72 -80
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +60 -33
- package/core/registry/contribution-points.json +32 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1542 -277
- package/core/registry/lifecycle.json +2837 -37
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +276 -0
- package/core/registry/storage.json +701 -30
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -1
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
- package/core/shared/scripts/doflow/bin/doflow-run +154 -35
- package/core/shared/skills/do/SKILL.md +12 -10
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +41 -144
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +24 -23
- package/core/shared/skills/do-diagnose/SKILL.md +14 -4
- package/core/shared/skills/do-document/SKILL.md +11 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +18 -6
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +8 -9
- package/core/shared/skills/do-flow/references/task_classes.md +2 -2
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +7 -9
- package/core/shared/skills/do-plan/SKILL.md +29 -19
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +226 -113
- package/src/adapters/claude/index.js +39 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +100 -22
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +294 -48
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +38 -13
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +120 -0
- package/src/adapters/pi/index.js +48 -16
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +3 -2
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +21 -2
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +3 -2
- package/src/cli/index.js +338 -33
- package/src/cli/runtime-commands.js +243 -52
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +74 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +434 -86
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +686 -32
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +492 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +514 -0
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +27 -12
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +221 -277
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +100 -0
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +1106 -245
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +41 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +77 -10
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +565 -17
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +277 -22
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +16 -6
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +6 -1
- package/src/runtime/lifecycle/overview.js +24 -1
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +3 -3
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +477 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +127 -16
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +402 -73
- package/src/runtime/retrieval-record.js +293 -0
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +270 -0
- package/src/runtime/roadmap/loop.js +1636 -0
- package/src/runtime/roadmap/merge.js +381 -0
- package/src/runtime/roadmap/rows.js +1113 -0
- package/src/runtime/roadmap/seed.js +951 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +63 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +290 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +47 -11
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +202 -56
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +440 -65
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -9
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +288 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +578 -63
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -5
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -210
- package/src/adapters/gemini/index.js +0 -390
package/src/registry/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const { validatePathsSection } = require('../helper/harness-paths.js');
|
|
9
|
+
const { BUILTIN_WORKER_ID_RE, RESERVED_WORKER_IDS } = require('./worker-ids.js');
|
|
9
10
|
|
|
10
11
|
const REGISTRY_FILES = Object.freeze({
|
|
11
12
|
harnesses: 'harnesses.json',
|
|
@@ -17,10 +18,16 @@ const REGISTRY_FILES = Object.freeze({
|
|
|
17
18
|
models: 'models.json',
|
|
18
19
|
});
|
|
19
20
|
const CAPABILITY_STATUS = new Set(['supported', 'different', 'unavailable']);
|
|
21
|
+
// Kept local to avoid a registry -> adapter import cycle; registry.test.js ties this list to SELECTORS.
|
|
22
|
+
const COPY_TREE_SELECTS = new Set(['guidance-manifest']);
|
|
23
|
+
// Renderers that materialize through the copy-tree engine and so may carry a `select`: the generic one and
|
|
24
|
+
// Copilot's rule renderer, which differs only by its transform and layout.
|
|
25
|
+
const COPY_TREE_SELECT_RENDERERS = new Set(['copy-tree', 'copilot-rule-instructions']);
|
|
20
26
|
/** The orchestration block's own status vocabulary (074 IC-026): whether a harness exposes a
|
|
21
27
|
* primitive, kept apart from CAPABILITY_STATUS because "unknown" is a recorded state here, not a gap. */
|
|
22
28
|
const ORCHESTRATION_STATUS = new Set(['supported', 'unsupported', 'unknown']);
|
|
23
|
-
|
|
29
|
+
// `nest` (118 IC-004): a subagent can start a subagent.
|
|
30
|
+
const ORCHESTRATION_OPS = ['spawn', 'wait', 'status', 'cancel', 'resume', 'nest'];
|
|
24
31
|
const ORCHESTRATION_FALLBACK = new Set(['sequential', 'manual']);
|
|
25
32
|
// 'core' = must be supported/different (never unavailable) on every currently-integrated harness;
|
|
26
33
|
// 'enhanced' = best-effort, wired wherever a harness's native surface allows. Optional field —
|
|
@@ -37,6 +44,7 @@ const EXTERNAL_TOOL_ACTIONS = new Set(['install', 'update', 'uninstall']);
|
|
|
37
44
|
const MODEL_KINDS = new Set(['hosted', 'local']);
|
|
38
45
|
const MODEL_SLOT_IDS = new Set(['dense', 'rerank']);
|
|
39
46
|
const MCP_STEP_KEYS = new Set(['skill', 'file', 'expectedWhen']);
|
|
47
|
+
const MCP_EXPECTED_WHENS = Object.freeze(new Set(['research-request', 'dependency-change', 'manifest-review']));
|
|
40
48
|
const MCP_NAME = /^[A-Za-z0-9_.-]+$/;
|
|
41
49
|
/** How a capability provider answers: from the current files or commands (`live`), or from a built
|
|
42
50
|
* artifact that can lag the source (`index`). Required on every provider (068). */
|
|
@@ -65,7 +73,17 @@ function parseRegistryFile(file , fsImpl = fs) {
|
|
|
65
73
|
|
|
66
74
|
function registryDir(repoRoot ) { return path.join(path.resolve(repoRoot), 'core', 'registry'); }
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
function registrySourceMode(repoRoot , fsImpl = fs) {
|
|
79
|
+
if (typeof repoRoot !== 'string' || !repoRoot.trim()) return 'skip';
|
|
80
|
+
return fsImpl.existsSync(path.join(path.resolve(repoRoot), 'package.json')) ? 'require' : 'skip';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadRegistry({ repoRoot, dir, fsImpl = fs, sources } = {}) {
|
|
84
|
+
if (sources !== undefined && sources !== 'require' && sources !== 'skip') {
|
|
85
|
+
throw new TypeError(`sources must be 'require' or 'skip', got ${JSON.stringify(sources)}`);
|
|
86
|
+
}
|
|
69
87
|
if (!repoRoot && !dir) throw new Error('loadRegistry requires repoRoot or dir');
|
|
70
88
|
const base = dir ? path.resolve(dir) : registryDir(repoRoot );
|
|
71
89
|
const loaded = {};
|
|
@@ -80,19 +98,24 @@ function loadRegistry({ repoRoot, dir, fsImpl = fs }
|
|
|
80
98
|
directory: base,
|
|
81
99
|
repoRoot: repoRoot ? path.resolve(repoRoot) : path.resolve(base, '..', '..'),
|
|
82
100
|
harnesses: loaded.harnesses.harnesses,
|
|
101
|
+
// Optional in the file; an absent list is an empty one. Anything present is validated as written.
|
|
102
|
+
retired: loaded.harnesses.retired === undefined ? [] : loaded.harnesses.retired,
|
|
83
103
|
assets: loaded.assets.assets,
|
|
84
104
|
mcp: loaded.mcp.servers,
|
|
85
105
|
lifecycle: loaded.lifecycle.policies,
|
|
106
|
+
coverage: loaded.lifecycle.coverage ?? null,
|
|
86
107
|
contracts: loaded.contracts.contracts,
|
|
87
108
|
externalTools: loaded.externalTools.tools,
|
|
88
109
|
modelProviders: loaded.models.providers,
|
|
89
110
|
modelRoles: loaded.models.roles,
|
|
90
|
-
|
|
111
|
+
// Absent is empty; anything else is passed on as written so validation can name a non-array.
|
|
112
|
+
retrievalSlots: loaded.models.slots === undefined ? [] : loaded.models.slots,
|
|
91
113
|
dispatchTiers: loaded.models.dispatchTiers,
|
|
92
114
|
capabilities,
|
|
93
115
|
versions: Object.fromEntries((Object.entries(loaded) ).map(([name, value]) => [name, value.version])),
|
|
94
116
|
};
|
|
95
|
-
const
|
|
117
|
+
const sourceMode = sources ?? registrySourceMode(registry.repoRoot, fsImpl);
|
|
118
|
+
const validation = validateRegistry(registry, { repoRoot: registry.repoRoot, fsImpl, sources: sourceMode });
|
|
96
119
|
if (!validation.ok) {
|
|
97
120
|
const error = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
|
|
98
121
|
error.validation = validation;
|
|
@@ -117,11 +140,23 @@ function validateContract(value , location , errors , harnes
|
|
|
117
140
|
if (!CONTRACT_COMPLETENESS.has(value.completeness)) {
|
|
118
141
|
issue(errors, location, 'completeness must be verified or lower-bound');
|
|
119
142
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
143
|
+
// agentFields is optional: shared agent specs are byte-identical across harnesses, so a per-harness
|
|
144
|
+
// field list is necessarily a subset of the union and cannot change any guard outcome. Declaring one
|
|
145
|
+
// anyway would restate an unverified claim as fact — the exact shape of inert declaration these
|
|
146
|
+
// guards exist to remove. skillFields is per-harness since row 131: a projected SKILL.md keeps only
|
|
147
|
+
// the spec keys and its harness's skillFields at the top level, so a list needs its vendor page and
|
|
148
|
+
// the date it was read. hookEvents is required because those genuinely differ per harness and G5 reads them.
|
|
149
|
+
if (value.skillFields !== undefined) {
|
|
150
|
+
if (typeof value.skillFieldsSource !== 'string' || !/^https:\/\//.test(value.skillFieldsSource)) {
|
|
151
|
+
issue(errors, `${location}.skillFieldsSource`, 'must be the HTTPS vendor page skillFields was read from');
|
|
152
|
+
}
|
|
153
|
+
if (typeof value.skillFieldsVerifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value.skillFieldsVerifiedOn)) {
|
|
154
|
+
issue(errors, `${location}.skillFieldsVerifiedOn`, 'must be the YYYY-MM-DD date skillFields was read');
|
|
155
|
+
}
|
|
156
|
+
if (!['ignored', 'warned', 'rejected', 'not-documented'].includes(value.unknownSkillFields)) {
|
|
157
|
+
issue(errors, `${location}.unknownSkillFields`, 'must be ignored, warned, rejected or not-documented');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
125
160
|
for (const field of ['skillFields', 'agentFields']) {
|
|
126
161
|
if (value[field] === undefined) continue;
|
|
127
162
|
const list = value[field];
|
|
@@ -132,11 +167,10 @@ function validateContract(value , location , errors , harnes
|
|
|
132
167
|
if (new Set(list).size !== list.length) issue(errors, `${location}.${field}`, 'must not contain duplicates');
|
|
133
168
|
}
|
|
134
169
|
// An empty hookEvents used to be rejected outright, which assumed every harness wires at least one
|
|
135
|
-
// hook. OpenCode
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
// between "wires nothing, deliberately" and "nobody filled this in" stays visible.
|
|
170
|
+
// hook. OpenCode has no DoFlow hook projection; Pi uses a code extension only for tool_call.
|
|
171
|
+
// Forcing a non-empty list for an unsupported event would declare a hook DoFlow does not install —
|
|
172
|
+
// an inert claim of exactly the kind these guards exist to remove. Empty is therefore allowed, but
|
|
173
|
+
// only with a note, so deliberate gaps stay distinct from missing registry data.
|
|
140
174
|
const events = value.hookEvents;
|
|
141
175
|
const noted = typeof value.note === 'string' && value.note.trim().length > 0;
|
|
142
176
|
if (!Array.isArray(events) || events.some((item) => typeof item !== 'string' || !item)) {
|
|
@@ -191,22 +225,24 @@ function validateCapability(value , location , errors )
|
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
227
|
|
|
194
|
-
function validateSource(repoRoot , source , location , errors , fsImpl , expected = 'any') {
|
|
228
|
+
function validateSource(repoRoot , source , location , errors , fsImpl , expected = 'any', sources = 'require') {
|
|
195
229
|
if (typeof source !== 'string' || !source.trim()) { issue(errors, location, 'requires a repository-relative source'); return; }
|
|
196
230
|
const root = path.resolve(repoRoot);
|
|
197
231
|
const resolved = path.resolve(root, source);
|
|
198
|
-
if (!resolved.startsWith(`${root}${path.sep}`)
|
|
232
|
+
if (!resolved.startsWith(`${root}${path.sep}`)) { issue(errors, location, `source does not exist inside repository: ${source}`); return; }
|
|
233
|
+
if (sources === 'skip') return;
|
|
234
|
+
if (!fsImpl.existsSync(resolved)) { issue(errors, location, `source does not exist inside repository: ${source}`); return; }
|
|
199
235
|
if (expected === 'file' && !fsImpl.statSync(resolved).isFile()) issue(errors, location, `source must be a file: ${source}`);
|
|
200
236
|
if (expected === 'directory' && !fsImpl.statSync(resolved).isDirectory()) issue(errors, location, `source must be a directory: ${source}`);
|
|
201
237
|
}
|
|
202
238
|
|
|
203
|
-
function validateNativeProjection(projection , harness , { repoRoot, fsImpl }
|
|
239
|
+
function validateNativeProjection(projection , harness , { repoRoot, fsImpl, sources } , errors ) {
|
|
204
240
|
const at = `harness '${harness.id}' nativeProjection`;
|
|
205
241
|
if (!object(projection)) { issue(errors, at, 'must be an object'); return; }
|
|
206
242
|
const config = projection.config;
|
|
207
243
|
if (!object(config)) issue(errors, at, 'requires config declaration');
|
|
208
244
|
else {
|
|
209
|
-
validateSource(repoRoot, config.source, `${at} config`, errors, fsImpl, 'file');
|
|
245
|
+
validateSource(repoRoot, config.source, `${at} config`, errors, fsImpl, 'file', sources);
|
|
210
246
|
if (!Array.isArray(config.resources) || !config.resources.length) issue(errors, `${at} config`, 'requires non-empty resources');
|
|
211
247
|
for (const resource of config.resources || []) {
|
|
212
248
|
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')) {
|
|
@@ -218,12 +254,12 @@ function validateNativeProjection(projection , harness , { repoRoot, fsI
|
|
|
218
254
|
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
255
|
const agents = projection.agents;
|
|
220
256
|
if (!object(agents)) issue(errors, `${at} agents`, 'requires agents declaration');
|
|
221
|
-
else validateSource(repoRoot, agents.source, `${at} agents`, errors, fsImpl, 'directory');
|
|
257
|
+
else validateSource(repoRoot, agents.source, `${at} agents`, errors, fsImpl, 'directory', sources);
|
|
222
258
|
const hooks = projection.hooks;
|
|
223
259
|
if (!object(hooks)) issue(errors, `${at} hooks`, 'requires hooks declaration');
|
|
224
260
|
else {
|
|
225
|
-
validateSource(repoRoot, hooks.configSource, `${at} hooks`, errors, fsImpl, 'file');
|
|
226
|
-
validateSource(repoRoot, hooks.scriptsSource, `${at} hooks`, errors, fsImpl, 'directory');
|
|
261
|
+
validateSource(repoRoot, hooks.configSource, `${at} hooks`, errors, fsImpl, 'file', sources);
|
|
262
|
+
validateSource(repoRoot, hooks.scriptsSource, `${at} hooks`, errors, fsImpl, 'directory', sources);
|
|
227
263
|
if (typeof hooks.trusted !== 'boolean') issue(errors, `${at} hooks`, 'trusted must be boolean');
|
|
228
264
|
}
|
|
229
265
|
}
|
|
@@ -282,6 +318,12 @@ function validateModelProvider(value , location , errors )
|
|
|
282
318
|
if (typeof value.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.id)) issue(errors, location, 'id must be a lowercase identifier');
|
|
283
319
|
if (typeof value.displayName !== 'string' || !value.displayName.trim()) issue(errors, location, 'requires displayName');
|
|
284
320
|
if (!MODEL_KINDS.has(value.kind)) issue(errors, location, `kind must be one of: ${[...MODEL_KINDS].join(', ')}`);
|
|
321
|
+
if (!Array.isArray(value.families) || value.families.length === 0
|
|
322
|
+
|| value.families.some((family ) => typeof family !== 'string' || !/^[a-z][a-z0-9-]*$/.test(family))) {
|
|
323
|
+
issue(errors, location, 'families must be a non-empty array of lowercase identifiers');
|
|
324
|
+
} else if (new Set(value.families).size !== value.families.length) {
|
|
325
|
+
issue(errors, location, 'families must not contain duplicate identifiers');
|
|
326
|
+
}
|
|
285
327
|
if (!Array.isArray(value.evidence) || value.evidence.length === 0
|
|
286
328
|
|| value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
|
|
287
329
|
issue(errors, location, 'must include one or more HTTPS evidence URLs');
|
|
@@ -337,8 +379,10 @@ function validateMcpStageUse(server , at , errors ) {
|
|
|
337
379
|
if (typeof step.file !== 'string' || !step.file.trim() || path.isAbsolute(step.file) || step.file.split(/[\\/]/).includes('..')) {
|
|
338
380
|
issue(errors, at, `${where}.file must be a path relative to the skill directory`);
|
|
339
381
|
}
|
|
340
|
-
if (step.expectedWhen !== undefined && step.expectedWhen
|
|
341
|
-
|
|
382
|
+
if (step.expectedWhen !== undefined && !MCP_EXPECTED_WHENS.has(step.expectedWhen)) {
|
|
383
|
+
issue(errors, at, `${where}.expectedWhen must be one of: ${[...MCP_EXPECTED_WHENS].join(', ')} when present`);
|
|
384
|
+
}
|
|
385
|
+
const key = `${step.skill}\u0000${step.file}\u0000${step.expectedWhen ?? ''}`;
|
|
342
386
|
if (seen.has(key)) issue(errors, at, `${where} duplicates skill '${step.skill}' and file '${step.file}'`);
|
|
343
387
|
seen.add(key);
|
|
344
388
|
});
|
|
@@ -412,7 +456,439 @@ function validateOrchestration(value , location , errors )
|
|
|
412
456
|
}
|
|
413
457
|
}
|
|
414
458
|
|
|
415
|
-
|
|
459
|
+
/** The `agentLimits.readOnly` status vocabulary (118 IC-003): `native` = the harness enforces the
|
|
460
|
+
* limit from keys DoFlow writes into the agent file; `unenforced` = it does not, and says why. */
|
|
461
|
+
const AGENT_LIMIT_STATUS = new Set(['native', 'unenforced']);
|
|
462
|
+
const AGENT_LIMIT_KEYS = ['readOnly', 'toolScope'];
|
|
463
|
+
/** The abstract tool categories an agent spec's `tool-scope` names (row 135); `toolScope.map` gives
|
|
464
|
+
* each built-in category's tools on a harness, and `mcpSelector` (a template holding `{server}`)
|
|
465
|
+
* writes the `mcp` category, one selector per MCP server DoFlow's registry names. `form` is how the
|
|
466
|
+
* adapter writes them: `comma` a comma-separated string, `list` an array, `closed` a permission
|
|
467
|
+
* object that denies `*` first and then allows only the declared tools. */
|
|
468
|
+
const TOOL_SCOPE_CATEGORIES = ['read', 'edit', 'shell', 'web', 'agents', 'mcp'];
|
|
469
|
+
const TOOL_SCOPE_MAPPED = TOOL_SCOPE_CATEGORIES.filter((category) => category !== 'mcp');
|
|
470
|
+
const TOOL_SCOPE_FORMS = new Set(['comma', 'list', 'closed']);
|
|
471
|
+
const TOOL_SCOPE_ENTRY_KEYS = new Set(['status', 'field', 'form', 'map', 'mcpSelector', 'evidence', 'checked', 'verifiedOn', 'reason']);
|
|
472
|
+
const AGENT_LIMIT_ENTRY_KEYS = new Set(['status', 'apply', 'evidence', 'checked', 'verifiedOn', 'reason']);
|
|
473
|
+
|
|
474
|
+
/** True when `value` is a non-blank string that parses as an http: or https: URL. */
|
|
475
|
+
function isHttpUrl(value ) {
|
|
476
|
+
if (!value.trim()) return false;
|
|
477
|
+
try { return ['http:', 'https:'].includes(new URL(value).protocol); } catch { return false; }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Validates one harness's `agentLimits` block (118 IC-003): how a `read-only` agent spec is limited
|
|
481
|
+
* on that harness. Declared, never inferred; the adapters read `apply` and nothing else. */
|
|
482
|
+
/** One row per harness for doctor and capabilities (135): how it writes an agent's tool scope, or why
|
|
483
|
+
* it writes none. An unenforced harness projects no tool field, so its agents keep every tool. */
|
|
484
|
+
function agentToolScopeView(registry ) {
|
|
485
|
+
return registry.harnesses.map((h ) => {
|
|
486
|
+
const entry = h.agentLimits?.toolScope || {};
|
|
487
|
+
return entry.status === 'native'
|
|
488
|
+
? { harness: h.id, status: 'native', field: entry.field }
|
|
489
|
+
: { harness: h.id, status: entry.status || 'unenforced', reason: entry.reason || 'not declared' };
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validateAgentLimits(value , location , errors ) {
|
|
494
|
+
if (!object(value)) { issue(errors, location, 'agentLimits block is required and must be an object'); return; }
|
|
495
|
+
for (const key of AGENT_LIMIT_KEYS) {
|
|
496
|
+
if (!Object.hasOwn(value, key)) issue(errors, location, `agentLimits is missing '${key}'`);
|
|
497
|
+
}
|
|
498
|
+
for (const key of Object.keys(value)) {
|
|
499
|
+
if (!AGENT_LIMIT_KEYS.includes(key)) issue(errors, location, `agentLimits has unknown key '${key}'`);
|
|
500
|
+
}
|
|
501
|
+
if (Object.hasOwn(value, 'toolScope')) validateToolScope(value.toolScope, `${location} agentLimits toolScope`, errors);
|
|
502
|
+
if (!Object.hasOwn(value, 'readOnly')) return;
|
|
503
|
+
const at = `${location} agentLimits readOnly`;
|
|
504
|
+
const entry = value.readOnly;
|
|
505
|
+
if (!object(entry)) { issue(errors, at, 'must be an object'); return; }
|
|
506
|
+
for (const key of Object.keys(entry)) {
|
|
507
|
+
if (!AGENT_LIMIT_ENTRY_KEYS.has(key)) issue(errors, at, `has unknown key '${key}'`);
|
|
508
|
+
}
|
|
509
|
+
if (!AGENT_LIMIT_STATUS.has(entry.status)) { issue(errors, at, 'status must be native or unenforced'); return; }
|
|
510
|
+
const evidenceOk = Array.isArray(entry.evidence) && entry.evidence.every((item ) => typeof item === 'string');
|
|
511
|
+
if (!evidenceOk) issue(errors, at, 'evidence must be an array of strings');
|
|
512
|
+
else for (const item of entry.evidence) if (!isHttpUrl(item)) issue(errors, at, `evidence entry ${JSON.stringify(item)} must be an http(s) URL`);
|
|
513
|
+
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)');
|
|
514
|
+
if (entry.status === 'native') {
|
|
515
|
+
if (!object(entry.apply) || Object.keys(entry.apply).length === 0) issue(errors, at, 'native requires apply as a non-empty object');
|
|
516
|
+
if (evidenceOk && entry.evidence.length === 0) issue(errors, at, 'native requires non-empty evidence');
|
|
517
|
+
if (entry.checked !== 'docs' && entry.checked !== 'live') issue(errors, at, 'native requires checked as docs or live');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (entry.apply !== undefined) issue(errors, at, 'unenforced must not carry apply');
|
|
521
|
+
if (typeof entry.reason !== 'string' || !entry.reason.trim()) { issue(errors, at, 'unenforced requires a reason'); return; }
|
|
522
|
+
if (evidenceOk && entry.evidence.length === 0 && entry.reason.trim().toLowerCase() !== 'no agent surface') {
|
|
523
|
+
issue(errors, at, 'unenforced requires non-empty evidence unless the reason is "no agent surface"');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Validates one harness's `agentLimits.toolScope` (row 135): how an agent spec's `tool-scope` is
|
|
528
|
+
* written on that harness, or why it is not. A `native` map names every category with at least one tool. */
|
|
529
|
+
function validateToolScope(entry , at , errors ) {
|
|
530
|
+
if (!object(entry)) { issue(errors, at, 'must be an object'); return; }
|
|
531
|
+
for (const key of Object.keys(entry)) if (!TOOL_SCOPE_ENTRY_KEYS.has(key)) issue(errors, at, `has unknown key '${key}'`);
|
|
532
|
+
if (!AGENT_LIMIT_STATUS.has(entry.status)) { issue(errors, at, 'status must be native or unenforced'); return; }
|
|
533
|
+
const evidenceOk = Array.isArray(entry.evidence) && entry.evidence.every((item ) => typeof item === 'string' && isHttpUrl(item));
|
|
534
|
+
if (!evidenceOk) issue(errors, at, 'evidence must be an array of http(s) URLs');
|
|
535
|
+
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)');
|
|
536
|
+
if (entry.status === 'unenforced') {
|
|
537
|
+
for (const key of ['field', 'form', 'map', 'mcpSelector']) if (entry[key] !== undefined) issue(errors, at, `unenforced must not carry ${key}`);
|
|
538
|
+
if (typeof entry.reason !== 'string' || !entry.reason.trim()) issue(errors, at, 'unenforced requires a reason');
|
|
539
|
+
else if (evidenceOk && entry.evidence.length === 0 && entry.reason.trim().toLowerCase() !== 'no agent surface') issue(errors, at, 'unenforced requires non-empty evidence unless the reason is "no agent surface"');
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (typeof entry.field !== 'string' || !entry.field.trim()) issue(errors, at, 'native requires field');
|
|
543
|
+
if (!TOOL_SCOPE_FORMS.has(entry.form)) issue(errors, at, `form must be one of ${[...TOOL_SCOPE_FORMS].join(', ')}`);
|
|
544
|
+
if (entry.checked !== 'docs' && entry.checked !== 'live') issue(errors, at, 'native requires checked as docs or live');
|
|
545
|
+
if (evidenceOk && entry.evidence.length === 0) issue(errors, at, 'native requires non-empty evidence');
|
|
546
|
+
// A selector names one server: no wildcard may stand before `{server}`, and the server is not the whole selector.
|
|
547
|
+
if (typeof entry.mcpSelector !== 'string' || !entry.mcpSelector.includes('{server}') || entry.mcpSelector === '{server}' || /[*?].*\{server\}/.test(entry.mcpSelector)) {
|
|
548
|
+
issue(errors, at, 'native requires mcpSelector, a template naming one {server} with no wildcard before it');
|
|
549
|
+
}
|
|
550
|
+
if (!object(entry.map)) { issue(errors, at, 'native requires map'); return; }
|
|
551
|
+
for (const key of Object.keys(entry.map)) if (!TOOL_SCOPE_MAPPED.includes(key)) issue(errors, at, `map has unknown category '${key}'`);
|
|
552
|
+
for (const category of TOOL_SCOPE_MAPPED) {
|
|
553
|
+
const tools = entry.map[category];
|
|
554
|
+
if (!Array.isArray(tools) || tools.length === 0 || tools.some((tool ) => typeof tool !== 'string' || !tool.trim())) issue(errors, at, `map.${category} must be a non-empty array of tool names`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Keys of a harness's `worker` block (081 IC-001) and the recipe fields each capability entry may
|
|
559
|
+
* carry when, and only when, its status is `supported`. `required` lists the fields a `supported`
|
|
560
|
+
* entry must carry. The last three are declared, not implemented, so they take no recipe field. */
|
|
561
|
+
const WORKER_CAPABILITIES = Object.freeze({
|
|
562
|
+
cli: { allowed: ['binary', 'versionArgv'], required: ['binary', 'versionArgv'] },
|
|
563
|
+
oneShot: { allowed: ['argv', 'promptVia'], required: ['argv', 'promptVia'] },
|
|
564
|
+
structuredOutput: { allowed: ['argv', 'parser', 'quotaSignals'], required: ['parser'] },
|
|
565
|
+
readOnly: { allowed: ['argv'], required: ['argv'] },
|
|
566
|
+
spawnDenial: { allowed: ['argv'], required: ['argv'] },
|
|
567
|
+
runnableProbe: { allowed: ['argv'], required: ['argv'] },
|
|
568
|
+
hooksProbe: { allowed: ['argv'], required: ['argv'] },
|
|
569
|
+
guidanceProbe: { allowed: ['argv', 'marker'], required: ['argv'] },
|
|
570
|
+
loginProbe: { allowed: ['argv'], required: ['argv'] },
|
|
571
|
+
persistentSession: { allowed: [], required: [] },
|
|
572
|
+
resume: { allowed: [], required: [] },
|
|
573
|
+
steering: { allowed: [], required: [] },
|
|
574
|
+
});
|
|
575
|
+
/** Capability entries a `worker` block may carry but need not (082 IC-013, DEC-020; 120 IC-011).
|
|
576
|
+
* Absent reads as `unknown`; present, the entry takes every IC-001 capability-entry rule. */
|
|
577
|
+
const WORKER_OPTIONAL_CAPABILITIES = Object.freeze({
|
|
578
|
+
isolatedEdit: { allowed: ['argv'], required: ['argv'] },
|
|
579
|
+
model: { allowed: ['argv'], required: ['argv'] },
|
|
580
|
+
});
|
|
581
|
+
const WORKER_BILLING = new Set(['free', 'metered', 'unknown']);
|
|
582
|
+
const WORKER_KEYS = Object.freeze(['profile', 'billing', ...Object.keys(WORKER_CAPABILITIES), 'modelChecks']);
|
|
583
|
+
const WORKER_ALLOWED_KEYS = Object.freeze([...WORKER_KEYS, ...Object.keys(WORKER_OPTIONAL_CAPABILITIES), 'family']);
|
|
584
|
+
const WORKER_ENTRY_FIELDS = new Set(['status', 'evidence', 'verification', 'verifiedOn', 'reason']);
|
|
585
|
+
const WORKER_RECIPE_FIELDS = new Set(['binary', 'versionArgv', 'argv', 'promptVia', 'parser', 'marker', 'quotaSignals']);
|
|
586
|
+
const WORKER_QUOTA_SIGNALS_FIELDS = new Set(['signals', 'basis', 'evidence']);
|
|
587
|
+
const WORKER_FACTS = new Set(['hooksLoaded', 'identityForwarded', 'guidanceLoaded', 'login']);
|
|
588
|
+
const WORKER_MODEL_CHECK_FIELDS = new Set(['id', 'argv', 'establishes', 'estimateTokens', 'timeoutSeconds']);
|
|
589
|
+
const WORKER_ARGV_MAX = 32;
|
|
590
|
+
const WORKER_ARG_MAX_CHARS = 200;
|
|
591
|
+
|
|
592
|
+
/** A worker argv: an array of 0 to 32 strings, each at most 200 characters and free of NUL. */
|
|
593
|
+
function workerArgvProblem(value ) {
|
|
594
|
+
if (!Array.isArray(value) || value.some((item ) => typeof item !== 'string')) return 'argv must be an array of strings';
|
|
595
|
+
if (value.length > WORKER_ARGV_MAX) return `argv must have at most ${WORKER_ARGV_MAX} elements`;
|
|
596
|
+
if (value.some((item ) => item.length > WORKER_ARG_MAX_CHARS)) return `argv elements must be at most ${WORKER_ARG_MAX_CHARS} characters`;
|
|
597
|
+
if (value.some((item ) => item.includes('\0'))) return 'argv elements must not contain NUL';
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Validates one harness's `worker` block (081 IC-001): the external-worker capabilities and launch
|
|
602
|
+
* recipe beside `orchestration`. Every message reads `harness '<id>' worker '<key>': ...` and never
|
|
603
|
+
* contains ` orchestration`, so G33.15's orchestration filter reads none of them. Membership of
|
|
604
|
+
* `structuredOutput.parser` in the parser table is checked where that table lives (G41.3, IC-008),
|
|
605
|
+
* not here, so the registry does not require a runtime module. */
|
|
606
|
+
function validateWorker(value , location , errors ) {
|
|
607
|
+
const fail = (key , message ) => issue(errors, `${location} worker '${key}'`, message);
|
|
608
|
+
if (!object(value)) { fail('*', 'worker block is required and must be an object'); return; }
|
|
609
|
+
for (const key of WORKER_KEYS) if (!Object.hasOwn(value, key)) fail(key, 'is missing');
|
|
610
|
+
for (const key of Object.keys(value)) if (!WORKER_ALLOWED_KEYS.includes(key)) fail(key, 'is not a worker key');
|
|
611
|
+
|
|
612
|
+
if (Object.hasOwn(value, 'profile') && value.profile !== null && !COVERAGE_PROFILES.has(value.profile)) {
|
|
613
|
+
fail('profile', 'must be a lifecycle coverage profile or null');
|
|
614
|
+
}
|
|
615
|
+
if (Object.hasOwn(value, 'billing') && !WORKER_BILLING.has(value.billing)) {
|
|
616
|
+
fail('billing', 'must be free, metered, or unknown');
|
|
617
|
+
}
|
|
618
|
+
if (Object.hasOwn(value, 'family') && (typeof value.family !== 'string' || !value.family.trim())) {
|
|
619
|
+
fail('family', 'must be a non-empty string');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
for (const [key, rule] of [...Object.entries(WORKER_CAPABILITIES), ...Object.entries(WORKER_OPTIONAL_CAPABILITIES)]) {
|
|
623
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
624
|
+
const entry = value[key];
|
|
625
|
+
if (!object(entry)) { fail(key, 'must be a capability entry object'); continue; }
|
|
626
|
+
if (!ORCHESTRATION_STATUS.has(entry.status)) fail(key, 'status must be supported, unsupported, or unknown');
|
|
627
|
+
if (!Array.isArray(entry.evidence) || entry.evidence.some((item ) => typeof item !== 'string')) {
|
|
628
|
+
fail(key, 'evidence must be an array of strings');
|
|
629
|
+
} else if (entry.evidence.length === 0 && (entry.status === 'supported' || entry.status === 'unsupported')) {
|
|
630
|
+
fail(key, `evidence must not be empty when status is ${entry.status}`);
|
|
631
|
+
}
|
|
632
|
+
if (typeof entry.verification !== 'string' || !entry.verification.trim()) fail(key, 'requires a verification instruction');
|
|
633
|
+
if (typeof entry.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.verifiedOn)) fail(key, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
|
|
634
|
+
if (entry.status === 'unknown' && (typeof entry.reason !== 'string' || !entry.reason.trim())) fail(key, 'requires a reason when status is unknown');
|
|
635
|
+
if (Object.hasOwn(entry, 'reason') && entry.status !== 'unknown' && (typeof entry.reason !== 'string' || !entry.reason.trim())) fail(key, 'reason must be a non-empty string');
|
|
636
|
+
|
|
637
|
+
for (const field of Object.keys(entry)) {
|
|
638
|
+
if (WORKER_ENTRY_FIELDS.has(field)) continue;
|
|
639
|
+
if (!WORKER_RECIPE_FIELDS.has(field) || !rule.allowed.includes(field)) { fail(key, `has unknown field '${field}'`); continue; }
|
|
640
|
+
if (entry.status !== 'supported') fail(key, `recipe field '${field}' is allowed only when status is supported`);
|
|
641
|
+
}
|
|
642
|
+
if (entry.status !== 'supported') continue;
|
|
643
|
+
for (const field of rule.required) if (!Object.hasOwn(entry, field)) fail(key, `requires recipe field '${field}' when status is supported`);
|
|
644
|
+
|
|
645
|
+
if (Object.hasOwn(entry, 'binary') && (typeof entry.binary !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.binary))) {
|
|
646
|
+
fail(key, 'binary must be a bare command name');
|
|
647
|
+
}
|
|
648
|
+
for (const field of ['versionArgv', 'argv']) {
|
|
649
|
+
if (!Object.hasOwn(entry, field)) continue;
|
|
650
|
+
const problem = workerArgvProblem(entry[field]);
|
|
651
|
+
if (problem) {
|
|
652
|
+
fail(key, problem.replace(/^argv/, field));
|
|
653
|
+
} else {
|
|
654
|
+
// 120 IC-011: only model.argv carries one whole model placeholder.
|
|
655
|
+
if (key === 'model' && field === 'argv') {
|
|
656
|
+
const count = entry.argv.filter((item ) => item === '{model}').length;
|
|
657
|
+
if (count !== 1 || entry.argv.some((item ) => item.includes('{model}') && item !== '{model}')) {
|
|
658
|
+
fail(key, "argv must contain the element '{model}' exactly once, as a whole element");
|
|
659
|
+
}
|
|
660
|
+
} else if (entry[field].some((item ) => item.includes('{model}'))) {
|
|
661
|
+
fail(key, `${field} must not contain '{model}' outside model.argv`);
|
|
662
|
+
}
|
|
663
|
+
// DEC-066: only oneShot.argv carries the prompt; a second placeholder would duplicate it.
|
|
664
|
+
if (!(key === 'oneShot' && field === 'argv') && entry[field].includes('{prompt}')) {
|
|
665
|
+
fail(key, `${field} must not contain '{prompt}' outside oneShot.argv`);
|
|
666
|
+
}
|
|
667
|
+
if (key === 'structuredOutput' && field === 'argv') {
|
|
668
|
+
const count = entry.argv.filter((item ) => item === '{lastMessageFile}').length;
|
|
669
|
+
if (count > 1) {
|
|
670
|
+
fail(key, "argv must contain '{lastMessageFile}' only once and only in structuredOutput.argv");
|
|
671
|
+
}
|
|
672
|
+
} else if (entry[field].includes('{lastMessageFile}')) {
|
|
673
|
+
fail(key, "argv must contain '{lastMessageFile}' only once and only in structuredOutput.argv");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (Object.hasOwn(entry, 'parser') && (typeof entry.parser !== 'string' || !entry.parser.trim())) fail(key, 'parser must be a parser id');
|
|
678
|
+
if (key === 'structuredOutput' && Object.hasOwn(entry, 'quotaSignals')) {
|
|
679
|
+
const quota = entry.quotaSignals;
|
|
680
|
+
if (!object(quota)) fail(key, 'quotaSignals must be an object');
|
|
681
|
+
else {
|
|
682
|
+
for (const field of Object.keys(quota)) if (!WORKER_QUOTA_SIGNALS_FIELDS.has(field)) fail(key, `quotaSignals has unknown field '${field}'`);
|
|
683
|
+
if (!Array.isArray(quota.signals) || quota.signals.length < 1 || quota.signals.length > 16) fail(key, 'quotaSignals.signals must contain 1 to 16 signals');
|
|
684
|
+
else for (const signal of quota.signals) {
|
|
685
|
+
if (typeof signal !== 'string' || signal !== signal.trim() || signal.length < 3 || signal.length > 64 || /[\u0000-\u001f\u007f]/.test(signal)) {
|
|
686
|
+
fail(key, 'quotaSignals.signals must be trimmed strings of 3 to 64 characters without control characters');
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (quota.basis !== 'unverified' && quota.basis !== 'transcript') fail(key, "quotaSignals.basis must be 'unverified' or 'transcript'");
|
|
691
|
+
if (!Array.isArray(quota.evidence) || quota.evidence.some((item ) => typeof item !== 'string')) fail(key, 'quotaSignals.evidence must be an array of strings');
|
|
692
|
+
else if (quota.basis === 'transcript' && quota.evidence.length === 0) fail(key, 'quotaSignals transcript basis requires evidence');
|
|
693
|
+
else if (quota.basis === 'unverified' && quota.evidence.length !== 0) fail(key, 'quotaSignals unverified basis requires empty evidence');
|
|
694
|
+
else if (quota.basis === 'transcript') {
|
|
695
|
+
const harnessMatch = location.match(/^harness '([^']+)'/);
|
|
696
|
+
for (const item of quota.evidence) {
|
|
697
|
+
if (path.isAbsolute(item) || item.split(/[\\/]/).includes('..')) {
|
|
698
|
+
fail(key, 'quotaSignals transcript evidence must be under its harness worker-evidence directory');
|
|
699
|
+
} else if (harnessMatch) {
|
|
700
|
+
const prefix = `core/worker-evidence/${harnessMatch[1]}/`;
|
|
701
|
+
if (!item.startsWith(prefix)) fail(key, 'quotaSignals transcript evidence must be under its harness worker-evidence directory');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (Object.hasOwn(entry, 'marker') && (typeof entry.marker !== 'string' || entry.marker.length < 1 || entry.marker.length > 200)) {
|
|
708
|
+
fail(key, 'marker must be 1 to 200 characters');
|
|
709
|
+
}
|
|
710
|
+
if (key === 'oneShot' && Object.hasOwn(entry, 'promptVia')) {
|
|
711
|
+
const count = Array.isArray(entry.argv) ? entry.argv.filter((item ) => item === '{prompt}').length : 0;
|
|
712
|
+
if (entry.promptVia === 'argv') {
|
|
713
|
+
if (count !== 1) fail(key, "argv must contain the element '{prompt}' exactly once when promptVia is argv");
|
|
714
|
+
} else if (entry.promptVia === 'stdin') {
|
|
715
|
+
if (count !== 0) fail(key, "argv must not contain '{prompt}' when promptVia is stdin");
|
|
716
|
+
} else {
|
|
717
|
+
fail(key, 'promptVia must be argv or stdin');
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (!Object.hasOwn(value, 'modelChecks')) return;
|
|
723
|
+
if (!Array.isArray(value.modelChecks)) { fail('modelChecks', 'must be an array'); return; }
|
|
724
|
+
const seen = new Set ();
|
|
725
|
+
value.modelChecks.forEach((check , index ) => {
|
|
726
|
+
const key = `modelChecks[${index}]`;
|
|
727
|
+
if (!object(check)) { fail(key, 'must be an object'); return; }
|
|
728
|
+
for (const field of Object.keys(check)) if (!WORKER_MODEL_CHECK_FIELDS.has(field)) fail(key, `has unknown field '${field}'`);
|
|
729
|
+
if (typeof check.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(check.id)) fail(key, 'id must be a lowercase identifier');
|
|
730
|
+
else if (seen.has(check.id)) fail(key, `duplicate id '${check.id}'`);
|
|
731
|
+
else seen.add(check.id);
|
|
732
|
+
const problem = workerArgvProblem(check.argv);
|
|
733
|
+
if (problem) {
|
|
734
|
+
fail(key, problem);
|
|
735
|
+
} else {
|
|
736
|
+
if (check.argv.some((item ) => item.includes('{model}'))) fail(key, "argv must not contain '{model}' outside model.argv");
|
|
737
|
+
if (check.argv.includes('{prompt}')) fail(key, "argv must not contain '{prompt}' outside oneShot.argv");
|
|
738
|
+
if (check.argv.includes('{lastMessageFile}')) fail(key, "argv must contain '{lastMessageFile}' only once and only in structuredOutput.argv");
|
|
739
|
+
}
|
|
740
|
+
if (!Array.isArray(check.establishes) || check.establishes.length === 0 || check.establishes.some((fact ) => !WORKER_FACTS.has(fact))) {
|
|
741
|
+
fail(key, `establishes must be a non-empty subset of ${[...WORKER_FACTS].join(', ')}`);
|
|
742
|
+
}
|
|
743
|
+
if (!Number.isInteger(check.estimateTokens) || check.estimateTokens < 1) fail(key, 'estimateTokens must be an integer of at least 1');
|
|
744
|
+
if (!Number.isInteger(check.timeoutSeconds) || check.timeoutSeconds < 1 || check.timeoutSeconds > 600) fail(key, 'timeoutSeconds must be an integer from 1 to 600');
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const CONTEXT_PROFILE_FIELDS = Object.freeze([
|
|
749
|
+
'capacityTokens',
|
|
750
|
+
'reserveTokens',
|
|
751
|
+
'tokenTelemetry',
|
|
752
|
+
'toolResultLimitBytes',
|
|
753
|
+
'retention',
|
|
754
|
+
]);
|
|
755
|
+
|
|
756
|
+
/** Validates one harness's `contextProfile` block (075 IC-013): capacity, reserve, telemetry,
|
|
757
|
+
* toolResultLimitBytes and retention. Each is a Fact<T> with {value, source, verifiedOn} or
|
|
758
|
+
* {unverified, verifiedOn}. */
|
|
759
|
+
function validateContextProfile(value , location , errors ) {
|
|
760
|
+
if (!object(value)) {
|
|
761
|
+
issue(errors, location, 'contextProfile block is required and must be an object');
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
for (const field of CONTEXT_PROFILE_FIELDS) {
|
|
765
|
+
if (!Object.hasOwn(value, field)) {
|
|
766
|
+
issue(errors, location, `contextProfile is missing field '${field}'`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const key of Object.keys(value)) {
|
|
770
|
+
if (!CONTEXT_PROFILE_FIELDS.includes(key)) {
|
|
771
|
+
issue(errors, location, `contextProfile has unknown key '${key}'`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
for (const field of CONTEXT_PROFILE_FIELDS) {
|
|
775
|
+
if (!Object.hasOwn(value, field)) continue;
|
|
776
|
+
const at = `${location} contextProfile '${field}'`;
|
|
777
|
+
const fact = value[field];
|
|
778
|
+
if (!object(fact)) {
|
|
779
|
+
issue(errors, at, 'must be a Fact object');
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const hasUnverified = Object.hasOwn(fact, 'unverified');
|
|
783
|
+
const hasValue = Object.hasOwn(fact, 'value');
|
|
784
|
+
|
|
785
|
+
if (hasUnverified) {
|
|
786
|
+
if (typeof fact.unverified !== 'string' || !fact.unverified.trim()) {
|
|
787
|
+
issue(errors, at, 'unverified must be a non-empty string');
|
|
788
|
+
}
|
|
789
|
+
if (hasValue) {
|
|
790
|
+
issue(errors, at, 'cannot declare both value and unverified');
|
|
791
|
+
}
|
|
792
|
+
} else if (hasValue) {
|
|
793
|
+
if (typeof fact.source !== 'string' || !fact.source.trim()) {
|
|
794
|
+
issue(errors, at, 'a declared value requires a non-empty source');
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
issue(errors, at, 'requires either value with source or a non-empty unverified');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (
|
|
801
|
+
typeof fact.verifiedOn !== 'string' ||
|
|
802
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(fact.verifiedOn) ||
|
|
803
|
+
Number.isNaN(Date.parse(fact.verifiedOn)) ||
|
|
804
|
+
new Date(fact.verifiedOn).toISOString().slice(0, 10) !== fact.verifiedOn
|
|
805
|
+
) {
|
|
806
|
+
issue(errors, at, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (hasValue && fact.value !== undefined) {
|
|
810
|
+
const val = fact.value;
|
|
811
|
+
if (field === 'capacityTokens') {
|
|
812
|
+
if (typeof val !== 'number' || !Number.isInteger(val) || val <= 0) {
|
|
813
|
+
issue(errors, at, 'value must be a positive integer');
|
|
814
|
+
}
|
|
815
|
+
} else if (field === 'reserveTokens') {
|
|
816
|
+
if (typeof val !== 'number' || !Number.isInteger(val) || val < 0) {
|
|
817
|
+
issue(errors, at, 'value must be a non-negative integer');
|
|
818
|
+
}
|
|
819
|
+
} else if (field === 'tokenTelemetry') {
|
|
820
|
+
if (val !== 'harness-reported' && val !== 'none') {
|
|
821
|
+
issue(errors, at, "value must be 'harness-reported' or 'none'");
|
|
822
|
+
}
|
|
823
|
+
} else if (field === 'toolResultLimitBytes') {
|
|
824
|
+
if (typeof val !== 'number' || !Number.isInteger(val) || val <= 0) {
|
|
825
|
+
issue(errors, at, 'value must be a positive integer');
|
|
826
|
+
}
|
|
827
|
+
} else if (field === 'retention') {
|
|
828
|
+
if (val !== 'supported' && val !== 'unsupported') {
|
|
829
|
+
issue(errors, at, "value must be 'supported' or 'unsupported'");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Validates the IC-006 primary model probe declaration for one harness. */
|
|
837
|
+
function validatePrimaryProbe(probe , harnessId , at , errors ) {
|
|
838
|
+
const location = `${at} primaryProbe`;
|
|
839
|
+
const fields = ['model', 'events', 'status', 'evidence', 'verifiedOn', 'reason'];
|
|
840
|
+
if (!object(probe)) {
|
|
841
|
+
issue(errors, location, 'must be an object');
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
for (const key of Object.keys(probe)) {
|
|
845
|
+
if (!fields.includes(key)) issue(errors, location, `has an unknown key '${key}'`);
|
|
846
|
+
}
|
|
847
|
+
for (const field of fields) {
|
|
848
|
+
if (!Object.prototype.hasOwnProperty.call(probe, field)) issue(errors, location, `is missing field '${field}'`);
|
|
849
|
+
}
|
|
850
|
+
if (probe.model !== 'payload' && probe.model !== 'none') issue(errors, location, "model must be 'payload' or 'none'");
|
|
851
|
+
if (!Array.isArray(probe.events)) {
|
|
852
|
+
issue(errors, location, 'events must be an array of distinct non-empty strings');
|
|
853
|
+
} else {
|
|
854
|
+
if (probe.events.some((event ) => typeof event !== 'string' || !event.trim())) {
|
|
855
|
+
issue(errors, location, 'events must contain only non-empty strings');
|
|
856
|
+
}
|
|
857
|
+
if (new Set(probe.events).size !== probe.events.length) issue(errors, location, 'events must contain distinct strings');
|
|
858
|
+
if (probe.model === 'payload' && probe.events.length === 0) issue(errors, location, "events must be non-empty when model is 'payload'");
|
|
859
|
+
if (probe.model === 'none' && probe.events.length !== 0) issue(errors, location, "events must be empty when model is 'none'");
|
|
860
|
+
}
|
|
861
|
+
if (probe.status !== 'supported' && probe.status !== 'unverified') issue(errors, location, "status must be 'supported' or 'unverified'");
|
|
862
|
+
if (!Array.isArray(probe.evidence) || probe.evidence.some((item ) => typeof item !== 'string')) {
|
|
863
|
+
issue(errors, location, 'evidence must be an array of strings');
|
|
864
|
+
}
|
|
865
|
+
if (probe.verifiedOn !== null && (typeof probe.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(probe.verifiedOn))) {
|
|
866
|
+
issue(errors, location, 'verifiedOn must be null or a YYYY-MM-DD string');
|
|
867
|
+
}
|
|
868
|
+
if (typeof probe.reason !== 'string' || !probe.reason.trim()) {
|
|
869
|
+
issue(errors, location, 'reason must be a non-empty line');
|
|
870
|
+
} else if (/[\r\n]/.test(probe.reason)) {
|
|
871
|
+
issue(errors, location, 'reason must be a non-empty line');
|
|
872
|
+
}
|
|
873
|
+
if (probe.status === 'supported') {
|
|
874
|
+
if (probe.model !== 'payload') issue(errors, location, "status 'supported' requires model 'payload'");
|
|
875
|
+
const prefix = `core/worker-evidence/${harnessId}/primary-probe/`;
|
|
876
|
+
if (!Array.isArray(probe.evidence) || !probe.evidence.some((item ) => typeof item === 'string' && item.startsWith(prefix) && item.length > prefix.length)) {
|
|
877
|
+
issue(errors, location, `status 'supported' requires evidence under '${prefix}'`);
|
|
878
|
+
}
|
|
879
|
+
if (typeof probe.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(probe.verifiedOn)) {
|
|
880
|
+
issue(errors, location, "status 'supported' requires a verifiedOn date");
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function validateRegistry(
|
|
886
|
+
registry ,
|
|
887
|
+
{ repoRoot, fsImpl = fs, sources = 'require' } = {},
|
|
888
|
+
) {
|
|
889
|
+
if (sources !== 'require' && sources !== 'skip') {
|
|
890
|
+
throw new TypeError(`sources must be 'require' or 'skip', got ${JSON.stringify(sources)}`);
|
|
891
|
+
}
|
|
416
892
|
const errors = [];
|
|
417
893
|
const harnesses = Array.isArray(registry?.harnesses) ? registry.harnesses : null;
|
|
418
894
|
const assets = Array.isArray(registry?.assets) ? registry.assets : null;
|
|
@@ -430,11 +906,12 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
|
|
|
430
906
|
idsUnique(mcp, 'mcp', errors); idsUnique(lifecycle, 'lifecycle', errors);
|
|
431
907
|
idsUnique(externalTools, 'externalTools', errors);
|
|
432
908
|
|
|
433
|
-
const harnessIds = new Set((harnesses || []).filter(object).map((item ) => item.id));
|
|
909
|
+
const harnessIds = new Set ((harnesses || []).filter(object).map((item ) => item.id ));
|
|
434
910
|
for (const harness of harnesses || []) {
|
|
435
911
|
const at = `harness '${harness?.id ?? '?'}'`;
|
|
436
912
|
if (!object(harness)) { issue(errors, 'harnesses', 'entries must be objects'); continue; }
|
|
437
|
-
if (typeof harness.id !== 'string' ||
|
|
913
|
+
if (typeof harness.id !== 'string' || !BUILTIN_WORKER_ID_RE.test(harness.id)) issue(errors, at, 'id must be a lowercase identifier');
|
|
914
|
+
else if (RESERVED_WORKER_IDS.includes(harness.id)) issue(errors, at, `id '${harness.id}' is a reserved worker id`);
|
|
438
915
|
if (typeof harness.displayName !== 'string' || !harness.displayName.trim()) issue(errors, at, 'requires displayName');
|
|
439
916
|
if (typeof harness.adapter !== 'string' || !harness.adapter.trim()) issue(errors, at, 'requires adapter');
|
|
440
917
|
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');
|
|
@@ -451,9 +928,23 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
|
|
|
451
928
|
validateCapability(declaration, `${at} capability '${capability}'`, errors);
|
|
452
929
|
}
|
|
453
930
|
validateOrchestration(harness.orchestration, at, errors);
|
|
454
|
-
|
|
931
|
+
validateAgentLimits(harness.agentLimits, at, errors);
|
|
932
|
+
validateWorker(harness.worker, at, errors);
|
|
933
|
+
const quota = harness.worker?.structuredOutput?.quotaSignals;
|
|
934
|
+
if (object(quota) && quota.basis === 'transcript' && Array.isArray(quota.evidence) && typeof repoRoot === 'string') {
|
|
935
|
+
for (const evidence of quota.evidence) {
|
|
936
|
+
if (typeof evidence === 'string' && !fsImpl.existsSync(path.resolve(repoRoot, evidence))) {
|
|
937
|
+
issue(errors, `${at} worker 'structuredOutput'`, `quotaSignals transcript evidence '${evidence}' does not exist`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
validateContextProfile(harness.contextProfile, at, errors);
|
|
942
|
+
validatePrimaryProbe(harness.primaryProbe, typeof harness.id === 'string' ? harness.id : '?', at, errors);
|
|
943
|
+
if (harness.id === 'codex') validateNativeProjection(harness.nativeProjection, harness, { repoRoot, fsImpl, sources }, errors);
|
|
455
944
|
}
|
|
456
945
|
|
|
946
|
+
validateRetired(registry?.retired, harnessIds, errors);
|
|
947
|
+
|
|
457
948
|
const contractHarnesses = new Set();
|
|
458
949
|
for (const contract of contracts || []) {
|
|
459
950
|
validateContract(contract, `contract '${contract?.harness ?? '?'}'`, errors, harnessIds);
|
|
@@ -474,7 +965,8 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
|
|
|
474
965
|
for (const field of ['id', 'kind', 'source', 'ownership']) if (typeof asset[field] !== 'string' || !asset[field].trim()) issue(errors, at, `requires ${field}`);
|
|
475
966
|
if (typeof asset.source === 'string' && repoRoot) {
|
|
476
967
|
const source = path.resolve(repoRoot, asset.source);
|
|
477
|
-
|
|
968
|
+
const outside = !source.startsWith(`${path.resolve(repoRoot)}${path.sep}`);
|
|
969
|
+
if (outside || (sources === 'require' && !fsImpl.existsSync(source))) issue(errors, at, `source does not exist inside repository: ${asset.source}`);
|
|
478
970
|
}
|
|
479
971
|
if (!Array.isArray(asset.appliesTo) || asset.appliesTo.length === 0) issue(errors, at, 'appliesTo must be a non-empty array');
|
|
480
972
|
if (!object(asset.projection)) issue(errors, at, 'projection must be an object');
|
|
@@ -488,6 +980,12 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
|
|
|
488
980
|
const capability = harness?.capabilities?.[projection.capability];
|
|
489
981
|
if (!capability) issue(errors, at, `projection '${harnessId}' references unknown capability '${projection.capability}'`);
|
|
490
982
|
else if (capability.status === 'unavailable') issue(errors, at, `projection '${harnessId}' cannot render unavailable capability '${projection.capability}'`);
|
|
983
|
+
if (projection.select !== undefined) {
|
|
984
|
+
if (!COPY_TREE_SELECT_RENDERERS.has(projection.renderer)) issue(errors, at, `projection '${harnessId}' select is only valid for ${[...COPY_TREE_SELECT_RENDERERS].map((name) => `'${name}'`).join(' and ')}`);
|
|
985
|
+
if (typeof projection.select !== 'string' || !COPY_TREE_SELECTS.has(projection.select)) {
|
|
986
|
+
issue(errors, at, `projection '${harnessId}' has unknown select '${projection.select}' (known: ${[...COPY_TREE_SELECTS].join(', ')})`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
491
989
|
}
|
|
492
990
|
}
|
|
493
991
|
|
|
@@ -520,6 +1018,8 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
|
|
|
520
1018
|
if (mapping.status === 'unavailable' && typeof mapping.fallback !== 'string') issue(errors, at, `unavailable mapping '${harnessId}' requires fallback guidance`);
|
|
521
1019
|
}
|
|
522
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
validateCoverage(registry?.coverage, registry?.lifecycle, harnessIds, errors);
|
|
523
1023
|
for (const tool of externalTools || []) validateExternalTool(tool, `external tool '${tool?.id ?? '?'}'`, errors);
|
|
524
1024
|
|
|
525
1025
|
const modelProviders = Array.isArray(registry?.modelProviders) ? registry.modelProviders : null;
|
|
@@ -542,9 +1042,163 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
|
|
|
542
1042
|
const providerIds = new Set((modelProviders || []).filter(object).map((item ) => item.id));
|
|
543
1043
|
for (const slot of modelSlots) validateModelSlot(slot, `model slot '${slot?.id ?? '?'}'`, errors, providerIds);
|
|
544
1044
|
}
|
|
545
|
-
return { ok: errors.length === 0, errors };
|
|
1045
|
+
return { ok: errors.length === 0, errors, sources };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const COVERAGE_CLASSES = new Set(['blocking', 'observation', 'advisory', 'unsupported', 'unknown']);
|
|
1049
|
+
const COVERAGE_TIMINGS = new Set(['pre-tool', 'post-tool', 'session', 'prompt', 'stop', 'compact', 'subagent']);
|
|
1050
|
+
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']);
|
|
1051
|
+
const COVERAGE_KINDS = new Set(['doc', 'types', 'fixture', 'live']);
|
|
1052
|
+
|
|
1053
|
+
function validateCoverage(value , lifecycle , harnessIds , errors ) {
|
|
1054
|
+
if (value === undefined || value === null) return;
|
|
1055
|
+
const at = 'lifecycle.coverage';
|
|
1056
|
+
if (!object(value)) { issue(errors, at, 'must be an object'); return; }
|
|
1057
|
+
if (value.schemaVersion !== 1) issue(errors, `${at}.schemaVersion`, 'must be 1');
|
|
1058
|
+
if (!Number.isInteger(value.revision) || value.revision < 1) issue(errors, `${at}.revision`, 'must be a positive integer');
|
|
1059
|
+
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');
|
|
1060
|
+
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');
|
|
1061
|
+
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');
|
|
1062
|
+
if (value.endOfTurn !== undefined) validateEndOfTurn(value.endOfTurn, `${at}.endOfTurn`, harnessIds, errors);
|
|
1063
|
+
if (value.behaviours !== undefined) validateBehaviours(value.behaviours, `${at}.behaviours`, harnessIds, errors);
|
|
1064
|
+
if (!Array.isArray(value.rows)) { issue(errors, `${at}.rows`, 'must be an array'); return; }
|
|
1065
|
+
const keys = new Set ();
|
|
1066
|
+
for (const [index, row] of value.rows.entries()) {
|
|
1067
|
+
const rowAt = `${at}.rows[${index}]`;
|
|
1068
|
+
if (!object(row)) { issue(errors, rowAt, 'must be an object'); continue; }
|
|
1069
|
+
for (const field of ['policy', 'harness', 'event']) if (typeof row[field] !== 'string' || !row[field]) issue(errors, `${rowAt}.${field}`, 'must be a non-empty string');
|
|
1070
|
+
if (row.policy !== '*' && !(lifecycle || []).some((policy ) => policy.id === row.policy)) issue(errors, `${rowAt}.policy`, 'must name a lifecycle policy or *');
|
|
1071
|
+
if (!harnessIds.has(row.harness)) issue(errors, `${rowAt}.harness`, 'must name a registry harness');
|
|
1072
|
+
if (!COVERAGE_TIMINGS.has(row.timing)) issue(errors, `${rowAt}.timing`, 'has an invalid timing');
|
|
1073
|
+
if (!COVERAGE_PROFILES.has(row.profile)) issue(errors, `${rowAt}.profile`, 'has an invalid profile');
|
|
1074
|
+
if (typeof row.wired !== 'boolean') issue(errors, `${rowAt}.wired`, 'must be a boolean');
|
|
1075
|
+
if (row.configSource !== null && (typeof row.configSource !== 'string' || !row.configSource)) issue(errors, `${rowAt}.configSource`, 'must be a path:line or null');
|
|
1076
|
+
if (row.wired && typeof row.configSource !== 'string') issue(errors, `${rowAt}.configSource`, 'is required when wired');
|
|
1077
|
+
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');
|
|
1078
|
+
if (!COVERAGE_CLASSES.has(row.class)) issue(errors, `${rowAt}.class`, 'has an invalid class');
|
|
1079
|
+
if (!['verified', 'unverified'].includes(row.verification)) issue(errors, `${rowAt}.verification`, 'must be verified or unverified');
|
|
1080
|
+
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');
|
|
1081
|
+
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');
|
|
1082
|
+
const key = JSON.stringify([row.policy, row.harness, row.event, row.profile]);
|
|
1083
|
+
if (keys.has(key)) issue(errors, rowAt, 'duplicates a policy/harness/event/profile tuple');
|
|
1084
|
+
keys.add(key);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const nonEmptyString = (item ) => typeof item === 'string' && item.trim() !== '';
|
|
1089
|
+
const provenanceItemOk = (item ) => object(item) && COVERAGE_KINDS.has(item.kind) && nonEmptyString(item.locator)
|
|
1090
|
+
&& (!['doc', 'types', 'live'].includes(item.kind) || (typeof item.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(item.date)));
|
|
1091
|
+
|
|
1092
|
+
/** Each registry harness appears exactly once under `key`; reports missing, unknown and duplicate entries. */
|
|
1093
|
+
function oncePerHarness(entries , at , harnessIds , key , label , groups , errors ) {
|
|
1094
|
+
const seen = new Set ();
|
|
1095
|
+
for (const entry of entries) {
|
|
1096
|
+
if (!object(entry)) continue;
|
|
1097
|
+
const id = key(entry);
|
|
1098
|
+
if (seen.has(id)) issue(errors, at, `duplicates ${label(entry)}`);
|
|
1099
|
+
seen.add(id);
|
|
1100
|
+
}
|
|
1101
|
+
for (const group of groups) for (const harness of harnessIds) if (!seen.has(JSON.stringify([group, harness]))) issue(errors, at, `missing an entry for ${group ? `${group} × ` : ''}harness '${harness}'`);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** IC-017: one end-of-turn entry per harness; `none` when no event, provenance when any. */
|
|
1105
|
+
function validateEndOfTurn(value , at , harnessIds , errors ) {
|
|
1106
|
+
if (!Array.isArray(value)) { issue(errors, at, 'must be an array'); return; }
|
|
1107
|
+
for (const [index, entry] of value.entries()) {
|
|
1108
|
+
const entryAt = `${at}[${index}]`;
|
|
1109
|
+
if (!object(entry)) { issue(errors, entryAt, 'must be an object'); continue; }
|
|
1110
|
+
if (!harnessIds.has(entry.harness)) issue(errors, `${entryAt}.harness`, 'must name a registry harness');
|
|
1111
|
+
if (!Array.isArray(entry.events) || !entry.events.every(nonEmptyString)) { issue(errors, `${entryAt}.events`, 'must be an array of event names'); continue; }
|
|
1112
|
+
if (!Array.isArray(entry.provenance) || !entry.provenance.every(provenanceItemOk)) issue(errors, `${entryAt}.provenance`, 'must be an array of well-formed provenance items');
|
|
1113
|
+
if (entry.events.length === 0 && !nonEmptyString(entry.none)) issue(errors, `${entryAt}.none`, 'is required, non-empty, when events is empty');
|
|
1114
|
+
if (entry.events.length > 0 && !(Array.isArray(entry.provenance) && entry.provenance.some((item ) => object(item) && ['doc', 'types'].includes(item.kind)))) issue(errors, `${entryAt}.provenance`, 'needs a doc or types item when events is non-empty');
|
|
1115
|
+
}
|
|
1116
|
+
oncePerHarness(value, at, harnessIds, (entry) => JSON.stringify(['', entry.harness]), (entry) => `harness '${entry.harness}'`, [''], errors);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const COVERAGE_BEHAVIOURS = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
|
|
1120
|
+
|
|
1121
|
+
/** IC-018: one entry per behaviour × harness; `enforced` names tests, `limitation` states its reason. */
|
|
1122
|
+
function validateBehaviours(value , at , harnessIds , errors ) {
|
|
1123
|
+
if (!Array.isArray(value)) { issue(errors, at, 'must be an array'); return; }
|
|
1124
|
+
for (const [index, entry] of value.entries()) {
|
|
1125
|
+
const entryAt = `${at}[${index}]`;
|
|
1126
|
+
if (!object(entry)) { issue(errors, entryAt, 'must be an object'); continue; }
|
|
1127
|
+
if (!COVERAGE_BEHAVIOURS.includes(entry.behaviour)) issue(errors, `${entryAt}.behaviour`, `must be one of ${COVERAGE_BEHAVIOURS.join(', ')}`);
|
|
1128
|
+
if (!harnessIds.has(entry.harness)) issue(errors, `${entryAt}.harness`, 'must name a registry harness');
|
|
1129
|
+
if (entry.status === 'enforced') {
|
|
1130
|
+
if (!Array.isArray(entry.tests) || entry.tests.length === 0 || !entry.tests.every(nonEmptyString)) issue(errors, `${entryAt}.tests`, 'must be a non-empty array of test paths when enforced');
|
|
1131
|
+
} else if (entry.status === 'limitation') {
|
|
1132
|
+
if (!nonEmptyString(entry.limitation)) issue(errors, `${entryAt}.limitation`, 'must be non-empty when status is limitation');
|
|
1133
|
+
} else issue(errors, `${entryAt}.status`, 'must be enforced or limitation');
|
|
1134
|
+
}
|
|
1135
|
+
oncePerHarness(value, at, harnessIds, (entry) => JSON.stringify([entry.behaviour, entry.harness]), (entry) => `behaviour '${entry.behaviour}' × harness '${entry.harness}'`, [...COVERAGE_BEHAVIOURS], errors);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const RETIRED_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
1139
|
+
const HANDOVER_SCOPES = new Set(['global', 'project']);
|
|
1140
|
+
|
|
1141
|
+
/** A harness DoFlow no longer supports, with what replaces it and how its installed files are
|
|
1142
|
+
* released (row 127 IC-001; `removal.handover` is IC-007). */
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
function isCalendarDate(value ) {
|
|
1153
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
1154
|
+
const date = new Date(`${value}T00:00:00Z`);
|
|
1155
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** Validates harnesses.json `retired[]`: optional; every entry fully typed; ids unique and disjoint
|
|
1159
|
+
* from the live harness ids; `replacement` and every `removal.handover[].to` name a live harness. */
|
|
1160
|
+
function validateRetired(value , harnessIds , errors ) {
|
|
1161
|
+
if (value === undefined) return;
|
|
1162
|
+
if (!Array.isArray(value)) { issue(errors, 'retired', 'must be an array when present'); return; }
|
|
1163
|
+
idsUnique(value, 'retired', errors);
|
|
1164
|
+
const strings = (items , allowEmpty ) => Array.isArray(items) && (allowEmpty || items.length > 0) && items.every(nonEmptyString);
|
|
1165
|
+
for (const [index, entry] of value.entries()) {
|
|
1166
|
+
if (!object(entry)) { issue(errors, `retired[${index}]`, 'must be an object'); continue; }
|
|
1167
|
+
const at = `retired harness '${typeof entry.id === 'string' ? entry.id : `#${index}`}'`;
|
|
1168
|
+
if (typeof entry.id !== 'string' || !RETIRED_ID_RE.test(entry.id)) issue(errors, at, `id must match ${RETIRED_ID_RE}`);
|
|
1169
|
+
else if (harnessIds.has(entry.id)) issue(errors, at, `id collides with live harness '${entry.id}'; an id is either live or retired`);
|
|
1170
|
+
for (const field of ['displayName', 'replacement', 'reason']) if (!nonEmptyString(entry[field])) issue(errors, at, `${field} must be a non-empty string`);
|
|
1171
|
+
if (nonEmptyString(entry.replacement) && !harnessIds.has(entry.replacement)) issue(errors, at, `replacement '${entry.replacement}' is not a live harness`);
|
|
1172
|
+
if (!isCalendarDate(entry.retiredOn)) issue(errors, at, 'retiredOn must be a YYYY-MM-DD date');
|
|
1173
|
+
if (!strings(entry.evidence, false)) issue(errors, at, 'evidence must be a non-empty array of non-empty strings');
|
|
1174
|
+
const removal = entry.removal;
|
|
1175
|
+
if (!object(removal)) { issue(errors, at, 'removal must be an object'); continue; }
|
|
1176
|
+
const hooks = removal.hookCommands;
|
|
1177
|
+
if (!object(hooks)) issue(errors, at, 'removal.hookCommands must be an object');
|
|
1178
|
+
else {
|
|
1179
|
+
for (const field of ['renderer', 'key']) if (!nonEmptyString(hooks[field])) issue(errors, at, `removal.hookCommands.${field} must be a non-empty string`);
|
|
1180
|
+
if (!strings(hooks.commands, false)) issue(errors, at, 'removal.hookCommands.commands must be a non-empty array of non-empty strings');
|
|
1181
|
+
}
|
|
1182
|
+
if (!strings(removal.markerSectionRenderers, true)) issue(errors, at, 'removal.markerSectionRenderers must be an array of non-empty strings');
|
|
1183
|
+
if (!Array.isArray(removal.handover)) { issue(errors, at, 'removal.handover must be an array'); continue; }
|
|
1184
|
+
for (const [h, handover] of removal.handover.entries()) {
|
|
1185
|
+
const hat = `removal.handover[${h}]`;
|
|
1186
|
+
if (!object(handover)) { issue(errors, at, `${hat} must be an object`); continue; }
|
|
1187
|
+
for (const field of ['renderer', 'to', 'surface']) if (!nonEmptyString(handover[field])) issue(errors, at, `${hat}.${field} must be a non-empty string`);
|
|
1188
|
+
if (!HANDOVER_SCOPES.has(handover.scope)) issue(errors, at, `${hat}.scope must be global or project`);
|
|
1189
|
+
if (nonEmptyString(handover.to) && !harnessIds.has(handover.to)) issue(errors, at, `${hat}.to '${handover.to}' is not a live harness`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/** The retired record for `id`, or null when `id` is live or unknown. */
|
|
1195
|
+
function retiredFor(registry , id ) {
|
|
1196
|
+
const retired = Array.isArray(registry?.retired) ? registry.retired : [];
|
|
1197
|
+
return retired.find((entry ) => entry?.id === id) ?? null;
|
|
546
1198
|
}
|
|
547
1199
|
|
|
1200
|
+
function isRetired(registry , id ) { return retiredFor(registry, id) !== null; }
|
|
1201
|
+
|
|
548
1202
|
function harnessFor(registry , id ) {
|
|
549
1203
|
const harness = registry.harnesses.find((item ) => item.id === id);
|
|
550
1204
|
if (!harness) throw new Error(`Unknown registry harness '${id}'`);
|
|
@@ -605,7 +1259,7 @@ function capabilityMapData(registry ) {
|
|
|
605
1259
|
}
|
|
606
1260
|
|
|
607
1261
|
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,
|
|
1262
|
+
REGISTRY_FILES, CAPABILITY_STATUS, COPY_TREE_SELECTS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, AGENT_LIMIT_STATUS, validateAgentLimits, TOOL_SCOPE_CATEGORIES, agentToolScopeView, WORKER_KEYS, WORKER_CAPABILITIES, WORKER_OPTIONAL_CAPABILITIES, validateWorker, CONTEXT_PROFILE_FIELDS, validateContextProfile, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS, validateCoverage,
|
|
1263
|
+
parseRegistryFile, registryDir, loadRegistry, validateRegistry, registrySourceMode,
|
|
1264
|
+
harnessFor, retiredFor, isRetired, selectAssets, selectMcpServers, mcpCapable, nativeMcpCatalog, capabilityMapData,
|
|
611
1265
|
};
|