@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1057 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +46 -4
- package/src/adapters/antigravity/index.ts +631 -0
- package/src/adapters/claude/index.ts +548 -0
- package/src/adapters/claude/mcp.ts +94 -0
- package/src/adapters/codex/agents.ts +208 -0
- package/src/adapters/codex/config.ts +272 -0
- package/src/adapters/codex/hooks.ts +203 -0
- package/src/adapters/codex/index.ts +707 -0
- package/src/adapters/codex/mcp.ts +271 -0
- package/src/adapters/codex/native-projection.ts +34 -0
- package/src/adapters/copilot/index.ts +413 -0
- package/src/adapters/copy-tree.ts +620 -0
- package/src/adapters/gemini/hooks.ts +210 -0
- package/src/adapters/gemini/index.ts +390 -0
- package/src/adapters/hook-commands.ts +60 -0
- package/src/adapters/index.ts +106 -0
- package/src/adapters/kiro/index.ts +310 -0
- package/src/adapters/mcp-entries.ts +331 -0
- package/src/adapters/opencode/index.ts +466 -0
- package/src/adapters/pi/index.ts +279 -0
- package/src/adapters/pi/json-members.ts +185 -0
- package/src/adapters/pi/mcp.ts +373 -0
- package/src/adapters/resolve-source.ts +19 -0
- package/src/cli/commands/install.ts +218 -0
- package/src/cli/commands/list-backups.ts +21 -0
- package/src/cli/commands/reconcile.ts +120 -0
- package/src/cli/commands/remove.ts +82 -0
- package/src/cli/commands/rollback.ts +172 -0
- package/src/cli/commands/self-update.ts +33 -0
- package/src/cli/commands/status.ts +138 -0
- package/src/cli/commands/tools.ts +92 -0
- package/src/cli/commands/update.ts +162 -0
- package/src/cli/index.ts +568 -0
- package/src/cli/runtime-commands.ts +281 -0
- package/src/cli/shared.ts +218 -0
- package/src/helper/bash-helper.ts +34 -0
- package/src/helper/git.ts +24 -0
- package/src/helper/guardrails.ts +49 -0
- package/src/helper/harness-paths.ts +140 -0
- package/src/helper/marker-merge.ts +116 -0
- package/src/helper/prompt.ts +61 -0
- package/src/helper/repo-root.ts +29 -0
- package/src/helper/settings-merge.ts +151 -0
- package/src/helper/settings-scope.ts +33 -0
- package/src/helper/toml.ts +244 -0
- package/src/install/backup-set.ts +149 -0
- package/src/install/backup.ts +724 -0
- package/src/install/context.ts +58 -0
- package/src/install/manifest.ts +145 -0
- package/src/install/mcp.ts +254 -0
- package/src/install/paths.ts +84 -0
- package/src/install/targets.ts +90 -0
- package/src/install/tool-lifecycle.ts +151 -0
- package/src/lifecycle/bash-availability.ts +25 -0
- package/src/lifecycle/index.ts +812 -0
- package/src/lifecycle/mcp-index.ts +28 -0
- package/src/lifecycle/policies.ts +73 -0
- package/src/lifecycle/view.ts +175 -0
- package/src/registry/index.ts +611 -0
- package/src/release/sync-plugin-versions.ts +66 -0
- package/src/runtime/advice/cli.ts +283 -0
- package/src/runtime/advice/client.ts +324 -0
- package/src/runtime/advice/config.ts +189 -0
- package/src/runtime/advice/doctor.ts +169 -0
- package/src/runtime/advice/endpoint-guard.ts +134 -0
- package/src/runtime/advice/kinds.ts +154 -0
- package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
- package/src/runtime/advice/record.ts +166 -0
- package/src/runtime/advice/registry.ts +170 -0
- package/src/runtime/advice/strength.ts +66 -0
- package/src/runtime/brief-tools.ts +342 -0
- package/src/runtime/c4-project.ts +371 -0
- package/src/runtime/capability-router.ts +484 -0
- package/src/runtime/checkouts.ts +170 -0
- package/src/runtime/claims.ts +468 -0
- package/src/runtime/cli-result.ts +55 -0
- package/src/runtime/cli.ts +678 -0
- package/src/runtime/command-detect.ts +516 -0
- package/src/runtime/context-pack.ts +492 -0
- package/src/runtime/decision-register.ts +493 -0
- package/src/runtime/evidence-ledger.ts +338 -0
- package/src/runtime/failure/capture.ts +272 -0
- package/src/runtime/failure/classifier.ts +68 -0
- package/src/runtime/failure/cli.ts +178 -0
- package/src/runtime/failure/home.ts +64 -0
- package/src/runtime/failure/store.ts +226 -0
- package/src/runtime/feature-resolve.ts +79 -0
- package/src/runtime/freshness.ts +177 -0
- package/src/runtime/health.ts +863 -0
- package/src/runtime/history-compactor.ts +237 -0
- package/src/runtime/implementation-gate.ts +321 -0
- package/src/runtime/inventory/coverage.ts +218 -0
- package/src/runtime/inventory/identity.ts +65 -0
- package/src/runtime/inventory/index.ts +705 -0
- package/src/runtime/inventory/precedence.ts +134 -0
- package/src/runtime/inventory/read-scopes.ts +131 -0
- package/src/runtime/inventory/siblings.ts +192 -0
- package/src/runtime/knowledge/chunker.ts +124 -0
- package/src/runtime/knowledge/hybrid.ts +149 -0
- package/src/runtime/knowledge/index-store.ts +145 -0
- package/src/runtime/knowledge/retrieval.ts +194 -0
- package/src/runtime/leak-scan.ts +185 -0
- package/src/runtime/lifecycle/cli.ts +276 -0
- package/src/runtime/lifecycle/event-store.ts +340 -0
- package/src/runtime/lifecycle/fold.ts +353 -0
- package/src/runtime/lifecycle/followup.ts +432 -0
- package/src/runtime/lifecycle/goal.ts +249 -0
- package/src/runtime/lifecycle/intent-writer.ts +86 -0
- package/src/runtime/lifecycle/overview.ts +292 -0
- package/src/runtime/lifecycle/release.ts +241 -0
- package/src/runtime/lifecycle/report-store.ts +221 -0
- package/src/runtime/lifecycle/retention.ts +212 -0
- package/src/runtime/lifecycle/root.ts +71 -0
- package/src/runtime/lifecycle/status.ts +353 -0
- package/src/runtime/lifecycle/store-upkeep.ts +393 -0
- package/src/runtime/locator-resolve.ts +158 -0
- package/src/runtime/mask.ts +210 -0
- package/src/runtime/mcp-consultations.ts +412 -0
- package/src/runtime/mcp-reach.ts +157 -0
- package/src/runtime/model-router.ts +153 -0
- package/src/runtime/outcome.ts +467 -0
- package/src/runtime/reach.ts +148 -0
- package/src/runtime/readiness-record.ts +150 -0
- package/src/runtime/readiness.ts +435 -0
- package/src/runtime/recovery.ts +282 -0
- package/src/runtime/research-request.ts +231 -0
- package/src/runtime/retrieval-plan.ts +578 -0
- package/src/runtime/scaffold/artifacts.ts +217 -0
- package/src/runtime/scaffold/fingerprint.ts +56 -0
- package/src/runtime/scaffold/generate.ts +911 -0
- package/src/runtime/scaffold/languages.ts +245 -0
- package/src/runtime/storage/cli.ts +64 -0
- package/src/runtime/storage/engine.ts +249 -0
- package/src/runtime/storage/registry.ts +130 -0
- package/src/runtime/storage/roots.ts +63 -0
- package/src/runtime/storage/rules.ts +282 -0
- package/src/runtime/storage/upkeep.ts +243 -0
- package/src/runtime/storage/work-status.ts +326 -0
- package/src/runtime/task-attempts.ts +415 -0
- package/src/runtime/task-classifier.ts +550 -0
- package/src/runtime/task-scope.ts +130 -0
- package/src/runtime/task-state.ts +176 -0
- package/src/runtime/trace/indicators.ts +235 -0
- package/src/runtime/trace/ledger.ts +626 -0
- package/src/runtime/trace/render.ts +76 -0
- package/src/runtime/trace/views.ts +459 -0
- package/src/runtime/verification/contract-runner.ts +341 -0
- package/src/runtime/verification/engine.ts +1461 -0
- package/src/runtime/verification/registry.ts +130 -0
- package/src/runtime/verification/scope-bound.ts +418 -0
- package/src/runtime/verification/test-selection.ts +172 -0
- package/src/runtime/workflow-engine.ts +588 -0
- package/src/runtime/workflow-orchestrator.ts +953 -0
- package/src/runtime/worktree.ts +340 -0
- package/src/state/index.ts +193 -0
- package/src/state/lockfile.ts +136 -0
- package/src/state/run-lock.ts +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Capability health probes and the health-aware `doctor` report (plan task C.3, components C4 and
|
|
7
|
+
* C9; requirement FR-013).
|
|
8
|
+
*
|
|
9
|
+
* The distinction this module exists to make: **a binary on `PATH` is not a working capability**.
|
|
10
|
+
* `capabilities` answers "which provider would be selected" from presence; `doctor` answers
|
|
11
|
+
* "did the provider actually answer", which is a different question and the only one that can
|
|
12
|
+
* catch a `semble` whose index is missing, a `graphify` whose graph was never built, or an `rtk`
|
|
13
|
+
* that dies on startup. A provider that is installed but cannot answer reports UNHEALTHY, and a
|
|
14
|
+
* provider that cannot be probed at all reports UNVERIFIED — never HEALTHY. That last rule is
|
|
15
|
+
* deliberate: this feature's own audit found three defects of the shape "reported PASS over no
|
|
16
|
+
* evidence" (a check with no command, a contract with no checks, a readiness verdict about the
|
|
17
|
+
* wrong task), and "the binary exists, so call it healthy" is the same defect wearing a hat.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const os = require('node:os');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const { execFileSync } = require('node:child_process');
|
|
24
|
+
const { CapabilityRouter } = require('./capability-router.ts');
|
|
25
|
+
const { loadRegistry } = require('../registry/index.ts');
|
|
26
|
+
const { evaluateReach } = require('./reach.ts');
|
|
27
|
+
const { mcpReach } = require('./mcp-reach.ts');
|
|
28
|
+
const { OUTCOME_LABELS } = require('./mcp-consultations.ts');
|
|
29
|
+
const { REPO_ROOT } = require('../helper/repo-root.ts');
|
|
30
|
+
|
|
31
|
+
/** Answered a probe. */
|
|
32
|
+
const HEALTHY = 'HEALTHY';
|
|
33
|
+
/** Present, but the probe failed, timed out, or returned nothing. */
|
|
34
|
+
const UNHEALTHY = 'UNHEALTHY';
|
|
35
|
+
/** Not installed. For an optional provider this is a normal state, not a fault. */
|
|
36
|
+
const ABSENT = 'ABSENT';
|
|
37
|
+
/** Installed (or needs no binary) but declares no probe, so function cannot be established. */
|
|
38
|
+
const UNVERIFIED = 'UNVERIFIED';
|
|
39
|
+
|
|
40
|
+
/** A probe must be fast enough that `doctor` stays interactive with every provider installed. */
|
|
41
|
+
const PROBE_TIMEOUT_MS = 2500;
|
|
42
|
+
|
|
43
|
+
/** Version banners are one short line; anything longer is not a banner and is not worth printing. */
|
|
44
|
+
const BANNER_MAX = 80;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where an index- or graph-backed provider keeps the artifact whose age determines whether its
|
|
48
|
+
* answers can be trusted. Each entry lists every location DoFlow knows to look in; when none of
|
|
49
|
+
* them exists the result is UNKNOWN naming the paths searched, because a provider that owns its
|
|
50
|
+
* own cache layout must not have a freshness verdict invented for it.
|
|
51
|
+
*/
|
|
52
|
+
const PROVIDER_ARTIFACTS: Record<string, any> = Object.freeze({
|
|
53
|
+
'graphify.query': {
|
|
54
|
+
label: 'code graph',
|
|
55
|
+
candidates: (root: any) => [path.join(root, 'graphify-out', 'graph.json')],
|
|
56
|
+
refresh: 'graphify update .',
|
|
57
|
+
},
|
|
58
|
+
'semble.search': {
|
|
59
|
+
label: 'semantic index',
|
|
60
|
+
candidates: (root: any, home: any) => [
|
|
61
|
+
path.join(root, '.semble', 'index.json'),
|
|
62
|
+
path.join(root, '.semble'),
|
|
63
|
+
path.join(home, '.cache', 'semble', 'index'),
|
|
64
|
+
],
|
|
65
|
+
refresh: 'semble search "<any query>" . (rebuilds the index on first run)',
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Directories a source-freshness scan must not descend into: build output, dependency trees and
|
|
70
|
+
* the provider artifacts themselves — a graph that indexed itself would always look fresh. */
|
|
71
|
+
const SCAN_SKIP = new Set([
|
|
72
|
+
'.git', 'node_modules', 'graphify-out', '.doflow', 'dist', 'build', 'out', 'coverage',
|
|
73
|
+
'.venv', 'venv', '__pycache__', '.next', '.nuxt', 'target', '.cache', '.pytest_cache', 'tmp',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** A bound on the freshness scan. Hitting it does not produce a guess — it produces UNKNOWN. */
|
|
77
|
+
const SCAN_FILE_LIMIT = 4000;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Runs a provider's declared probe command.
|
|
81
|
+
* @param {string[]} command
|
|
82
|
+
* @param {Object} [options]
|
|
83
|
+
* @param {Function} [options.execFileImpl=execFileSync]
|
|
84
|
+
* @param {number} [options.timeoutMs=PROBE_TIMEOUT_MS]
|
|
85
|
+
* @returns {{ok: boolean, output: string, durationMs: number, timedOut: boolean, error: string|null}}
|
|
86
|
+
*/
|
|
87
|
+
function runProbe(command: any, { execFileImpl = execFileSync, timeoutMs = PROBE_TIMEOUT_MS }: any = {}) {
|
|
88
|
+
const [bin, ...args] = command;
|
|
89
|
+
const started = Number(process.hrtime.bigint() / 1000000n);
|
|
90
|
+
try {
|
|
91
|
+
const stdout = execFileImpl(bin, args, {
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
timeout: timeoutMs,
|
|
94
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
output: String(stdout || '').trim(),
|
|
99
|
+
durationMs: Number(process.hrtime.bigint() / 1000000n) - started,
|
|
100
|
+
timedOut: false,
|
|
101
|
+
error: null,
|
|
102
|
+
};
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
// A killed process and a non-zero exit are both "did not answer", but they are different
|
|
105
|
+
// repairs — a timeout usually means a first-run index build, a non-zero exit usually means a
|
|
106
|
+
// broken install — so they are reported apart rather than as one failure string.
|
|
107
|
+
const timedOut = error.killed === true || error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM';
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
output: '',
|
|
111
|
+
durationMs: Number(process.hrtime.bigint() / 1000000n) - started,
|
|
112
|
+
timedOut,
|
|
113
|
+
error: timedOut ? `probe exceeded ${timeoutMs}ms` : (error.message || 'probe failed'),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** First line of probe output, trimmed to a banner. Control characters are stripped so a provider
|
|
119
|
+
* cannot rewrite the doctor report with an escape sequence. */
|
|
120
|
+
function banner(output: any) {
|
|
121
|
+
if (!output) return null;
|
|
122
|
+
// Control characters are removed by code point rather than by a literal class, so this source
|
|
123
|
+
// file stays free of the escape sequences it is defending against.
|
|
124
|
+
const firstLine = [...output.split('\n')[0]]
|
|
125
|
+
.map((character) => (character.codePointAt(0) < 0x20 || character.codePointAt(0) === 0x7f ? ' ' : character))
|
|
126
|
+
.join('')
|
|
127
|
+
.trim();
|
|
128
|
+
return firstLine.length > BANNER_MAX ? `${firstLine.slice(0, BANNER_MAX - 1)}…` : firstLine;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Newest modification time among the project's own source files, used as the lower bound an index
|
|
133
|
+
* must be at least as new as.
|
|
134
|
+
* @param {string} root
|
|
135
|
+
* @param {Object} [options]
|
|
136
|
+
* @param {Object} [options.fsImpl=fs]
|
|
137
|
+
* @param {number} [options.limit=SCAN_FILE_LIMIT]
|
|
138
|
+
* @returns {{mtimeMs: number|null, filesScanned: number, truncated: boolean}}
|
|
139
|
+
*/
|
|
140
|
+
function newestSourceMtime(root: any, { fsImpl = fs, limit = SCAN_FILE_LIMIT }: any = {}) {
|
|
141
|
+
let mtimeMs = null;
|
|
142
|
+
let filesScanned = 0;
|
|
143
|
+
let truncated = false;
|
|
144
|
+
const queue = [root];
|
|
145
|
+
|
|
146
|
+
while (queue.length) {
|
|
147
|
+
if (filesScanned >= limit) { truncated = true; break; }
|
|
148
|
+
const dir = queue.shift();
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = fsImpl.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
} catch {
|
|
153
|
+
continue; // An unreadable directory narrows the scan; it does not fail the probe.
|
|
154
|
+
}
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (entry.name.startsWith('.') && entry.isDirectory()) continue;
|
|
157
|
+
if (SCAN_SKIP.has(entry.name)) continue;
|
|
158
|
+
const full = path.join(dir, entry.name);
|
|
159
|
+
if (entry.isDirectory()) { queue.push(full); continue; }
|
|
160
|
+
if (!entry.isFile()) continue;
|
|
161
|
+
if (filesScanned >= limit) { truncated = true; break; }
|
|
162
|
+
filesScanned += 1;
|
|
163
|
+
try {
|
|
164
|
+
const stat = fsImpl.statSync(full);
|
|
165
|
+
if (mtimeMs === null || stat.mtimeMs > mtimeMs) mtimeMs = stat.mtimeMs;
|
|
166
|
+
} catch {
|
|
167
|
+
// Vanished between readdir and stat — a normal race in a working tree.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { mtimeMs, filesScanned, truncated };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Freshness of a provider's index or graph relative to the project's source.
|
|
176
|
+
* @param {string} providerId
|
|
177
|
+
* @param {Object} [options]
|
|
178
|
+
* @param {string} [options.root=process.cwd()]
|
|
179
|
+
* @param {string} [options.homeDir]
|
|
180
|
+
* @param {Object} [options.fsImpl=fs]
|
|
181
|
+
* @returns {Object|null} null when the provider is not index-backed
|
|
182
|
+
*/
|
|
183
|
+
function probeFreshness(providerId: any, { root = process.cwd(), homeDir = os.homedir(), fsImpl = fs }: any = {}) {
|
|
184
|
+
const spec = PROVIDER_ARTIFACTS[providerId];
|
|
185
|
+
if (!spec) return null;
|
|
186
|
+
|
|
187
|
+
const searched = spec.candidates(path.resolve(root), path.resolve(homeDir || '.'));
|
|
188
|
+
let artifact = null;
|
|
189
|
+
let artifactMtimeMs = null;
|
|
190
|
+
for (const candidate of searched) {
|
|
191
|
+
try {
|
|
192
|
+
const stat = fsImpl.statSync(candidate);
|
|
193
|
+
artifact = candidate;
|
|
194
|
+
artifactMtimeMs = stat.mtimeMs;
|
|
195
|
+
break;
|
|
196
|
+
} catch {
|
|
197
|
+
// Not at this location; try the next.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const base = { providerId, label: spec.label, searched, artifact, refresh: spec.refresh };
|
|
202
|
+
if (!artifact) {
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
state: 'UNKNOWN',
|
|
206
|
+
reason: `no ${spec.label} found at any location DoFlow knows to check; the provider owns its own layout, so no age can be established for it`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const source = newestSourceMtime(path.resolve(root), { fsImpl });
|
|
211
|
+
if (source.mtimeMs === null) {
|
|
212
|
+
return { ...base, state: 'UNKNOWN', indexedAt: new Date(artifactMtimeMs).toISOString(), reason: 'no source file was readable under the project root, so there is nothing to compare the index against' };
|
|
213
|
+
}
|
|
214
|
+
const behindMs = Math.round(source.mtimeMs - artifactMtimeMs);
|
|
215
|
+
if (behindMs > 0) {
|
|
216
|
+
return {
|
|
217
|
+
...base,
|
|
218
|
+
state: 'STALE',
|
|
219
|
+
indexedAt: new Date(artifactMtimeMs).toISOString(),
|
|
220
|
+
newestSourceAt: new Date(source.mtimeMs).toISOString(),
|
|
221
|
+
behindMs,
|
|
222
|
+
filesScanned: source.filesScanned,
|
|
223
|
+
reason: `source has changed since the ${spec.label} was built`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (source.truncated) {
|
|
227
|
+
// The scan stopped early, so "nothing newer was seen" is not "nothing newer exists". Saying
|
|
228
|
+
// FRESH here would be a verdict over incomplete evidence.
|
|
229
|
+
return {
|
|
230
|
+
...base,
|
|
231
|
+
state: 'UNKNOWN',
|
|
232
|
+
indexedAt: new Date(artifactMtimeMs).toISOString(),
|
|
233
|
+
filesScanned: source.filesScanned,
|
|
234
|
+
reason: `freshness scan stopped at ${source.filesScanned} files, so no file newer than the ${spec.label} was seen — which is not the same as none existing`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
...base,
|
|
239
|
+
state: 'FRESH',
|
|
240
|
+
indexedAt: new Date(artifactMtimeMs).toISOString(),
|
|
241
|
+
newestSourceAt: new Date(source.mtimeMs).toISOString(),
|
|
242
|
+
filesScanned: source.filesScanned,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Probes one provider declared in `capabilities.json`.
|
|
248
|
+
* @param {Object} provider
|
|
249
|
+
* @param {Object} options
|
|
250
|
+
* @param {CapabilityRouter} options.router
|
|
251
|
+
* @param {string} [options.projectRoot]
|
|
252
|
+
* @param {Function} [options.execFileImpl]
|
|
253
|
+
* @param {Object} [options.fsImpl]
|
|
254
|
+
* @returns {Object} probe result
|
|
255
|
+
*/
|
|
256
|
+
function probeProvider(provider: any, { router, projectRoot = process.cwd(), execFileImpl, fsImpl = fs }: any = {}) {
|
|
257
|
+
if (!provider || !provider.id) {
|
|
258
|
+
return { id: null, status: UNVERIFIED, detail: 'no provider declared' };
|
|
259
|
+
}
|
|
260
|
+
const result: any = {
|
|
261
|
+
id: provider.id,
|
|
262
|
+
name: provider.name || provider.id,
|
|
263
|
+
binary: provider.binary || null,
|
|
264
|
+
present: null,
|
|
265
|
+
answered: false,
|
|
266
|
+
status: UNVERIFIED,
|
|
267
|
+
detail: null,
|
|
268
|
+
durationMs: null,
|
|
269
|
+
version: null,
|
|
270
|
+
freshness: null,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (provider.binary) {
|
|
274
|
+
result.present = router.isBinaryAvailable(provider.binary);
|
|
275
|
+
if (!result.present) {
|
|
276
|
+
result.status = ABSENT;
|
|
277
|
+
result.detail = `binary '${provider.binary}' is not on PATH`;
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const command = Array.isArray(provider.checkCommand) && provider.checkCommand.length
|
|
283
|
+
? provider.checkCommand
|
|
284
|
+
: null;
|
|
285
|
+
if (!command) {
|
|
286
|
+
result.status = UNVERIFIED;
|
|
287
|
+
result.detail = provider.binary
|
|
288
|
+
? `binary '${provider.binary}' is present but the registry declares no probe command, so DoFlow cannot establish that it answers`
|
|
289
|
+
: 'declares neither a binary nor a probe command, so its function depends entirely on the project (see the project commands section)';
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const probe = runProbe(command, { execFileImpl });
|
|
294
|
+
result.durationMs = probe.durationMs;
|
|
295
|
+
result.probeCommand = command.join(' ');
|
|
296
|
+
if (!probe.ok) {
|
|
297
|
+
result.status = UNHEALTHY;
|
|
298
|
+
// `banner` rather than the raw error: a failing provider can emit a stack trace, and a
|
|
299
|
+
// multi-line detail turns the report into an unreadable wall. The full text is not lost — the
|
|
300
|
+
// reader is told the exact command to re-run.
|
|
301
|
+
result.detail = `installed, but \`${result.probeCommand}\` did not answer: ${banner(probe.error) || 'no error text'}`;
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
if (!probe.output) {
|
|
305
|
+
// An exit-0 with no output is the exact "PASS over zero evidence" shape this feature is
|
|
306
|
+
// correcting: the process ran, but nothing establishes that the capability works.
|
|
307
|
+
result.status = UNHEALTHY;
|
|
308
|
+
result.detail = `installed, but \`${result.probeCommand}\` returned no output, so nothing establishes that it answered`;
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
result.answered = true;
|
|
313
|
+
result.status = HEALTHY;
|
|
314
|
+
result.version = banner(probe.output);
|
|
315
|
+
result.freshness = probeFreshness(provider.id, { root: projectRoot, fsImpl });
|
|
316
|
+
result.detail = result.version;
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Probes every provider the capability registry declares, de-duplicated by provider id.
|
|
322
|
+
* @param {Object} [options]
|
|
323
|
+
* @param {string} [options.repoRoot] where the registry lives
|
|
324
|
+
* @param {string} [options.projectRoot] the tree whose index freshness is judged
|
|
325
|
+
* @param {string[]} [options.ids] restrict to these provider ids
|
|
326
|
+
* @param {CapabilityRouter} [options.router]
|
|
327
|
+
* @returns {Object} map of provider id -> probe result
|
|
328
|
+
*/
|
|
329
|
+
function probeProviders({ repoRoot, projectRoot = process.cwd(), ids = null, router, execFileImpl, fsImpl = fs }: any = {}) {
|
|
330
|
+
const activeRouter = router || new CapabilityRouter({ repoRoot: repoRoot || REPO_ROOT, projectRoot });
|
|
331
|
+
const wanted = ids ? new Set(ids) : null;
|
|
332
|
+
const results: Record<string, any> = {};
|
|
333
|
+
for (const capability of Object.values<any>(activeRouter.capabilities || {})) {
|
|
334
|
+
for (const provider of capability.providers || []) {
|
|
335
|
+
if (!provider || !provider.id) continue;
|
|
336
|
+
if (wanted && !wanted.has(provider.id)) continue;
|
|
337
|
+
if (results[provider.id]) continue; // one probe per provider, however many capabilities list it
|
|
338
|
+
results[provider.id] = probeProvider(provider, { router: activeRouter, projectRoot, execFileImpl, fsImpl });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The project's build and test commands.
|
|
346
|
+
*
|
|
347
|
+
* Detection itself belongs to `command-detect.ts` (plan task C.2) and is deliberately **not**
|
|
348
|
+
* reimplemented here — two detectors would be exactly the "one verb, two implementations" defect
|
|
349
|
+
* this feature exists to remove (FR-005). This function reports what that module found, and when
|
|
350
|
+
* the module is absent it says so by name rather than guessing a command. A guessed build command
|
|
351
|
+
* is worse than an admitted gap, because it is a command the model will run.
|
|
352
|
+
*
|
|
353
|
+
* @param {Object} [options]
|
|
354
|
+
* @param {string} [options.projectRoot=process.cwd()]
|
|
355
|
+
* @param {Function} [options.detector] injection seam; defaults to the C.2 module when present
|
|
356
|
+
* @returns {{status: string, build: Object|null, test: Object|null, reason?: string}}
|
|
357
|
+
*/
|
|
358
|
+
function detectProjectCommands({ projectRoot = process.cwd(), detector }: any = {}) {
|
|
359
|
+
let detect = detector;
|
|
360
|
+
if (!detect) {
|
|
361
|
+
let mod = null;
|
|
362
|
+
try {
|
|
363
|
+
mod = require('./command-detect.ts');
|
|
364
|
+
} catch {
|
|
365
|
+
return {
|
|
366
|
+
status: 'UNKNOWN',
|
|
367
|
+
build: null,
|
|
368
|
+
test: null,
|
|
369
|
+
reason: 'src/runtime/command-detect.ts is not present in this build; DoFlow reports detected commands but does not detect them itself, and will not guess',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
detect = typeof mod.detectCommands === 'function' ? mod.detectCommands : null;
|
|
373
|
+
if (!detect) {
|
|
374
|
+
return {
|
|
375
|
+
status: 'UNKNOWN',
|
|
376
|
+
build: null,
|
|
377
|
+
test: null,
|
|
378
|
+
reason: `src/runtime/command-detect.ts exports no detectCommands() (found: ${Object.keys(mod).join(', ') || 'nothing'})`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let detected;
|
|
384
|
+
try {
|
|
385
|
+
detected = detect({ projectRoot }) || {};
|
|
386
|
+
} catch (error: any) {
|
|
387
|
+
return { status: 'UNKNOWN', build: null, test: null, reason: `command detection failed: ${error.message}` };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const commands = detected.commands || {};
|
|
391
|
+
const build = commands.build || null;
|
|
392
|
+
const test = commands.test || null;
|
|
393
|
+
// Reported per role rather than as one boolean: "test detected, build absent" is the common and
|
|
394
|
+
// correct state for a project with no build step (this repository is one), and collapsing it into
|
|
395
|
+
// a single NOT_DETECTED would read as a broken detector.
|
|
396
|
+
const status = build && test ? 'DETECTED'
|
|
397
|
+
: build || test ? 'PARTIAL'
|
|
398
|
+
: detected.manifestFound ? 'NOT_DETECTED'
|
|
399
|
+
: 'UNKNOWN';
|
|
400
|
+
const absent = Array.isArray(detected.absent) ? detected.absent : [];
|
|
401
|
+
const reason = status === 'DETECTED' ? null
|
|
402
|
+
: status === 'UNKNOWN' ? 'no project manifest was recognised under this root, so no command could be detected'
|
|
403
|
+
: `detector found no ${absent.length ? absent.join(', ') : 'build'} command declared by ${(detected.manifests || ['the project manifest']).join(', ')}`;
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
status,
|
|
407
|
+
build,
|
|
408
|
+
test,
|
|
409
|
+
manifests: detected.manifests || [],
|
|
410
|
+
absent,
|
|
411
|
+
errors: detected.errors || [],
|
|
412
|
+
...(reason ? { reason } : {}),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Roll a capability's provider probes up into one status. */
|
|
417
|
+
function capabilityStatus(probes: any) {
|
|
418
|
+
if (!probes.length) return { status: 'UNAVAILABLE', activeProvider: null };
|
|
419
|
+
const firstHealthy = probes.findIndex((probe: any) => probe.status === HEALTHY);
|
|
420
|
+
if (firstHealthy === 0) return { status: HEALTHY, activeProvider: probes[0].id };
|
|
421
|
+
if (firstHealthy > 0) return { status: 'FALLBACK', activeProvider: probes[firstHealthy].id };
|
|
422
|
+
// Nothing answered. "Installed but broken" and "not installed" are different problems: the first
|
|
423
|
+
// is actionable now, the second is the graceful degradation NFR-002 requires.
|
|
424
|
+
if (probes.some((probe: any) => probe.status === UNHEALTHY)) return { status: 'DEGRADED', activeProvider: null };
|
|
425
|
+
if (probes.every((probe: any) => probe.status === ABSENT)) return { status: 'UNAVAILABLE', activeProvider: null };
|
|
426
|
+
return { status: UNVERIFIED, activeProvider: null };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The full health model behind `doflow doctor`.
|
|
431
|
+
* @param {Object} [options]
|
|
432
|
+
* @param {string} [options.repoRoot] where the registry lives
|
|
433
|
+
* @param {string} [options.projectRoot] the project being reported on
|
|
434
|
+
* @returns {Object} report model
|
|
435
|
+
*/
|
|
436
|
+
/** The installed DoFlow source's own version, read from its package.json; null when unreadable. */
|
|
437
|
+
function installVersion(root: any, fsImpl = fs) {
|
|
438
|
+
try {
|
|
439
|
+
return JSON.parse(fsImpl.readFileSync(path.join(root, 'package.json'), 'utf8')).version || null;
|
|
440
|
+
} catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** The project's git identity — short commit and branch — or null outside a repository. */
|
|
446
|
+
function projectGitIdentity(projectRoot: any) {
|
|
447
|
+
const git = (args: any) => {
|
|
448
|
+
try {
|
|
449
|
+
return execFileSync('git', ['-C', projectRoot, ...args], {
|
|
450
|
+
encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
451
|
+
}).trim() || null;
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const commit = git(['rev-parse', '--short', 'HEAD']);
|
|
457
|
+
if (!commit) return null;
|
|
458
|
+
return { commit, branch: git(['rev-parse', '--abbrev-ref', 'HEAD']) };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** How many of the most recent recorded stage runs per stage the consultations view counts. */
|
|
462
|
+
const CONSULTATION_WINDOW = 20;
|
|
463
|
+
/** How many no-outcome entries the view lists per server and stage; the count covers the rest. */
|
|
464
|
+
const NO_OUTCOME_SHOWN = 3;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The `[MCP Servers]` data (IC-014): per server a shipped stage names, which installed harness has
|
|
468
|
+
* it and the command that adds it where it is missing. Read-only; starts no server (NFR-001).
|
|
469
|
+
* Server ids come from the registry, never from this file.
|
|
470
|
+
*/
|
|
471
|
+
function mcpServersView({ registry, projectRoot, fsImpl, adapters }: any) {
|
|
472
|
+
const named = registry.mcp.filter((server: any) => (server.steps || []).length);
|
|
473
|
+
if (!named.length) return [];
|
|
474
|
+
if (!adapters) throw new Error('no adapter registry was given');
|
|
475
|
+
const reach = mcpReach({ registry, projectRoot, fsImpl, adapters });
|
|
476
|
+
const unreadableFile = (harness: any) => reach.unreadable.find((item: any) => item.harness === harness.id && item.scope === harness.scope);
|
|
477
|
+
return named.map((server: any) => ({
|
|
478
|
+
id: server.id,
|
|
479
|
+
selection: server.selection,
|
|
480
|
+
namedBy: [...new Set(server.steps.map((step: any) => (step.expectedWhen === 'research-request' ? `${step.skill} (research request)` : step.skill)))],
|
|
481
|
+
harnesses: reach.harnesses.map((harness: any) => {
|
|
482
|
+
const state = harness.servers[server.id];
|
|
483
|
+
const row: any = { id: harness.id, scope: harness.scope, state };
|
|
484
|
+
const unreadable = state === 'unknown' ? unreadableFile(harness) : null;
|
|
485
|
+
// An unreadable config is fixed before anything is added to it (IC-014).
|
|
486
|
+
if (unreadable) row.fix = unreadable.file || `${harness.id}'s MCP config`;
|
|
487
|
+
else if (state !== 'selected' && state !== 'configured') row.add = reach.addCommand(harness.id, harness.scope, server.id);
|
|
488
|
+
return row;
|
|
489
|
+
}),
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const byText = (a: any, b: any) => (a < b ? -1 : a > b ? 1 : 0);
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The `[MCP Consultations]` data (IC-015): the recorded `consultations` of the project's run
|
|
497
|
+
* journals, grouped by calling skill, the most recent CONSULTATION_WINDOW entries per skill by
|
|
498
|
+
* `at`, counted per server. An entry without `consultations` predates the feature and is never
|
|
499
|
+
* counted (NFR-005); "no outcome" is `named` minus the servers with an outcome, never stored.
|
|
500
|
+
*/
|
|
501
|
+
function mcpConsultationsView({ projectRoot, fsImpl }: any) {
|
|
502
|
+
const dir = path.join(projectRoot, '.doflow', 'state', 'orchestration');
|
|
503
|
+
let files = [];
|
|
504
|
+
try { files = fsImpl.readdirSync(dir).filter((name: any) => name.endsWith('.json')).sort(); } catch { files = []; }
|
|
505
|
+
let unreadableRuns = 0;
|
|
506
|
+
const bySkill = new Map();
|
|
507
|
+
for (const name of files) {
|
|
508
|
+
let run;
|
|
509
|
+
try { run = JSON.parse(fsImpl.readFileSync(path.join(dir, name), 'utf8')); } catch { run = null; }
|
|
510
|
+
if (!run || typeof run !== 'object' || Array.isArray(run)) { unreadableRuns += 1; continue; }
|
|
511
|
+
for (const item of Array.isArray(run.history) ? run.history : []) {
|
|
512
|
+
const record = item && item.consultations;
|
|
513
|
+
if (!record || typeof record !== 'object' || typeof record.skill !== 'string') continue;
|
|
514
|
+
if (!bySkill.has(record.skill)) bySkill.set(record.skill, []);
|
|
515
|
+
bySkill.get(record.skill).push({ taskId: run.taskId || name.replace(/\.json$/, ''), node: item.node ?? null, at: String(item.at ?? ''), record });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const stages = [...bySkill.keys()].sort(byText).map((skill) => {
|
|
519
|
+
const kept = bySkill.get(skill)
|
|
520
|
+
.sort((a: any, b: any) => byText(b.at, a.at) || byText(a.taskId, b.taskId) || byText(String(a.node), String(b.node)))
|
|
521
|
+
.slice(0, CONSULTATION_WINDOW);
|
|
522
|
+
const counts = new Map();
|
|
523
|
+
const row = (server: any) => {
|
|
524
|
+
if (!counts.has(server)) counts.set(server, { server, used: 0, nothingUsable: 0, skipped: 0, unavailable: 0, noOutcome: 0 });
|
|
525
|
+
return counts.get(server);
|
|
526
|
+
};
|
|
527
|
+
const key: Record<string, any> = { 'consulted-used': 'used', 'consulted-nothing-usable': 'nothingUsable', skipped: 'skipped', unavailable: 'unavailable' };
|
|
528
|
+
const noOutcome = [];
|
|
529
|
+
const shown = new Map();
|
|
530
|
+
for (const { taskId, node, at, record } of kept) {
|
|
531
|
+
const outcomes = new Map();
|
|
532
|
+
for (const outcome of Array.isArray(record.outcomes) ? record.outcomes : []) {
|
|
533
|
+
if (outcome && typeof outcome.server === 'string' && key[outcome.state]) outcomes.set(outcome.server, key[outcome.state]);
|
|
534
|
+
}
|
|
535
|
+
for (const [server, field] of outcomes) row(server)[field] += 1;
|
|
536
|
+
for (const server of (Array.isArray(record.named) ? record.named : []).filter((id: any) => typeof id === 'string').sort(byText)) {
|
|
537
|
+
if (outcomes.has(server)) continue;
|
|
538
|
+
row(server).noOutcome += 1;
|
|
539
|
+
// `kept` is newest first, so the first entries per server are its latest.
|
|
540
|
+
shown.set(server, (shown.get(server) || 0) + 1);
|
|
541
|
+
if (shown.get(server) <= NO_OUTCOME_SHOWN) noOutcome.push({ taskId, node, at, server });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return { skill, runs: kept.length, servers: [...counts.values()].sort((a, b) => byText(a.server, b.server)), noOutcome };
|
|
545
|
+
});
|
|
546
|
+
return { window: CONSULTATION_WINDOW, stages, unreadableRuns };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** The doctor's `mcp` block. A servers view that cannot be built degrades to a one-line note; the
|
|
550
|
+
* sections never enter findings or change the exit code (FR-011, FR-012). */
|
|
551
|
+
function mcpView({ registry, projectRoot, fsImpl, adapters }: any) {
|
|
552
|
+
let servers = [];
|
|
553
|
+
let serversNote;
|
|
554
|
+
try { servers = mcpServersView({ registry, projectRoot, fsImpl, adapters }); } catch (error: any) { serversNote = `MCP server view unavailable: ${error.message}`; }
|
|
555
|
+
return { servers, ...(serversNote ? { serversNote } : {}), consultations: mcpConsultationsView({ projectRoot, fsImpl }) };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const MCP_STATE_MARK: Record<string, any> = Object.freeze({
|
|
559
|
+
selected: '✓ selected',
|
|
560
|
+
configured: '✓ configured',
|
|
561
|
+
'not-selected': '○ not selected',
|
|
562
|
+
'not-taken': '○ not taken',
|
|
563
|
+
unknown: '? unknown',
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
function printMcpSections(mcp: any) {
|
|
567
|
+
console.log('\n[MCP Servers] — named by shipped stages; not blocking');
|
|
568
|
+
if (mcp.serversNote) {
|
|
569
|
+
console.log(` ${mcp.serversNote}`);
|
|
570
|
+
} else if (!mcp.servers.some((server: any) => server.harnesses.length)) {
|
|
571
|
+
console.log(' No harness has DoFlow ledger rows here, so no server selection to report.');
|
|
572
|
+
} else {
|
|
573
|
+
for (const server of mcp.servers) {
|
|
574
|
+
console.log(` ${server.id.padEnd(22)}named by ${server.namedBy.join(', ')}`);
|
|
575
|
+
for (const harness of server.harnesses) {
|
|
576
|
+
// An install command reads `add: <command>`; "configure it in ..." text stands as it is.
|
|
577
|
+
const add = harness.fix ? `fix ${harness.fix} first`
|
|
578
|
+
: harness.add ? (harness.add.startsWith('doflow ') ? `add: ${harness.add}` : harness.add) : '';
|
|
579
|
+
console.log(` ${harness.id.padEnd(14)}${harness.scope.padEnd(9)}${(MCP_STATE_MARK[harness.state] || harness.state).padEnd(19)}${add}`.trimEnd());
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const { window, stages, unreadableRuns } = mcp.consultations;
|
|
585
|
+
console.log(`\n[MCP Consultations] — last ${window} recorded stage runs per stage in this project; not blocking`);
|
|
586
|
+
if (!stages.length) console.log(' No stage run in this project has recorded MCP outcomes yet.');
|
|
587
|
+
for (const item of stages) {
|
|
588
|
+
for (const row of item.servers) {
|
|
589
|
+
const counts = [
|
|
590
|
+
`${OUTCOME_LABELS['consulted-used']} ${row.used}`,
|
|
591
|
+
`${OUTCOME_LABELS['consulted-nothing-usable']} ${row.nothingUsable}`,
|
|
592
|
+
`${OUTCOME_LABELS.skipped} ${row.skipped}`,
|
|
593
|
+
`${OUTCOME_LABELS.unavailable} ${row.unavailable}`,
|
|
594
|
+
`${OUTCOME_LABELS['no-outcome']} ${row.noOutcome}`,
|
|
595
|
+
];
|
|
596
|
+
console.log(` ${item.skill.padEnd(16)}${row.server.padEnd(22)}${counts.join(' · ')}`);
|
|
597
|
+
}
|
|
598
|
+
for (const miss of item.noOutcome) {
|
|
599
|
+
console.log(` ${OUTCOME_LABELS['no-outcome']}: ${miss.taskId} ${miss.node} ${miss.at} (${miss.server})`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (unreadableRuns) console.log(` ${unreadableRuns} run file(s) could not be read`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function buildHealthReport({ repoRoot, projectRoot = process.cwd(), router, execFileImpl, fsImpl = fs, detector, adapters }: any = {}) {
|
|
606
|
+
const root = repoRoot || REPO_ROOT;
|
|
607
|
+
const registry = loadRegistry({ repoRoot: root });
|
|
608
|
+
const activeRouter = router || new CapabilityRouter({ repoRoot: root, projectRoot });
|
|
609
|
+
const probes: Record<string, any> = probeProviders({ repoRoot: root, projectRoot, router: activeRouter, execFileImpl, fsImpl });
|
|
610
|
+
const project = { root: path.resolve(projectRoot), commands: detectProjectCommands({ projectRoot, detector }) };
|
|
611
|
+
|
|
612
|
+
// `native.test` is the one provider with neither a binary nor a probe: it stands for whatever the
|
|
613
|
+
// project runs its tests with. Its detail is resolved from the detected commands so the report
|
|
614
|
+
// says which of the two very different situations applies — a test command exists but running a
|
|
615
|
+
// suite is too expensive to use as a probe, or no test command was found at all. It stays
|
|
616
|
+
// UNVERIFIED either way, because neither case establishes that it answers.
|
|
617
|
+
const projectTest = probes['native.test'];
|
|
618
|
+
if (projectTest && projectTest.status === UNVERIFIED) {
|
|
619
|
+
projectTest.detail = project.commands.test
|
|
620
|
+
? `no probe of its own; the project's test command was detected (\`${project.commands.test.command}\`), but running a suite is not a health probe`
|
|
621
|
+
: 'no probe of its own, and no project test command was detected — nothing establishes that this capability can answer';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const capabilities = Object.entries<any>(activeRouter.capabilities || {}).map(([id, capability]) => {
|
|
625
|
+
const declared = (capability.providers || []).map((provider: any) => probes[provider.id]).filter(Boolean);
|
|
626
|
+
const rolled = capabilityStatus(declared);
|
|
627
|
+
return {
|
|
628
|
+
capability: id,
|
|
629
|
+
description: capability.description,
|
|
630
|
+
status: rolled.status,
|
|
631
|
+
activeProvider: rolled.activeProvider,
|
|
632
|
+
totalProviders: declared.length,
|
|
633
|
+
providers: declared.map((probe: any) => ({ id: probe.id, status: probe.status, detail: probe.detail })),
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// External tools are the same binaries the providers use, so their status comes from the probe
|
|
638
|
+
// that already ran rather than from a second, weaker presence check.
|
|
639
|
+
const externalTools = registry.externalTools.map((tool: any) => {
|
|
640
|
+
const probe: any = Object.values(probes).find((candidate) => candidate.binary === tool.id);
|
|
641
|
+
if (probe) return { id: tool.id, displayName: tool.displayName, status: probe.status, detail: probe.detail };
|
|
642
|
+
const present = activeRouter.isBinaryAvailable(tool.id);
|
|
643
|
+
return {
|
|
644
|
+
id: tool.id,
|
|
645
|
+
displayName: tool.displayName,
|
|
646
|
+
status: present ? UNVERIFIED : ABSENT,
|
|
647
|
+
detail: present ? 'on PATH, but no capability provider declares a probe for it' : `binary '${tool.id}' is not on PATH`,
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const freshness = Object.values(probes).map((probe) => probe.freshness).filter(Boolean);
|
|
652
|
+
const reach = evaluateReach({ registry, projectRoot, fsImpl });
|
|
653
|
+
|
|
654
|
+
// A finding is a thing to act on now: something installed that does not work. An absent optional
|
|
655
|
+
// provider is not a finding (NFR-002 makes degradation normal), and a stale index still answers,
|
|
656
|
+
// so both are warnings that leave the exit code at 0.
|
|
657
|
+
const findings = [
|
|
658
|
+
...Object.values(probes)
|
|
659
|
+
.filter((probe) => probe.status === UNHEALTHY)
|
|
660
|
+
.map((probe) => ({ kind: 'provider-unhealthy', subject: probe.id, detail: probe.detail })),
|
|
661
|
+
...capabilities
|
|
662
|
+
.filter((capability) => capability.status === 'DEGRADED')
|
|
663
|
+
.map((capability) => ({ kind: 'capability-degraded', subject: capability.capability, detail: 'every installed provider for this capability failed its probe' })),
|
|
664
|
+
...reach.rows
|
|
665
|
+
.filter((row: any) => row.state === 'NO-REACH')
|
|
666
|
+
.map((row: any) => ({ kind: 'runtime-no-reach', subject: `${row.harness}:${row.scope}`, detail: `${row.reason}; fix: ${row.fix}` })),
|
|
667
|
+
];
|
|
668
|
+
const warnings = [
|
|
669
|
+
...freshness
|
|
670
|
+
.filter((item) => item.state === 'STALE')
|
|
671
|
+
.map((item) => ({ kind: 'index-stale', subject: item.providerId, detail: `${item.label} is behind the working tree; refresh with: ${item.refresh}` })),
|
|
672
|
+
...freshness
|
|
673
|
+
.filter((item) => item.state === 'UNKNOWN')
|
|
674
|
+
.map((item) => ({ kind: 'index-age-unknown', subject: item.providerId, detail: item.reason })),
|
|
675
|
+
...Object.values(probes)
|
|
676
|
+
.filter((probe) => probe.status === UNVERIFIED)
|
|
677
|
+
.map((probe) => ({ kind: 'provider-unverified', subject: probe.id, detail: probe.detail })),
|
|
678
|
+
...reach.unreadable
|
|
679
|
+
.map((item: any) => ({ kind: 'reach-ledger-unreadable', subject: `${item.scope}: ${item.ledger}`, detail: item.detail })),
|
|
680
|
+
...(project.commands.status === 'DETECTED' ? [] : [{ kind: 'project-commands', subject: 'build/test', detail: project.commands.reason }]),
|
|
681
|
+
];
|
|
682
|
+
|
|
683
|
+
// Effective identity, in one block (review A3): which DoFlow source answered, which project it
|
|
684
|
+
// was asked about, and which capabilities are actually connected — so "a clean install has a
|
|
685
|
+
// usable X" claims can be checked against what THIS machine effectively runs, rather than
|
|
686
|
+
// inferred from the pieces separately.
|
|
687
|
+
const identity = {
|
|
688
|
+
source: { root, version: installVersion(root, fsImpl) },
|
|
689
|
+
project: { root: path.resolve(projectRoot), git: projectGitIdentity(projectRoot) },
|
|
690
|
+
connectedCapabilities: capabilities.filter((capability) => capability.activeProvider).map((capability) => capability.capability),
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
identity,
|
|
695
|
+
harnesses: registry.harnesses.map((harness: any) => ({
|
|
696
|
+
id: harness.id,
|
|
697
|
+
displayName: harness.displayName,
|
|
698
|
+
status: 'PASS',
|
|
699
|
+
reach: reach.rows.filter((row: any) => row.harness === harness.id).map(({ harness: _harness, ...row }: any) => row),
|
|
700
|
+
})),
|
|
701
|
+
externalTools,
|
|
702
|
+
capabilities,
|
|
703
|
+
providers: Object.values(probes),
|
|
704
|
+
freshness,
|
|
705
|
+
project,
|
|
706
|
+
mcp: mcpView({ registry, projectRoot, fsImpl, adapters }),
|
|
707
|
+
findings,
|
|
708
|
+
warnings,
|
|
709
|
+
summary: {
|
|
710
|
+
probed: Object.keys(probes).length,
|
|
711
|
+
healthy: Object.values(probes).filter((probe) => probe.status === HEALTHY).length,
|
|
712
|
+
unhealthy: Object.values(probes).filter((probe) => probe.status === UNHEALTHY).length,
|
|
713
|
+
absent: Object.values(probes).filter((probe) => probe.status === ABSENT).length,
|
|
714
|
+
unverified: Object.values(probes).filter((probe) => probe.status === UNVERIFIED).length,
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Records the report's exit code and hands it back to the caller.
|
|
721
|
+
*
|
|
722
|
+
* Design §4.2: 0 = answered, 1 = a finding the caller must act on. An installed provider that
|
|
723
|
+
* does not answer is a finding, and so is an installed harness whose skills cannot reach a runtime
|
|
724
|
+
* (`runtime-no-reach`). An absent optional provider is the graceful degradation NFR-002 requires,
|
|
725
|
+
* and a stale index still answers, so both leave the code at 0 and appear as warnings; so do a
|
|
726
|
+
* harness with no skills at a scope (N/A) and an unreadable ledger. Setting `process.exitCode`
|
|
727
|
+
* rather than calling `process.exit` lets stdout flush.
|
|
728
|
+
* @param {number} code
|
|
729
|
+
* @returns {number}
|
|
730
|
+
*/
|
|
731
|
+
function finish(code: any) {
|
|
732
|
+
process.exitCode = code;
|
|
733
|
+
return code;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const STATUS_MARK: Record<string, any> = Object.freeze({
|
|
737
|
+
HEALTHY: '✓ HEALTHY',
|
|
738
|
+
UNHEALTHY: '✗ UNHEALTHY',
|
|
739
|
+
ABSENT: '○ ABSENT',
|
|
740
|
+
UNVERIFIED: '? UNVERIFIED',
|
|
741
|
+
FALLBACK: '▲ FALLBACK',
|
|
742
|
+
DEGRADED: '✗ DEGRADED',
|
|
743
|
+
UNAVAILABLE: '○ UNAVAILABLE',
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const REACH_MARK: Record<string, any> = Object.freeze({
|
|
747
|
+
REACHED: '✓ REACHED',
|
|
748
|
+
'NO-REACH': '✗ NO-REACH',
|
|
749
|
+
'N/A': '○ N/A',
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
const FRESHNESS_MARK: Record<string, any> = Object.freeze({
|
|
753
|
+
FRESH: '✓ FRESH',
|
|
754
|
+
STALE: '▲ STALE',
|
|
755
|
+
UNKNOWN: '? UNKNOWN',
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Handles `doflow doctor` — installation and capability health.
|
|
760
|
+
* @param {Object} options
|
|
761
|
+
* @param {boolean} [options.json=false]
|
|
762
|
+
* @param {string} [options.repoRoot]
|
|
763
|
+
* @param {string} [options.projectRoot]
|
|
764
|
+
* @returns {number} process exit code — 1 when a provider is installed but does not answer, or an
|
|
765
|
+
* installed harness's skills cannot reach a runtime
|
|
766
|
+
*/
|
|
767
|
+
async function handleDoctorCommand({ json = false, repoRoot, projectRoot = process.cwd(), ...rest }: any = {}) {
|
|
768
|
+
const report: any = buildHealthReport({ repoRoot, projectRoot, ...rest });
|
|
769
|
+
const { buildAdviceDoctor, printAdviceDoctor } = require('./advice/doctor.ts');
|
|
770
|
+
const adviceSections = await buildAdviceDoctor({ repoRoot, projectRoot, fsImpl: rest.fsImpl });
|
|
771
|
+
report.decisionModel = adviceSections.decisionModel;
|
|
772
|
+
report.advice = adviceSections.advice;
|
|
773
|
+
|
|
774
|
+
if (json) {
|
|
775
|
+
console.log(JSON.stringify(report, null, 2));
|
|
776
|
+
return finish(report.findings.length ? 1 : 0);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
console.log('\nDoFlow System Diagnostics (doflow doctor)');
|
|
780
|
+
console.log('═'.repeat(72));
|
|
781
|
+
console.log('Health means a provider answered a probe, not that a binary exists on PATH.');
|
|
782
|
+
|
|
783
|
+
console.log('\n[Effective Identity]');
|
|
784
|
+
const identity = report.identity;
|
|
785
|
+
console.log(` source ${identity.source.root}${identity.source.version ? ` (v${identity.source.version})` : ' (version unreadable)'}`);
|
|
786
|
+
console.log(` project ${identity.project.root}${identity.project.git ? ` (${identity.project.git.branch} @ ${identity.project.git.commit})` : ' (not a git repository)'}`);
|
|
787
|
+
console.log(` connected ${identity.connectedCapabilities.length ? identity.connectedCapabilities.join(', ') : 'no capability has an answering provider'}`);
|
|
788
|
+
|
|
789
|
+
console.log('\n[Harness Adapters]');
|
|
790
|
+
for (const harness of report.harnesses) console.log(` ${harness.displayName.padEnd(28)} adapter PASS`);
|
|
791
|
+
|
|
792
|
+
console.log('\n[Runtime Reach]');
|
|
793
|
+
const reachLines = report.harnesses.flatMap((harness: any) => harness.reach.map((row: any) => {
|
|
794
|
+
const detail = ({ REACHED: row.root, 'NO-REACH': `fix: ${row.fix}`, 'N/A': row.reason } as Record<string, any>)[row.state];
|
|
795
|
+
const where = row.scope === 'project' ? ` [install at ${row.installRoot}]` : '';
|
|
796
|
+
return ` ${harness.id.padEnd(14)}${row.scope.padEnd(9)}${(REACH_MARK[row.state] || row.state).padEnd(13)}${detail}${where}`;
|
|
797
|
+
}));
|
|
798
|
+
if (reachLines.length) for (const line of reachLines) console.log(line);
|
|
799
|
+
else console.log(' No harness has DoFlow ledger rows in this project or in the home directory.');
|
|
800
|
+
|
|
801
|
+
console.log('\n[Capability Providers]');
|
|
802
|
+
for (const probe of report.providers) {
|
|
803
|
+
console.log(` ${probe.id.padEnd(18)}${(STATUS_MARK[probe.status] || probe.status).padEnd(15)}${probe.detail || ''}`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
console.log('\n[Runtime Capabilities]');
|
|
807
|
+
for (const capability of report.capabilities) {
|
|
808
|
+
console.log(` ${capability.capability.padEnd(26)}${(STATUS_MARK[capability.status] || capability.status).padEnd(15)}(${capability.activeProvider || 'none answering'})`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
console.log('\n[Index Freshness]');
|
|
812
|
+
if (!report.freshness.length) {
|
|
813
|
+
console.log(' No index- or graph-backed provider answered, so there is no index to age.');
|
|
814
|
+
} else {
|
|
815
|
+
for (const item of report.freshness) {
|
|
816
|
+
console.log(` ${item.providerId.padEnd(18)}${FRESHNESS_MARK[item.state].padEnd(15)}${item.reason || item.artifact || ''}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
console.log('\n[Project Commands]');
|
|
821
|
+
const commands = report.project.commands;
|
|
822
|
+
const commandLine = (role: any) => (role
|
|
823
|
+
? `✓ ${role.command}${role.source ? ` (${role.source}, ${role.derivation || 'declared'})` : ''}`
|
|
824
|
+
: '? not detected');
|
|
825
|
+
console.log(` build ${commandLine(commands.build)}`);
|
|
826
|
+
console.log(` test ${commandLine(commands.test)}`);
|
|
827
|
+
if (commands.reason) console.log(` note ${commands.reason}`);
|
|
828
|
+
|
|
829
|
+
printMcpSections(report.mcp);
|
|
830
|
+
printAdviceDoctor(adviceSections);
|
|
831
|
+
|
|
832
|
+
if (report.warnings.length) {
|
|
833
|
+
console.log('\n[Warnings] — not blocking; a stale or unprobeable provider still degrades cleanly');
|
|
834
|
+
for (const warning of report.warnings) console.log(` ${warning.subject.padEnd(18)}${warning.detail}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
console.log('\n[Findings]');
|
|
838
|
+
if (!report.findings.length) {
|
|
839
|
+
console.log(` None. ${report.summary.healthy} of ${report.summary.probed} provider(s) answered; ${report.summary.absent} not installed, ${report.summary.unverified} unprobeable.`);
|
|
840
|
+
} else {
|
|
841
|
+
for (const finding of report.findings) console.log(` ${finding.kind.padEnd(22)}${finding.subject}: ${finding.detail}`);
|
|
842
|
+
}
|
|
843
|
+
console.log('═'.repeat(72) + '\n');
|
|
844
|
+
|
|
845
|
+
return finish(report.findings.length ? 1 : 0);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
module.exports = {
|
|
849
|
+
HEALTHY,
|
|
850
|
+
UNHEALTHY,
|
|
851
|
+
ABSENT,
|
|
852
|
+
UNVERIFIED,
|
|
853
|
+
runProbe,
|
|
854
|
+
probeProvider,
|
|
855
|
+
probeProviders,
|
|
856
|
+
probeFreshness,
|
|
857
|
+
PROVIDER_ARTIFACTS,
|
|
858
|
+
newestSourceMtime,
|
|
859
|
+
detectProjectCommands,
|
|
860
|
+
buildHealthReport,
|
|
861
|
+
handleDoctorCommand,
|
|
862
|
+
PROBE_TIMEOUT_MS,
|
|
863
|
+
};
|