@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1061 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +48 -4
- package/src/adapters/antigravity/index.js +631 -0
- package/src/adapters/claude/index.js +548 -0
- package/src/adapters/claude/mcp.js +94 -0
- package/src/adapters/codex/agents.js +208 -0
- package/src/adapters/codex/config.js +272 -0
- package/src/adapters/codex/hooks.js +203 -0
- package/src/adapters/codex/index.js +707 -0
- package/src/adapters/codex/mcp.js +271 -0
- package/src/adapters/codex/native-projection.js +34 -0
- package/src/adapters/copilot/index.js +413 -0
- package/src/adapters/copy-tree.js +620 -0
- package/src/adapters/gemini/hooks.js +210 -0
- package/src/adapters/gemini/index.js +390 -0
- package/src/adapters/hook-commands.js +60 -0
- package/src/adapters/index.js +106 -0
- package/src/adapters/kiro/index.js +310 -0
- package/src/adapters/mcp-entries.js +331 -0
- package/src/adapters/opencode/index.js +466 -0
- package/src/adapters/pi/index.js +279 -0
- package/src/adapters/pi/json-members.js +185 -0
- package/src/adapters/pi/mcp.js +373 -0
- package/src/adapters/resolve-source.js +19 -0
- package/src/cli/commands/install.js +218 -0
- package/src/cli/commands/list-backups.js +21 -0
- package/src/cli/commands/reconcile.js +120 -0
- package/src/cli/commands/remove.js +82 -0
- package/src/cli/commands/rollback.js +172 -0
- package/src/cli/commands/self-update.js +33 -0
- package/src/cli/commands/status.js +138 -0
- package/src/cli/commands/tools.js +92 -0
- package/src/cli/commands/update.js +162 -0
- package/src/cli/index.js +568 -0
- package/src/cli/runtime-commands.js +281 -0
- package/src/cli/shared.js +218 -0
- package/src/helper/bash-helper.js +34 -0
- package/src/helper/git.js +24 -0
- package/src/helper/guardrails.js +49 -0
- package/src/helper/harness-paths.js +140 -0
- package/src/helper/marker-merge.js +116 -0
- package/src/helper/prompt.js +61 -0
- package/src/helper/repo-root.js +29 -0
- package/src/helper/settings-merge.js +151 -0
- package/src/helper/settings-scope.js +33 -0
- package/src/helper/toml.js +244 -0
- package/src/install/backup-set.js +149 -0
- package/src/install/backup.js +724 -0
- package/src/install/context.js +58 -0
- package/src/install/manifest.js +145 -0
- package/src/install/mcp.js +254 -0
- package/src/install/paths.js +84 -0
- package/src/install/targets.js +90 -0
- package/src/install/tool-lifecycle.js +151 -0
- package/src/lifecycle/bash-availability.js +25 -0
- package/src/lifecycle/index.js +812 -0
- package/src/lifecycle/mcp-index.js +28 -0
- package/src/lifecycle/policies.js +73 -0
- package/src/lifecycle/view.js +175 -0
- package/src/registry/index.js +611 -0
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/sync-plugin-versions.js +66 -0
- package/src/runtime/advice/cli.js +283 -0
- package/src/runtime/advice/client.js +324 -0
- package/src/runtime/advice/config.js +189 -0
- package/src/runtime/advice/doctor.js +169 -0
- package/src/runtime/advice/endpoint-guard.js +134 -0
- package/src/runtime/advice/kinds.js +154 -0
- package/src/runtime/advice/providers/ollama-systemone.js +209 -0
- package/src/runtime/advice/record.js +166 -0
- package/src/runtime/advice/registry.js +170 -0
- package/src/runtime/advice/strength.js +66 -0
- package/src/runtime/brief-tools.js +342 -0
- package/src/runtime/c4-project.js +371 -0
- package/src/runtime/capability-router.js +484 -0
- package/src/runtime/checkouts.js +170 -0
- package/src/runtime/claims.js +468 -0
- package/src/runtime/cli-result.js +55 -0
- package/src/runtime/cli.js +678 -0
- package/src/runtime/command-detect.js +516 -0
- package/src/runtime/context-pack.js +492 -0
- package/src/runtime/decision-register.js +493 -0
- package/src/runtime/evidence-ledger.js +338 -0
- package/src/runtime/failure/capture.js +272 -0
- package/src/runtime/failure/classifier.js +68 -0
- package/src/runtime/failure/cli.js +178 -0
- package/src/runtime/failure/home.js +64 -0
- package/src/runtime/failure/store.js +226 -0
- package/src/runtime/feature-resolve.js +79 -0
- package/src/runtime/freshness.js +177 -0
- package/src/runtime/health.js +863 -0
- package/src/runtime/history-compactor.js +237 -0
- package/src/runtime/implementation-gate.js +321 -0
- package/src/runtime/inventory/coverage.js +218 -0
- package/src/runtime/inventory/identity.js +65 -0
- package/src/runtime/inventory/index.js +705 -0
- package/src/runtime/inventory/precedence.js +134 -0
- package/src/runtime/inventory/read-scopes.js +131 -0
- package/src/runtime/inventory/siblings.js +192 -0
- package/src/runtime/knowledge/chunker.js +124 -0
- package/src/runtime/knowledge/hybrid.js +149 -0
- package/src/runtime/knowledge/index-store.js +145 -0
- package/src/runtime/knowledge/retrieval.js +194 -0
- package/src/runtime/leak-scan.js +185 -0
- package/src/runtime/lifecycle/cli.js +276 -0
- package/src/runtime/lifecycle/event-store.js +340 -0
- package/src/runtime/lifecycle/fold.js +353 -0
- package/src/runtime/lifecycle/followup.js +432 -0
- package/src/runtime/lifecycle/goal.js +249 -0
- package/src/runtime/lifecycle/intent-writer.js +86 -0
- package/src/runtime/lifecycle/overview.js +292 -0
- package/src/runtime/lifecycle/release.js +241 -0
- package/src/runtime/lifecycle/report-store.js +221 -0
- package/src/runtime/lifecycle/retention.js +212 -0
- package/src/runtime/lifecycle/root.js +71 -0
- package/src/runtime/lifecycle/status.js +353 -0
- package/src/runtime/lifecycle/store-upkeep.js +393 -0
- package/src/runtime/locator-resolve.js +158 -0
- package/src/runtime/mask.js +210 -0
- package/src/runtime/mcp-consultations.js +412 -0
- package/src/runtime/mcp-reach.js +157 -0
- package/src/runtime/model-router.js +153 -0
- package/src/runtime/outcome.js +467 -0
- package/src/runtime/reach.js +148 -0
- package/src/runtime/readiness-record.js +150 -0
- package/src/runtime/readiness.js +435 -0
- package/src/runtime/recovery.js +282 -0
- package/src/runtime/research-request.js +231 -0
- package/src/runtime/retrieval-plan.js +578 -0
- package/src/runtime/scaffold/artifacts.js +217 -0
- package/src/runtime/scaffold/fingerprint.js +56 -0
- package/src/runtime/scaffold/generate.js +911 -0
- package/src/runtime/scaffold/languages.js +245 -0
- package/src/runtime/storage/cli.js +64 -0
- package/src/runtime/storage/engine.js +249 -0
- package/src/runtime/storage/registry.js +130 -0
- package/src/runtime/storage/roots.js +63 -0
- package/src/runtime/storage/rules.js +282 -0
- package/src/runtime/storage/upkeep.js +243 -0
- package/src/runtime/storage/work-status.js +326 -0
- package/src/runtime/task-attempts.js +415 -0
- package/src/runtime/task-classifier.js +550 -0
- package/src/runtime/task-scope.js +130 -0
- package/src/runtime/task-state.js +176 -0
- package/src/runtime/trace/indicators.js +235 -0
- package/src/runtime/trace/ledger.js +626 -0
- package/src/runtime/trace/render.js +76 -0
- package/src/runtime/trace/views.js +459 -0
- package/src/runtime/verification/contract-runner.js +341 -0
- package/src/runtime/verification/engine.js +1461 -0
- package/src/runtime/verification/registry.js +130 -0
- package/src/runtime/verification/scope-bound.js +418 -0
- package/src/runtime/verification/test-selection.js +172 -0
- package/src/runtime/workflow-engine.js +588 -0
- package/src/runtime/workflow-orchestrator.js +953 -0
- package/src/runtime/worktree.js +340 -0
- package/src/state/index.js +193 -0
- package/src/state/lockfile.js +136 -0
- package/src/state/run-lock.js +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Failure classifier and bounded recovery planner — the JavaScript port of
|
|
7
|
+
* `core/shared/scripts/doflow/recovery/recovery_manager.py` (plan task B.3, component C8).
|
|
8
|
+
*
|
|
9
|
+
* All **eleven** failure classes are kept, against the specification's seven (design R4, plan
|
|
10
|
+
* decision D3 and risk RK6): TOOL_FAILURE, AGENT_FAILURE, BUDGET_EXCEEDED and UNKNOWN_FAILURE are
|
|
11
|
+
* real discriminations the Python already made, and narrowing to match a document would throw away
|
|
12
|
+
* working behaviour. The classification order, every keyword, the strategy table, the
|
|
13
|
+
* agent-switch rule and the three-iteration bound are reproduced exactly.
|
|
14
|
+
*
|
|
15
|
+
* Two defects are fixed rather than reproduced, each explained at its site:
|
|
16
|
+
* 1. `plan_recovery` returned two incompatible record shapes — the abort path omitted `canRetry`
|
|
17
|
+
* and `failureClass`, so the natural caller check `result["canRetry"]` raised on exactly the
|
|
18
|
+
* path that must stop the loop.
|
|
19
|
+
* 2. `classify_failure` assumed its arguments' types; a `None` message or a bare string of check
|
|
20
|
+
* names either raised or silently iterated characters.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
24
|
+
|
|
25
|
+
/** The eleven classes, in the order the Python declared them. */
|
|
26
|
+
const FAILURE_CLASSES = Object.freeze([
|
|
27
|
+
'CONTEXT_MISSING',
|
|
28
|
+
'WRONG_IMPLEMENTATION',
|
|
29
|
+
'TEST_FAILURE',
|
|
30
|
+
'BUILD_FAILURE',
|
|
31
|
+
'ARCHITECTURE_FAILURE',
|
|
32
|
+
'REQUIREMENT_FAILURE',
|
|
33
|
+
'ENVIRONMENT_FAILURE',
|
|
34
|
+
'TOOL_FAILURE',
|
|
35
|
+
'AGENT_FAILURE',
|
|
36
|
+
'BUDGET_EXCEEDED',
|
|
37
|
+
'UNKNOWN_FAILURE',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const DEFAULT_MAX_ITERATIONS = 3;
|
|
41
|
+
|
|
42
|
+
/** Check-name fragments that identify a build-stage failure regardless of the error text. */
|
|
43
|
+
const BUILD_CHECK_MARKERS = Object.freeze(['syntax', 'compile', 'build']);
|
|
44
|
+
/** Check-name fragments that identify a test-stage failure. */
|
|
45
|
+
const TEST_CHECK_MARKERS = Object.freeze(['test', 'unit']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Per-class recovery strategy — **one entry per class, all eleven** (plan task C.2).
|
|
49
|
+
*
|
|
50
|
+
* The B.3 port preserved the Python's six-entry table, under which WRONG_IMPLEMENTATION,
|
|
51
|
+
* TOOL_FAILURE, AGENT_FAILURE, BUDGET_EXCEEDED and UNKNOWN_FAILURE all fell through to
|
|
52
|
+
* `GENERIC_DEBUGGER_FIX`. C.2 completes the table, because that fallthrough directly contradicts
|
|
53
|
+
* FR-010 — "the response MUST be the targeted action for that class" — and in two cases the generic
|
|
54
|
+
* action is not merely untargeted but wrong:
|
|
55
|
+
*
|
|
56
|
+
* - BUDGET_EXCEEDED: handing the task back to a debugger spends more of the budget that is
|
|
57
|
+
* already gone. It is the one class where the targeted action is to stop, so it is `terminal`.
|
|
58
|
+
* - TOOL_FAILURE: an unreachable MCP server or a missing provider is not fixed by rewriting code,
|
|
59
|
+
* and swapping the agent changes nothing about the tool. The action routes to a fallback
|
|
60
|
+
* provider instead (the capability router already declares those fallbacks).
|
|
61
|
+
*
|
|
62
|
+
* Classifying eleven ways and then acting six ways discarded most of what the classifier
|
|
63
|
+
* established. `GENERIC_STRATEGY` is retained and still reachable — a caller may pass a class
|
|
64
|
+
* string that is not one of the eleven — but no declared class relies on it any more.
|
|
65
|
+
*
|
|
66
|
+
* `switchAtIteration` encodes the Python's three agent-switch behaviours: `null` never switches,
|
|
67
|
+
* `0` always switches (its unconditional `True`), and `2` switches only once two attempts have
|
|
68
|
+
* already been spent (its `current_iteration >= 2`). `terminal` is new and means the class is not
|
|
69
|
+
* retryable at any iteration.
|
|
70
|
+
*/
|
|
71
|
+
const STRATEGY_MAP = Object.freeze({
|
|
72
|
+
CONTEXT_MISSING: { action: 'RETRIEVE_MORE_CONTEXT', targetRole: 'SCOUT', switchAtIteration: null },
|
|
73
|
+
WRONG_IMPLEMENTATION: { action: 'REVISE_IMPLEMENTATION', targetRole: 'IMPLEMENTER', switchAtIteration: 2 },
|
|
74
|
+
TEST_FAILURE: { action: 'TARGETED_DEBUGGER_FIX', targetRole: 'DEBUGGER', switchAtIteration: 2 },
|
|
75
|
+
BUILD_FAILURE: { action: 'TARGETED_COMPILER_FIX', targetRole: 'DEBUGGER', switchAtIteration: 2 },
|
|
76
|
+
ARCHITECTURE_FAILURE: { action: 'GRAPH_RE_EVALUATION', targetRole: 'PLANNER', switchAtIteration: 0 },
|
|
77
|
+
REQUIREMENT_FAILURE: { action: 'SPEC_RECONCILIATION', targetRole: 'PLANNER', switchAtIteration: 0 },
|
|
78
|
+
ENVIRONMENT_FAILURE: { action: 'ENVIRONMENT_DIAGNOSTIC', targetRole: 'VERIFIER', switchAtIteration: null },
|
|
79
|
+
TOOL_FAILURE: { action: 'ROUTE_TO_FALLBACK_PROVIDER', targetRole: 'VERIFIER', switchAtIteration: null },
|
|
80
|
+
// The agent itself is the failure, so the switch is the action rather than a later escalation.
|
|
81
|
+
AGENT_FAILURE: { action: 'SWITCH_AGENT_AND_RESTATE_TASK', targetRole: 'IMPLEMENTER', switchAtIteration: 0 },
|
|
82
|
+
BUDGET_EXCEEDED: { action: 'ESCALATE_TO_USER', targetRole: null, switchAtIteration: null, terminal: true },
|
|
83
|
+
// Diagnose before acting: an unclassified failure is the one case where a targeted fix cannot be
|
|
84
|
+
// chosen, and guessing one is how a recovery loop burns its whole bound on the wrong repair.
|
|
85
|
+
UNKNOWN_FAILURE: { action: 'DIAGNOSTIC_TRIAGE', targetRole: 'VERIFIER', switchAtIteration: null },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/** Reachable only for a class string outside the eleven — see the note on STRATEGY_MAP. */
|
|
89
|
+
const GENERIC_STRATEGY = Object.freeze({
|
|
90
|
+
action: 'GENERIC_DEBUGGER_FIX',
|
|
91
|
+
targetRole: 'DEBUGGER',
|
|
92
|
+
switchAtIteration: 2,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {*} value
|
|
97
|
+
* @returns {Array<string>} only the string members, lowercased
|
|
98
|
+
*/
|
|
99
|
+
function normalizeCheckNames(value ) {
|
|
100
|
+
// Defect fix (2a): the Python accepted `failed_checks` unvalidated. Handed a plain string it
|
|
101
|
+
// iterated characters, so every single-letter "check" was tested for "syntax" and none matched —
|
|
102
|
+
// a build failure silently classified as WRONG_IMPLEMENTATION.
|
|
103
|
+
if (!Array.isArray(value)) return [];
|
|
104
|
+
return value.filter((v ) => typeof v === 'string').map((v ) => v.toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class RecoveryManager {
|
|
108
|
+
maxIterations ;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {Object|number} [options] a number is accepted for parity with the Python's positional
|
|
112
|
+
* `max_iterations` argument
|
|
113
|
+
* @param {number} [options.maxIterations=3]
|
|
114
|
+
*/
|
|
115
|
+
constructor(options = {}) {
|
|
116
|
+
const opts = typeof options === 'number' ? { maxIterations: options } : options;
|
|
117
|
+
this.maxIterations = opts.maxIterations || DEFAULT_MAX_ITERATIONS;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Classifies a failure into one of the eleven classes.
|
|
122
|
+
*
|
|
123
|
+
* Order matters and is preserved: the names of the checks that failed outrank the wording of the
|
|
124
|
+
* error message, because a check name is structural evidence and an error string is prose.
|
|
125
|
+
* @param {string} errorMessage
|
|
126
|
+
* @param {Array<string>} [failedChecks=[]]
|
|
127
|
+
* @returns {string} one of FAILURE_CLASSES
|
|
128
|
+
*/
|
|
129
|
+
classifyFailure(errorMessage , failedChecks = []) {
|
|
130
|
+
const checks = normalizeCheckNames(failedChecks);
|
|
131
|
+
// Defect fix (2b): `None.lower()` raised. A classifier handed no message should return
|
|
132
|
+
// UNKNOWN_FAILURE, not abort the recovery path that exists to handle failures.
|
|
133
|
+
const msg = typeof errorMessage === 'string' ? errorMessage.toLowerCase() : '';
|
|
134
|
+
|
|
135
|
+
const anyCheck = (markers ) => checks.some((chk) => markers.some((m) => chk.includes(m)));
|
|
136
|
+
|
|
137
|
+
if (anyCheck(BUILD_CHECK_MARKERS) || msg.includes('build error')) {
|
|
138
|
+
return 'BUILD_FAILURE';
|
|
139
|
+
}
|
|
140
|
+
if (anyCheck(TEST_CHECK_MARKERS) || msg.includes('test failed') || msg.includes('assertionerror')) {
|
|
141
|
+
return 'TEST_FAILURE';
|
|
142
|
+
}
|
|
143
|
+
if (msg.includes('budget') || msg.includes('token limit')) {
|
|
144
|
+
return 'BUDGET_EXCEEDED';
|
|
145
|
+
}
|
|
146
|
+
if (msg.includes('missing file') || msg.includes('cannot find module') || msg.includes('not found')) {
|
|
147
|
+
return 'CONTEXT_MISSING';
|
|
148
|
+
}
|
|
149
|
+
if (msg.includes('timeout') || msg.includes('connection refused') || msg.includes('econnrefused')) {
|
|
150
|
+
return 'ENVIRONMENT_FAILURE';
|
|
151
|
+
}
|
|
152
|
+
if (msg.includes('tool error') || msg.includes('mcp error')) {
|
|
153
|
+
return 'TOOL_FAILURE';
|
|
154
|
+
}
|
|
155
|
+
if (msg.includes('architecture') || msg.includes('boundary')) {
|
|
156
|
+
return 'ARCHITECTURE_FAILURE';
|
|
157
|
+
}
|
|
158
|
+
if (msg.includes('requirement') || msg.includes('acceptance criterion')) {
|
|
159
|
+
return 'REQUIREMENT_FAILURE';
|
|
160
|
+
}
|
|
161
|
+
if (msg.includes('agent') || msg.includes('hallucination')) {
|
|
162
|
+
return 'AGENT_FAILURE';
|
|
163
|
+
}
|
|
164
|
+
if (checks.length > 0) {
|
|
165
|
+
return 'WRONG_IMPLEMENTATION';
|
|
166
|
+
}
|
|
167
|
+
return 'UNKNOWN_FAILURE';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Determines the next recovery action and whether to hand the task to a different agent.
|
|
172
|
+
* @param {string} failureClass
|
|
173
|
+
* @param {number} currentIteration zero-based count of retries already spent
|
|
174
|
+
* @param {string} [lastAgent='codex']
|
|
175
|
+
* @returns {{ iteration: number, failureClass: string, action: string, targetRole: string|null, agent: string, canRetry: boolean, reason?: string }}
|
|
176
|
+
*/
|
|
177
|
+
planRecovery(failureClass , currentIteration , lastAgent = 'codex') {
|
|
178
|
+
// A negative or non-numeric iteration is caller nonsense; clamping keeps it from reading as
|
|
179
|
+
// "before the first attempt" and skipping the agent switch that iteration 0 is supposed to get.
|
|
180
|
+
const iteration = Number.isFinite(currentIteration) ? Math.max(0, Math.trunc(currentIteration)) : 0;
|
|
181
|
+
const agent = lastAgent || 'codex';
|
|
182
|
+
|
|
183
|
+
if (iteration >= this.maxIterations) {
|
|
184
|
+
// Defect fix (1): the Python's abort record carried only `action`, `reason` and
|
|
185
|
+
// `switchAgent`, while its retry record carried `iteration`, `failureClass`, `targetRole`,
|
|
186
|
+
// `agent` and `canRetry`. A caller written against either shape broke on the other, and the
|
|
187
|
+
// key it most needed — `canRetry` — was the one missing from the stop path. One shape now,
|
|
188
|
+
// with `canRetry: false` stating the bound explicitly.
|
|
189
|
+
return {
|
|
190
|
+
iteration,
|
|
191
|
+
failureClass,
|
|
192
|
+
action: 'ABORT',
|
|
193
|
+
targetRole: null,
|
|
194
|
+
agent,
|
|
195
|
+
canRetry: false,
|
|
196
|
+
reason: `Maximum retry iterations (${this.maxIterations}) exceeded.`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const strategy = STRATEGY_MAP[failureClass] || GENERIC_STRATEGY;
|
|
201
|
+
|
|
202
|
+
// A terminal class stops the loop regardless of how much of the bound is left. Today that is
|
|
203
|
+
// BUDGET_EXCEEDED only: retrying is the one response guaranteed to make the failure worse.
|
|
204
|
+
if (strategy.terminal) {
|
|
205
|
+
return {
|
|
206
|
+
iteration,
|
|
207
|
+
failureClass,
|
|
208
|
+
action: strategy.action,
|
|
209
|
+
targetRole: strategy.targetRole,
|
|
210
|
+
agent,
|
|
211
|
+
canRetry: false,
|
|
212
|
+
reason: `Failure class '${failureClass}' is not retryable; a retry would repeat the condition that caused it.`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const switchAgent =
|
|
217
|
+
strategy.switchAtIteration !== null && iteration >= strategy.switchAtIteration;
|
|
218
|
+
|
|
219
|
+
// Preserved verbatim: the swap is a two-way toggle between codex and claude, so any other
|
|
220
|
+
// agent name switches to codex.
|
|
221
|
+
const recommendedAgent = switchAgent ? (agent === 'codex' ? 'claude' : 'codex') : agent;
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
iteration: iteration + 1,
|
|
225
|
+
failureClass,
|
|
226
|
+
action: strategy.action,
|
|
227
|
+
targetRole: strategy.targetRole,
|
|
228
|
+
agent: recommendedAgent,
|
|
229
|
+
canRetry: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handles `doflow recover` — classify a verification failure and return the targeted action for
|
|
236
|
+
* that class (FR-010).
|
|
237
|
+
*
|
|
238
|
+
* Exit 0 means a bounded retry is available and the plan says what to change; exit 1 means the
|
|
239
|
+
* loop must stop — the retry budget is spent, or the class is one where retrying is guaranteed to
|
|
240
|
+
* reproduce the failure. That is the distinction a caller has to branch on, so it is the one the
|
|
241
|
+
* exit code carries.
|
|
242
|
+
*
|
|
243
|
+
* @param {Object} options
|
|
244
|
+
* @param {string} options.errorMessage
|
|
245
|
+
* @param {Array<string>} [options.failedChecks]
|
|
246
|
+
* @param {number} [options.iteration=0] retries already spent
|
|
247
|
+
* @param {string} [options.agent]
|
|
248
|
+
* @param {boolean} [options.json=false]
|
|
249
|
+
* @returns {number} exit code
|
|
250
|
+
*/
|
|
251
|
+
function handleRecoverCommand({ errorMessage, failedChecks = [], iteration = 0, agent, json = false } = {}) {
|
|
252
|
+
// Handed nothing, the classifier answers UNKNOWN_FAILURE — a confident-looking verdict about a
|
|
253
|
+
// failure it never saw. Refuse instead.
|
|
254
|
+
if ((typeof errorMessage !== 'string' || errorMessage.trim() === '') && failedChecks.length === 0) {
|
|
255
|
+
return usageError('recover', '--error (and/or one or more --failed-check) is required — classifying a failure nobody described would name a class over no evidence.', json);
|
|
256
|
+
}
|
|
257
|
+
const manager = new RecoveryManager();
|
|
258
|
+
const failureClass = manager.classifyFailure(errorMessage, failedChecks);
|
|
259
|
+
const plan = manager.planRecovery(failureClass, iteration, agent || undefined);
|
|
260
|
+
|
|
261
|
+
if (json) console.log(JSON.stringify({ failureClass, failedChecks, ...(plan ) }, null, 2));
|
|
262
|
+
else {
|
|
263
|
+
console.log(`\nDoFlow Recovery [${plan.failureClass}]:`);
|
|
264
|
+
console.log('═'.repeat(78));
|
|
265
|
+
console.log(`Action: ${plan.action}`);
|
|
266
|
+
console.log(`Target: ${plan.targetRole || 'unchanged'}`);
|
|
267
|
+
console.log(`Agent: ${plan.agent}`);
|
|
268
|
+
console.log(`Retry: ${plan.canRetry ? `yes — iteration ${plan.iteration}` : 'no'}`);
|
|
269
|
+
if (plan.reason) console.log(`Reason: ${plan.reason}`);
|
|
270
|
+
console.log('═'.repeat(78) + '\n');
|
|
271
|
+
}
|
|
272
|
+
return finishRuntime(plan.canRetry ? 0 : 1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
RecoveryManager,
|
|
277
|
+
FAILURE_CLASSES,
|
|
278
|
+
STRATEGY_MAP,
|
|
279
|
+
GENERIC_STRATEGY,
|
|
280
|
+
DEFAULT_MAX_ITERATIONS,
|
|
281
|
+
handleRecoverCommand,
|
|
282
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { acquireLock, updateTaskState, readTaskState } = require('./task-state.js');
|
|
9
|
+
const { EvidenceLedger, assertSafeTaskId } = require('./evidence-ledger.js');
|
|
10
|
+
const { ClaimsManager } = require('./claims.js');
|
|
11
|
+
const { taskStoreDir } = require('./task-scope.js');
|
|
12
|
+
const { FreshnessValidator } = require('./freshness.js');
|
|
13
|
+
const { resolveLocator } = require('./locator-resolve.js');
|
|
14
|
+
const { finishRuntime, usageError } = require('./cli-result.js');
|
|
15
|
+
|
|
16
|
+
const STATES = new Set(['OPEN', 'ANSWERED', 'UNRESOLVED']);
|
|
17
|
+
const REASONS = new Set(['detected-gap', 'user-request']);
|
|
18
|
+
const safeId = (value , label ) => {
|
|
19
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) {
|
|
20
|
+
throw new Error(`${label} must be a safe non-empty identifier`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function hasLocatedSource(item , projectRoot ) {
|
|
26
|
+
const locator = item.locator || {};
|
|
27
|
+
if (locator.uri !== undefined) {
|
|
28
|
+
if (typeof locator.uri !== 'string') return false;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(locator.uri);
|
|
31
|
+
if (!['https:', 'http:'].includes(url.protocol) || !url.hostname || url.username || url.password) return false;
|
|
32
|
+
} catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
return Boolean((locator.uri && typeof locator.uri === 'string')
|
|
35
|
+
|| (locator.file && resolveLocator({ locator, repoRoot: projectRoot }).resolved));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class ResearchRequestStore {
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
constructor({ projectRoot = process.cwd(), fsImpl = fs, slug = null } = {}) {
|
|
44
|
+
this.projectRoot = projectRoot;
|
|
45
|
+
this.fsImpl = fsImpl;
|
|
46
|
+
this.slug = slug;
|
|
47
|
+
this.stateDir = path.join(projectRoot, '.doflow', 'state', 'research');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The feature's namespace directory when the task has one (task-scope.js), else the flat store. */
|
|
51
|
+
file(taskId ) {
|
|
52
|
+
const dir = taskStoreDir({ projectRoot: this.projectRoot, store: 'research', taskId: assertSafeTaskId(taskId), slug: this.slug });
|
|
53
|
+
return path.join(dir, `${taskId}.json`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
runFile(taskId ) {
|
|
57
|
+
return path.join(this.projectRoot, '.doflow', 'state', 'orchestration', `${assertSafeTaskId(taskId)}.json`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
read(taskId ) {
|
|
61
|
+
const data = readTaskState(this.fsImpl, this.file(taskId));
|
|
62
|
+
if (!data) return { taskId, revision: 0, requests: [] };
|
|
63
|
+
if (data.taskId !== taskId || !Array.isArray(data.requests)
|
|
64
|
+
|| data.requests.some((r ) => !STATES.has(r.status))) {
|
|
65
|
+
throw new Error(`Invalid research state for task '${taskId}'`);
|
|
66
|
+
}
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Re-read inside updateTaskState's lock: overlapping writers cannot lose a request.
|
|
71
|
+
change(taskId , update ) {
|
|
72
|
+
let result;
|
|
73
|
+
updateTaskState({ fsImpl: this.fsImpl, file: this.file(taskId), build: (disk ) => {
|
|
74
|
+
if (disk && (disk.taskId !== taskId || !Array.isArray(disk.requests))) {
|
|
75
|
+
throw new Error(`Invalid research state for task '${taskId}'`);
|
|
76
|
+
}
|
|
77
|
+
const requests = disk?.requests || [];
|
|
78
|
+
result = update(requests);
|
|
79
|
+
return { taskId, requests };
|
|
80
|
+
} });
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
stageFor(taskId , stageId ) {
|
|
85
|
+
const run = readTaskState(this.fsImpl, this.runFile(taskId));
|
|
86
|
+
if (!run || run.taskClass !== 'feature' || run.state !== 'RUNNING') {
|
|
87
|
+
throw new Error(`Task '${taskId}' has no active feature stage`);
|
|
88
|
+
}
|
|
89
|
+
const current = run.program[run.cursor];
|
|
90
|
+
if (current?.type !== 'stage' || current.id !== stageId) {
|
|
91
|
+
throw new Error(`Current feature stage is '${current?.type === 'stage' ? current.id : 'none'}', not '${stageId}'`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
open({ taskId, stageId, question, reason, blocking } = {}) {
|
|
96
|
+
safeId(taskId, 'taskId'); safeId(stageId, 'stageId');
|
|
97
|
+
if (typeof question !== 'string' || !question.trim()) throw new Error('question is required');
|
|
98
|
+
if (!REASONS.has(reason)) throw new Error(`reason must be one of: ${[...REASONS].join(', ')}`);
|
|
99
|
+
if (blocking !== undefined && typeof blocking !== 'boolean') throw new Error('blocking must be true or false');
|
|
100
|
+
// Lock order is run -> research for both open and completion. The stage check and request
|
|
101
|
+
// write must be indivisible with the orchestrator's final completion check.
|
|
102
|
+
const runFile = this.runFile(taskId);
|
|
103
|
+
if (!this.fsImpl.existsSync(runFile)) throw new Error(`Task '${taskId}' has no active feature stage`);
|
|
104
|
+
const release = acquireLock(this.fsImpl, runFile);
|
|
105
|
+
try {
|
|
106
|
+
this.stageFor(taskId, stageId);
|
|
107
|
+
return this.change(taskId, (requests ) => {
|
|
108
|
+
const existing = requests.find((r ) => r.stageId === stageId && r.question === question.trim() && r.status === 'OPEN');
|
|
109
|
+
if (existing) {
|
|
110
|
+
if ((blocking ?? reason === 'detected-gap') && !existing.blocking) {
|
|
111
|
+
existing.blocking = true;
|
|
112
|
+
existing.reason = reason;
|
|
113
|
+
existing.updatedAt = new Date().toISOString();
|
|
114
|
+
}
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
const at = new Date().toISOString();
|
|
118
|
+
const record = { id: `req_${crypto.randomBytes(12).toString('hex')}`, taskId, stageId,
|
|
119
|
+
question: question.trim(), reason, blocking: blocking ?? reason === 'detected-gap',
|
|
120
|
+
status: 'OPEN', claimId: null, evidenceIds: [], gap: null, createdAt: at, updatedAt: at };
|
|
121
|
+
requests.push(record);
|
|
122
|
+
return record;
|
|
123
|
+
});
|
|
124
|
+
} finally { release(); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
list(taskId ) {
|
|
128
|
+
safeId(taskId, 'taskId');
|
|
129
|
+
return this.read(taskId).requests;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate references before taking the write lock; only the status transition itself mutates state.
|
|
133
|
+
resolve({ taskId, requestId, outcome, claimId, evidenceIds = [], gap } = {}) {
|
|
134
|
+
safeId(taskId, 'taskId'); safeId(requestId, 'requestId');
|
|
135
|
+
if (!['answered', 'unresolved'].includes(outcome)) throw new Error('outcome must be answered or unresolved');
|
|
136
|
+
if (!Array.isArray(evidenceIds)) throw new Error('evidenceIds must be an array');
|
|
137
|
+
if (outcome === 'unresolved') {
|
|
138
|
+
if (!gap || !String(gap).trim()) throw new Error('unresolved requires a gap');
|
|
139
|
+
if (claimId) throw new Error('unresolved cannot carry a claim');
|
|
140
|
+
} else {
|
|
141
|
+
if (gap) throw new Error('answered cannot carry a gap');
|
|
142
|
+
if (!claimId || evidenceIds.length === 0) throw new Error('answered requires a claim and evidence IDs');
|
|
143
|
+
const ledger = new EvidenceLedger({ repoRoot: this.projectRoot, slug: this.slug });
|
|
144
|
+
ledger.load(taskId);
|
|
145
|
+
new FreshnessValidator({ repoRoot: this.projectRoot }).validateLedgerFreshness(ledger);
|
|
146
|
+
const claims = new ClaimsManager({ repoRoot: this.projectRoot, evidenceLedger: ledger, slug: this.slug });
|
|
147
|
+
claims.load(taskId); claims.evaluateAll();
|
|
148
|
+
const claim = claims.getClaim(claimId);
|
|
149
|
+
if (!claim || claim.taskId !== taskId || claim.status !== 'supported') {
|
|
150
|
+
throw new Error('answered requires a supported claim in the same task');
|
|
151
|
+
}
|
|
152
|
+
for (const id of evidenceIds) {
|
|
153
|
+
const item = ledger.getEvidence(id);
|
|
154
|
+
if (!item || item.taskId !== taskId || item.provenance !== 'extracted'
|
|
155
|
+
|| item.status === 'superseded' || item.freshness?.status !== 'FRESH'
|
|
156
|
+
|| !hasLocatedSource(item, this.projectRoot)
|
|
157
|
+
|| !claim.supportingEvidence.includes(id)) {
|
|
158
|
+
throw new Error(`Evidence '${id}' is not fresh, located support for this claim and task`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return this.change(taskId, (requests ) => {
|
|
163
|
+
const request = requests.find((r ) => r.id === requestId);
|
|
164
|
+
if (!request) throw new Error(`Unknown research request '${requestId}' for task '${taskId}'`);
|
|
165
|
+
if (request.status !== 'OPEN') throw new Error(`Request '${requestId}' is ${request.status}, not OPEN`);
|
|
166
|
+
Object.assign(request, { status: outcome === 'answered' ? 'ANSWERED' : 'UNRESOLVED',
|
|
167
|
+
claimId: claimId || null, evidenceIds: [...evidenceIds], gap: gap || null,
|
|
168
|
+
updatedAt: new Date().toISOString() });
|
|
169
|
+
return request;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
blockingGap(taskId , stageId ) {
|
|
174
|
+
const requests = this.list(taskId).filter((r ) => r.stageId === stageId && r.blocking);
|
|
175
|
+
for (const request of requests) {
|
|
176
|
+
if (request.status !== 'ANSWERED') return request;
|
|
177
|
+
try {
|
|
178
|
+
this.validateAnswer(taskId, request);
|
|
179
|
+
} catch (error ) {
|
|
180
|
+
return { ...request, failureReason: error.message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
validateAnswer(taskId , request ) {
|
|
187
|
+
const ledger = new EvidenceLedger({ repoRoot: this.projectRoot, slug: this.slug });
|
|
188
|
+
ledger.load(taskId);
|
|
189
|
+
new FreshnessValidator({ repoRoot: this.projectRoot }).validateLedgerFreshness(ledger);
|
|
190
|
+
const claims = new ClaimsManager({ repoRoot: this.projectRoot, evidenceLedger: ledger, slug: this.slug });
|
|
191
|
+
claims.load(taskId); claims.evaluateAll();
|
|
192
|
+
const claim = claims.getClaim(request.claimId);
|
|
193
|
+
if (!claim || claim.taskId !== taskId) {
|
|
194
|
+
throw new Error(`Research claim '${request.claimId}' is missing from this task`);
|
|
195
|
+
}
|
|
196
|
+
if (claim.status !== 'supported') {
|
|
197
|
+
throw new Error(`Research claim '${request.claimId}' is ${claim.status}, not supported`);
|
|
198
|
+
}
|
|
199
|
+
if (!request.evidenceIds?.length) throw new Error('Research answer has no linked evidence');
|
|
200
|
+
for (const id of request.evidenceIds) {
|
|
201
|
+
const item = ledger.getEvidence(id);
|
|
202
|
+
if (!item || item.taskId !== taskId || item.provenance !== 'extracted'
|
|
203
|
+
|| item.status === 'superseded' || item.freshness?.status !== 'FRESH'
|
|
204
|
+
|| !hasLocatedSource(item, this.projectRoot)
|
|
205
|
+
|| !claim.supportingEvidence.includes(id)) {
|
|
206
|
+
throw new Error(`Research evidence '${id}' is no longer fresh, located support`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handleResearchRequestCommand({ action = 'list', taskId, stageId, question, reason,
|
|
213
|
+
blocking, requestId, outcome, claimId, evidenceIds, gap, json = false, projectRoot, slug } = {}) {
|
|
214
|
+
try {
|
|
215
|
+
const store = new ResearchRequestStore({ projectRoot, slug });
|
|
216
|
+
let result;
|
|
217
|
+
switch (action) {
|
|
218
|
+
case 'open': result = store.open({ taskId, stageId, question, reason, blocking }); break;
|
|
219
|
+
case 'list': result = { taskId, requests: store.list(taskId) }; break;
|
|
220
|
+
case 'resolve': result = store.resolve({ taskId, requestId, outcome, claimId, evidenceIds, gap }); break;
|
|
221
|
+
default: return usageError('research-request', `unknown action '${action}'; valid: open, list, resolve`, json);
|
|
222
|
+
}
|
|
223
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
224
|
+
else console.log(JSON.stringify(result));
|
|
225
|
+
return finishRuntime(0);
|
|
226
|
+
} catch (error ) {
|
|
227
|
+
return usageError('research-request', error.message, json, error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = { ResearchRequestStore, handleResearchRequestCommand };
|