@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,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* History compactor (IC-005, IC-009). Moves the body of an artifact's `## <n>. History` section into
|
|
7
|
+
* `decisions/history/<artifact>.md` and leaves a comment-and-pointer stub behind, so the live
|
|
8
|
+
* artifact stops growing with the feature's age while every History line stays reachable
|
|
9
|
+
* (NFR-004).
|
|
10
|
+
*
|
|
11
|
+
* Mechanical by design (NFR-001): no model is asked to decide what to move. A line moves unless it
|
|
12
|
+
* is blank, part of an HTML comment, the pointer line, or the initial-version literal.
|
|
13
|
+
*
|
|
14
|
+
* Write order per artifact is the safety property (FR-014): append to the archive, read it back and
|
|
15
|
+
* confirm the block is there, and only then rewrite the artifact. A failure at any step leaves the
|
|
16
|
+
* artifact as it was. A crash between the two writes leaves the block in the archive and the
|
|
17
|
+
* artifact unchanged; the next run recognises the identical chunk and does not append it twice.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const nodeFs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const HISTORY_HEADING = /^## ([0-9]+)\. History\s*$/;
|
|
24
|
+
const ANY_H2 = /^## /;
|
|
25
|
+
const POINTER = /^Earlier entries: \[decisions\/history\/[a-z-]+\.md\]/;
|
|
26
|
+
const INITIAL_VERSION = 'None — initial version.';
|
|
27
|
+
const TARGET_KEYS = ['requirement', 'design', 'specs', 'data_model', 'plan'];
|
|
28
|
+
|
|
29
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Classifies every line as `text`, `fence` (a fence marker or a line inside a fenced block),
|
|
33
|
+
* `comment` (the first line of an HTML comment) or `comment-cont`. A `## ` line inside a fence or a
|
|
34
|
+
* comment is not a heading, and fence markers inside a comment (or the reverse) do not open
|
|
35
|
+
* anything, so a History example quoted in a code block cannot be mistaken for the real section.
|
|
36
|
+
* @returns {{kinds:string[], openFence:boolean, openComment:boolean, openAt:number}} the middle two
|
|
37
|
+
* are true when a fence or an HTML comment is never closed, so everything after it was swallowed;
|
|
38
|
+
* `openAt` is the line index where that construct opened (-1 when none is left open)
|
|
39
|
+
*/
|
|
40
|
+
function scan(lines ) {
|
|
41
|
+
const kinds = new Array(lines.length);
|
|
42
|
+
let fence = null;
|
|
43
|
+
let comment = false;
|
|
44
|
+
let openedAt = -1;
|
|
45
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
46
|
+
const line = lines[i];
|
|
47
|
+
if (fence) {
|
|
48
|
+
kinds[i] = 'fence';
|
|
49
|
+
const close = /^ {0,3}(`+|~+)\s*$/.exec(line);
|
|
50
|
+
if (close && close[1][0] === fence.ch && close[1].length >= fence.len) fence = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (comment) {
|
|
54
|
+
kinds[i] = 'comment-cont';
|
|
55
|
+
if (line.includes('-->')) comment = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const open = FENCE_OPEN.exec(line);
|
|
59
|
+
if (open) { kinds[i] = 'fence'; fence = { ch: open[1][0], len: open[1].length }; openedAt = i; continue; }
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (trimmed.startsWith('<!--')) {
|
|
62
|
+
kinds[i] = 'comment';
|
|
63
|
+
if (!trimmed.includes('-->', 4)) { comment = true; openedAt = i; }
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
kinds[i] = 'text';
|
|
67
|
+
}
|
|
68
|
+
return { kinds, openFence: fence !== null, openComment: comment, openAt: fence !== null || comment ? openedAt : -1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Splits History body lines into what stays and what moves. Fenced content moves intact. Only the
|
|
73
|
+
* exact canonical pointer for this artifact is the pointer; any other line, including one that
|
|
74
|
+
* merely looks like a pointer, moves, so no text is dropped.
|
|
75
|
+
*/
|
|
76
|
+
function classify(bodyLines , kinds , canonicalPointer ) {
|
|
77
|
+
const comments = [];
|
|
78
|
+
const moved = [];
|
|
79
|
+
let pointer = null;
|
|
80
|
+
let block = null;
|
|
81
|
+
bodyLines.forEach((line, i) => {
|
|
82
|
+
const kind = kinds[i];
|
|
83
|
+
if (kind === 'comment-cont') { (block ).push(line); return; }
|
|
84
|
+
block = null;
|
|
85
|
+
if (kind === 'comment') { block = [line]; comments.push(block); return; }
|
|
86
|
+
if (kind === 'fence') { moved.push(line); return; }
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (trimmed === '') return;
|
|
89
|
+
if (trimmed === canonicalPointer) { if (pointer === null) pointer = line; return; }
|
|
90
|
+
if (trimmed === INITIAL_VERSION) return;
|
|
91
|
+
moved.push(line);
|
|
92
|
+
});
|
|
93
|
+
return { comments, moved, pointer };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeAtomic(fsImpl , file , content ) {
|
|
97
|
+
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
99
|
+
try {
|
|
100
|
+
fsImpl.writeFileSync(tmp, content, 'utf8');
|
|
101
|
+
fsImpl.renameSync(tmp, file);
|
|
102
|
+
} catch (error ) {
|
|
103
|
+
try { fsImpl.rmSync(tmp, { force: true }); } catch { /* nothing to clean */ }
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toPosix(p ) {
|
|
109
|
+
return p.split(path.sep).join('/');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Whether the archive's most recent chunk is exactly this block. Only the last chunk counts: the
|
|
114
|
+
* crash this guards against (archive written, artifact not) always leaves the interrupted chunk
|
|
115
|
+
* last. The dated header is ignored, so a recovery on a later day does not append a second copy,
|
|
116
|
+
* and a block that merely prefixes an earlier chunk does not match.
|
|
117
|
+
*
|
|
118
|
+
* A chunk header is a `## Compacted` line `scan()` classifies as text: a moved block may quote such
|
|
119
|
+
* a line inside a code fence, and that must not be read as the start of a chunk. The dedupe and the
|
|
120
|
+
* post-write confirmation both use this one predicate.
|
|
121
|
+
*/
|
|
122
|
+
function lastChunkIs(archiveText , blockLF ) {
|
|
123
|
+
const lines = archiveText.replace(/\r\n/g, '\n').split('\n');
|
|
124
|
+
const { kinds } = scan(lines);
|
|
125
|
+
let header = -1;
|
|
126
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
127
|
+
if (kinds[i] === 'text' && lines[i].startsWith('## Compacted ')) { header = i; break; }
|
|
128
|
+
}
|
|
129
|
+
if (header === -1 || lines[header + 1] !== '') return false;
|
|
130
|
+
return lines.slice(header + 2).join('\n').replace(/\n+$/, '') === blockLF;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Compacts one artifact.
|
|
135
|
+
* @returns {{artifact:string, path:string, archive:string, lines:number}|null} null when there was
|
|
136
|
+
* nothing to move
|
|
137
|
+
*/
|
|
138
|
+
function compactArtifact({ fsImpl, featureDir, file, date } ) {
|
|
139
|
+
const text = fsImpl.readFileSync(file, 'utf8');
|
|
140
|
+
const crlf = text.includes('\r\n');
|
|
141
|
+
const lines = text.split('\n');
|
|
142
|
+
const { kinds, openFence, openComment, openAt } = scan(lines);
|
|
143
|
+
const unclosed = openFence ? 'a code fence' : openComment ? 'an HTML comment' : null;
|
|
144
|
+
const refuse = () => {
|
|
145
|
+
throw new Error(`${unclosed} is never closed, so the end of the History section cannot be told`);
|
|
146
|
+
};
|
|
147
|
+
const start = lines.findIndex((l , i ) => kinds[i] === 'text' && HISTORY_HEADING.test(l));
|
|
148
|
+
// An unclosed construct swallows everything after it. It matters only when it opens before the
|
|
149
|
+
// History section ends (FR-001): then the section's end cannot be told. A History heading it
|
|
150
|
+
// swallowed is such a case; one that simply is not there, or that closed before the construct
|
|
151
|
+
// opened, is not, and the construct is treated as absent.
|
|
152
|
+
if (start === -1) {
|
|
153
|
+
if (unclosed && lines.some((l , i ) => i > openAt && HISTORY_HEADING.test(l))) refuse();
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const section = Number((HISTORY_HEADING.exec(lines[start]) )[1]);
|
|
157
|
+
let end = lines.length;
|
|
158
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
159
|
+
if (kinds[i] === 'text' && ANY_H2.test(lines[i])) { end = i; break; }
|
|
160
|
+
}
|
|
161
|
+
if (unclosed && openAt < end) refuse();
|
|
162
|
+
const name = path.basename(file);
|
|
163
|
+
const archiveFile = path.join(featureDir, 'decisions', 'history', name);
|
|
164
|
+
const link = toPosix(path.relative(path.dirname(file), archiveFile));
|
|
165
|
+
const canonicalPointer = `Earlier entries: [decisions/history/${name}](${link}).`;
|
|
166
|
+
const { comments, moved, pointer } = classify(lines.slice(start + 1, end), kinds.slice(start + 1, end), canonicalPointer);
|
|
167
|
+
if (moved.length === 0) return null;
|
|
168
|
+
|
|
169
|
+
const rel = toPosix(path.relative(featureDir, file));
|
|
170
|
+
const archiveRel = toPosix(path.relative(featureDir, archiveFile));
|
|
171
|
+
|
|
172
|
+
// Chunks are built with LF and converted to the artifact's own line ending on write, so a CRLF
|
|
173
|
+
// artifact gets a CRLF archive and no file ends up mixed.
|
|
174
|
+
const eol = crlf ? '\r\n' : '\n';
|
|
175
|
+
const toEol = (t ) => (crlf ? t.replace(/\n/g, '\r\n') : t);
|
|
176
|
+
const blockLF = moved.map((l) => l.replace(/\r$/, '')).join('\n');
|
|
177
|
+
const chunk = `## Compacted ${date} from ${rel} §${section}\n\n${blockLF}\n`;
|
|
178
|
+
const existing = fsImpl.existsSync(archiveFile) ? fsImpl.readFileSync(archiveFile, 'utf8') : null;
|
|
179
|
+
if (existing === null || !lastChunkIs(existing, blockLF)) {
|
|
180
|
+
const base = existing === null ? toEol(`# History archive: ${rel}\n`) : (existing.endsWith('\n') ? existing : `${existing}${eol}`);
|
|
181
|
+
writeAtomic(fsImpl, archiveFile, `${base}${eol}${toEol(chunk)}`);
|
|
182
|
+
}
|
|
183
|
+
if (!lastChunkIs(fsImpl.readFileSync(archiveFile, 'utf8'), blockLF)) {
|
|
184
|
+
throw new Error(`the moved block is not present in ${archiveRel} after the write`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const blank = crlf ? '\r' : '';
|
|
188
|
+
const pointerLine = pointer !== null ? pointer : `${canonicalPointer}${blank}`;
|
|
189
|
+
const body = [blank];
|
|
190
|
+
for (const c of comments) body.push(...c, blank);
|
|
191
|
+
body.push(pointerLine, end === lines.length ? '' : blank);
|
|
192
|
+
writeAtomic(fsImpl, file, [...lines.slice(0, start + 1), ...body, ...lines.slice(end)].join('\n'));
|
|
193
|
+
return { artifact: name, path: rel, archive: archiveRel, lines: moved.length };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Compacts every existing artifact among requirement, design, specs, data model and plan.
|
|
198
|
+
*
|
|
199
|
+
* @param {Object} options
|
|
200
|
+
* @param {string} options.repoRoot repository root the resolver's paths are relative to
|
|
201
|
+
* @param {string} options.featureDir absolute feature folder
|
|
202
|
+
* @param {Object} options.paths the resolver's JSON (`requirement`, `design`, `specs`,
|
|
203
|
+
* `data_model`, `plan`, repo-root-relative or null)
|
|
204
|
+
* @param {string} [options.date] YYYY-MM-DD stamped on the archive chunk; defaults to today (UTC)
|
|
205
|
+
* @param {Object} [options.fsImpl] fs implementation (tests substitute one)
|
|
206
|
+
*
|
|
207
|
+
* Not locked: concurrent callers can interleave archive appends. Call it through
|
|
208
|
+
* `compactDecisions` / `runDecision` in decision-register.js, which hold the register lock.
|
|
209
|
+
*
|
|
210
|
+
* Every artifact is attempted. One that cannot be compacted is left as it was and reported in
|
|
211
|
+
* `failed`; it does not stop the others (IC-005).
|
|
212
|
+
*
|
|
213
|
+
* @returns {{status:'compacted'|'unchanged'|'partial', moved:Object[], failed:Array<{artifact:string, path:string, message:string}>}}
|
|
214
|
+
* `partial` means at least one artifact failed
|
|
215
|
+
*/
|
|
216
|
+
function compactHistory({ repoRoot, featureDir, paths, date, fsImpl = nodeFs } ) {
|
|
217
|
+
const stamp = date || new Date().toISOString().slice(0, 10);
|
|
218
|
+
const moved = [];
|
|
219
|
+
const failed = [];
|
|
220
|
+
for (const key of TARGET_KEYS) {
|
|
221
|
+
if (!paths || !paths[key]) continue;
|
|
222
|
+
const file = path.resolve(repoRoot, paths[key]);
|
|
223
|
+
if (!fsImpl.existsSync(file)) continue;
|
|
224
|
+
let result ;
|
|
225
|
+
try {
|
|
226
|
+
result = compactArtifact({ fsImpl, featureDir, file, date: stamp });
|
|
227
|
+
} catch (error ) {
|
|
228
|
+
const name = toPosix(path.relative(featureDir, file));
|
|
229
|
+
failed.push({ artifact: path.basename(file), path: name, message: `could not compact ${name}: ${error.message}; the artifact was left unchanged` });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (result) moved.push(result);
|
|
233
|
+
}
|
|
234
|
+
return { status: failed.length ? 'partial' : moved.length ? 'compacted' : 'unchanged', moved, failed };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { compactHistory, HISTORY_HEADING, POINTER, INITIAL_VERSION };
|
|
@@ -0,0 +1,321 @@
|
|
|
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 { readReadinessRecord, newerVersionOf, RECORD_VERSION } = require('./readiness-record.js');
|
|
8
|
+
const { listCheckouts } = require('./checkouts.js');
|
|
9
|
+
const { readTaskState } = require('./task-state.js');
|
|
10
|
+
const { resolveActiveFeature } = require('./feature-resolve.js');
|
|
11
|
+
const { isSafeSlug } = require('./task-scope.js');
|
|
12
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
13
|
+
|
|
14
|
+
// The implementation gate: which stage of a workflow needs a READY readiness record before it is
|
|
15
|
+
// handed off, whether a task has one, and the words every refusal uses.
|
|
16
|
+
//
|
|
17
|
+
// The held set is read from the workflow registry, never written here: the gated stage is the
|
|
18
|
+
// first one that mutates source and carries a readiness template. The texts live here once, so the
|
|
19
|
+
// orchestrator, `verify` and the edit hook (which prints the same bytes) say the same thing.
|
|
20
|
+
|
|
21
|
+
const GATE = 'doflow gate readiness-before-implementation';
|
|
22
|
+
|
|
23
|
+
/** What clears a record a newer DoFlow wrote; `readiness` here refuses to overwrite it. */
|
|
24
|
+
const NEWER_NEXT = 'run this command with the DoFlow that wrote the record, or upgrade DoFlow here.';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The first stage that mutates source and carries a readiness template; with `editTime`, the first
|
|
28
|
+
* such stage that has not opted out of the edit-time check.
|
|
29
|
+
* @param {Array<Object>} stages a resolved workflow's stages, or a run's stage nodes
|
|
30
|
+
* @param {{editTime?: boolean}} [options]
|
|
31
|
+
* @returns {{id: string, readinessTemplate: string, editTimeGate: boolean}|null}
|
|
32
|
+
*/
|
|
33
|
+
function gatedStage(stages , { editTime = false } = {}) {
|
|
34
|
+
const stage = (stages || []).find((s ) => s && s.mutatesSource === true
|
|
35
|
+
&& typeof s.readinessTemplate === 'string' && s.readinessTemplate !== ''
|
|
36
|
+
&& (!editTime || s.editTimeGate !== false));
|
|
37
|
+
return stage ? { id: stage.id, readinessTemplate: stage.readinessTemplate, editTimeGate: stage.editTimeGate !== false } : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The classes whose workflow has a gated stage, in registry order.
|
|
42
|
+
* @param {Object} engine a WorkflowEngine
|
|
43
|
+
* @param {{editTime?: boolean}} [options]
|
|
44
|
+
* @returns {Array<string>}
|
|
45
|
+
*/
|
|
46
|
+
function heldClasses(engine , { editTime = false } = {}) {
|
|
47
|
+
return engine.listClasses().filter((cls ) => gatedStage(engine.resolveWorkflow(cls).stages, { editTime }) !== null);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The command that clears every readiness refusal. */
|
|
51
|
+
function nextCommand({ taskId, slug, template } ) {
|
|
52
|
+
const withSlug = slug && slug !== taskId ? ` --slug=${slug}` : '';
|
|
53
|
+
return `doflow-run readiness --task-class ${template} --task-id ${taskId}${withSlug}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The refusal for one check code.
|
|
58
|
+
* @param {'missing'|'not-ready'|'wrong-template'|'unusable'|'ambiguous'} code
|
|
59
|
+
* @param {Object} options
|
|
60
|
+
* @param {string} options.taskId
|
|
61
|
+
* @param {string|null} [options.slug]
|
|
62
|
+
* @param {string} options.template the template the gated stage needs
|
|
63
|
+
* @param {Object|null} [options.record] the record read, for `not-ready` and `wrong-template`
|
|
64
|
+
* @param {Array<string>} [options.candidates] the files found, for `ambiguous`
|
|
65
|
+
* @param {string|null} [options.detail] why the record cannot be used, for `unusable`
|
|
66
|
+
* @param {string|null} [options.file] the record's file, for `unusable` when a newer DoFlow wrote it
|
|
67
|
+
* @param {number|null} [options.newerVersion] that newer record version; `readiness` would refuse to
|
|
68
|
+
* replace such a record, so the text does not send the caller there
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function refusalText(code , { taskId, slug = null, template, record = null, candidates = [], detail = null, file = null, newerVersion = null } ) {
|
|
72
|
+
const next = nextCommand({ taskId, slug, template });
|
|
73
|
+
const tail = `Next: ${next}, then gather what it lists until it reports READY. Nothing was changed.`;
|
|
74
|
+
if (code === 'unusable' && newerVersion !== null) {
|
|
75
|
+
return `${GATE}: task '${taskId}' has a readiness record ${file} written by a newer DoFlow (record version ${newerVersion}; this runtime reads ${RECORD_VERSION}), `
|
|
76
|
+
+ `so this runtime cannot read it and readiness cannot replace it. Next: ${NEWER_NEXT} Nothing was changed.`;
|
|
77
|
+
}
|
|
78
|
+
switch (code) {
|
|
79
|
+
case 'missing':
|
|
80
|
+
return `${GATE}: task '${taskId}' has no readiness record for the '${template}' template. ${tail}`;
|
|
81
|
+
case 'not-ready':
|
|
82
|
+
return `${GATE}: task '${taskId}' was last evaluated ${record.state} at ${record.evaluatedAt} against the '${template}' template, not READY. ${tail}`;
|
|
83
|
+
case 'wrong-template':
|
|
84
|
+
return `${GATE}: task '${taskId}' has a READY record for the '${record.taskClass}' template, and this stage needs '${template}'. ${tail}`;
|
|
85
|
+
case 'unusable':
|
|
86
|
+
return `${GATE}: task '${taskId}' has a readiness record that cannot be used (${detail}). ${tail}`;
|
|
87
|
+
case 'ambiguous':
|
|
88
|
+
return `${GATE}: task '${taskId}' has records in more than one other checkout (${candidates.join(', ')}). Next: run the command from the checkout that holds the one you mean, or run ${next} here. Nothing was changed.`;
|
|
89
|
+
case 'ambiguous-run':
|
|
90
|
+
// A readiness record written here cannot say which run is meant, so it is not offered.
|
|
91
|
+
return `${GATE}: task '${taskId}' has runs in more than one other checkout (${candidates.join(', ')}). Next: run verify from the checkout that holds the run you mean. Nothing was changed.`;
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`no readiness refusal text for code '${code}'`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Which harnesses run this gate before each source edit and which meet it first at handoff, from the
|
|
99
|
+
* gate's own mapping in lifecycle.json (`pre-implementation-gate`): `supported`, or `different`
|
|
100
|
+
* with no prerequisite, runs it; a prerequisite (such as Codex's hook review) is named, since until
|
|
101
|
+
* it is met nothing was checked; `unavailable` has no hook layer for it. Empty when the registry
|
|
102
|
+
* cannot be read.
|
|
103
|
+
* @param {{repoRoot?: string}} [options]
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
function harnessHookNote({ repoRoot = REPO_ROOT } = {}) {
|
|
107
|
+
try {
|
|
108
|
+
const { policies } = JSON.parse(fs.readFileSync(path.join(repoRoot, 'core', 'registry', 'lifecycle.json'), 'utf8'));
|
|
109
|
+
const { mappings } = policies.find((p ) => p.id === 'pre-implementation-gate');
|
|
110
|
+
const runs = [];
|
|
111
|
+
const gated = new Map();
|
|
112
|
+
const none = [];
|
|
113
|
+
for (const [id, m] of (Object.entries(mappings) )) {
|
|
114
|
+
const prerequisites = Array.isArray(m.prerequisites) ? m.prerequisites : [];
|
|
115
|
+
if (m.status === 'unavailable' || (m.status !== 'supported' && m.status !== 'different')) none.push(id);
|
|
116
|
+
else if (prerequisites.length === 0) runs.push(id);
|
|
117
|
+
else {
|
|
118
|
+
const key = prerequisites.join(' and ');
|
|
119
|
+
gated.set(key, [...(gated.get(key) || []), id]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const parts = [];
|
|
123
|
+
if (runs.length) parts.push(`${runs.join(', ')} ${runs.length === 1 ? 'runs' : 'run'} this check before each source edit`);
|
|
124
|
+
for (const [key, ids] of gated) parts.push(`${ids.join(', ')} ${ids.length === 1 ? 'runs' : 'run'} it only after ${key}`);
|
|
125
|
+
if (none.length) parts.push(`${none.join(', ')} ${none.length === 1 ? 'has' : 'have'} no hook layer, so this refusal is ${none.length === 1 ? 'its' : 'their'} first check`);
|
|
126
|
+
return `Edit-time check: ${parts.join('; ')}.`;
|
|
127
|
+
} catch {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const isDir = (dir ) => {
|
|
133
|
+
try {
|
|
134
|
+
return fs.statSync(dir).isDirectory();
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const folderOf = (root , slug ) => path.join(root, 'agent-docs', 'doflow', slug);
|
|
140
|
+
|
|
141
|
+
/** The branch's feature slug as the resolver names it, or null. */
|
|
142
|
+
function branchFeatureSlug(cwd ) {
|
|
143
|
+
try {
|
|
144
|
+
const found = resolveActiveFeature({ projectRoot: cwd });
|
|
145
|
+
const s = !found.error && found.paths ? found.paths.feature_slug : null;
|
|
146
|
+
return isSafeSlug(s) ? s : null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The feature a task belongs to, as seen from `projectRoot`: `--slug`, else the task id when it
|
|
154
|
+
* names a feature folder there, else the branch's feature.
|
|
155
|
+
* @param {{projectRoot: string, taskId?: string|null, slug?: string|null}} options
|
|
156
|
+
* @returns {string|null}
|
|
157
|
+
*/
|
|
158
|
+
function featureSlugFor({ projectRoot, taskId = null, slug = null } ) {
|
|
159
|
+
if (slug) return slug;
|
|
160
|
+
if (isSafeSlug(taskId) && isDir(folderOf(projectRoot, taskId))) return taskId;
|
|
161
|
+
return branchFeatureSlug(projectRoot);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The feature a run belongs to: the slug recorded when it started, else its task id when that names
|
|
165
|
+
* a feature folder in the checkout holding the run. */
|
|
166
|
+
function runSlugOf(run , root ) {
|
|
167
|
+
if (run && typeof run.featureSlug === 'string' && run.featureSlug !== '') return run.featureSlug;
|
|
168
|
+
return run && isSafeSlug(run.taskId) && isDir(folderOf(root, run.taskId)) ? run.taskId : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A task's run file: in this checkout, else in exactly one other checkout whose run belongs to the
|
|
173
|
+
* same feature. Task ids repeat across features, so another checkout's run with the same id but
|
|
174
|
+
* another feature (or none) is not this task's and is never read or updated from here.
|
|
175
|
+
* @param {{stateRoot: string, taskId: string, slug: string|null, exec?: Function}} options
|
|
176
|
+
* @returns {{status: 'found'|'missing'|'ambiguous', file: string|null, root: string|null,
|
|
177
|
+
* origin: 'current'|'other'|null, candidates: Array<string>}}
|
|
178
|
+
*/
|
|
179
|
+
function findRunFile({ stateRoot, taskId, slug, exec } ) {
|
|
180
|
+
const rel = path.join('.doflow', 'state', 'orchestration', `${taskId}.json`);
|
|
181
|
+
const missing = { status: 'missing', file: null, root: null, origin: null, candidates: [] };
|
|
182
|
+
if (fs.existsSync(path.join(stateRoot, rel))) {
|
|
183
|
+
return { status: 'found', file: path.join(stateRoot, rel), root: stateRoot, origin: 'current', candidates: [] };
|
|
184
|
+
}
|
|
185
|
+
if (!slug) return missing;
|
|
186
|
+
const checkouts = listCheckouts({ cwd: stateRoot, ...(exec ? { exec } : {}) });
|
|
187
|
+
if (!checkouts.ok || checkouts.sandbox) return missing;
|
|
188
|
+
const matches = checkouts.others.filter((root ) => {
|
|
189
|
+
const file = path.join(root, rel);
|
|
190
|
+
if (!fs.existsSync(file)) return false;
|
|
191
|
+
try {
|
|
192
|
+
return runSlugOf(JSON.parse(fs.readFileSync(file, 'utf8')), root) === slug;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (matches.length === 0) return missing;
|
|
198
|
+
if (matches.length > 1) return { ...missing, status: 'ambiguous', candidates: matches.map((root ) => path.join(root, rel)) };
|
|
199
|
+
return { status: 'found', file: path.join(matches[0], rel), root: matches[0], origin: 'other', candidates: [] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The slug a refusal's next command names: one whose feature folder exists in this checkout or in
|
|
203
|
+
* the main checkout, which is when `readiness --slug` resolves it the same way the edit hook does. */
|
|
204
|
+
function nextSlugFor({ stateRoot, taskId, slug, exec } ) {
|
|
205
|
+
if (!slug || slug === taskId) return null;
|
|
206
|
+
if (isDir(folderOf(stateRoot, slug))) return slug;
|
|
207
|
+
const checkouts = listCheckouts({ cwd: stateRoot, ...(exec ? { exec } : {}) });
|
|
208
|
+
return checkouts.main && isDir(folderOf(checkouts.main, slug)) ? slug : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Why a record cannot be read as an evaluation, or null when it can. */
|
|
212
|
+
function unusableDetail(record , now ) {
|
|
213
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) return 'not a JSON object';
|
|
214
|
+
if (record.version !== RECORD_VERSION) return `version ${JSON.stringify(record.version)}, this runtime reads ${RECORD_VERSION}`;
|
|
215
|
+
for (const key of ['state', 'taskClass', 'evaluatedAt']) {
|
|
216
|
+
if (typeof record[key] !== 'string' || record[key] === '') return `no ${key}`;
|
|
217
|
+
}
|
|
218
|
+
const at = Date.parse(record.evaluatedAt);
|
|
219
|
+
if (Number.isNaN(at)) return `evaluatedAt '${record.evaluatedAt}' is not a time`;
|
|
220
|
+
if (at > now.getTime()) return `evaluated at ${record.evaluatedAt}, after this check at ${now.toISOString()}`;
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Whether the task holds a READY record of `template` made no later than `now`.
|
|
226
|
+
* @param {Object} options
|
|
227
|
+
* @param {string} options.stateRoot
|
|
228
|
+
* @param {string} options.taskId
|
|
229
|
+
* @param {string|null} [options.slug] the task's feature slug: the record's namespace, and named in
|
|
230
|
+
* the next command when its folder exists here or in the main checkout
|
|
231
|
+
* @param {string} options.template
|
|
232
|
+
* @param {Date} [options.now]
|
|
233
|
+
* @param {Function} [options.exec]
|
|
234
|
+
* @returns {{ok: boolean, code: 'ready'|'missing'|'not-ready'|'wrong-template'|'unusable'|'ambiguous',
|
|
235
|
+
* record: Object|null, file: string|null, origin: string|null, candidates: Array<string>,
|
|
236
|
+
* detail: string|null, nextSlug: string|null, message: string|null}}
|
|
237
|
+
*/
|
|
238
|
+
function checkReadiness({ stateRoot, taskId, slug = null, template, now = new Date(), exec } ) {
|
|
239
|
+
const read = readReadinessRecord({ stateRoot, taskId, slug, exec });
|
|
240
|
+
const newerVersion = read.status === 'found' ? newerVersionOf(read.record) : null;
|
|
241
|
+
const nextSlug = nextSlugFor({ stateRoot, taskId, slug, exec });
|
|
242
|
+
const result = (code , detail = null) => ({
|
|
243
|
+
ok: code === 'ready',
|
|
244
|
+
code,
|
|
245
|
+
record: read.record,
|
|
246
|
+
file: read.file,
|
|
247
|
+
origin: read.origin,
|
|
248
|
+
candidates: read.candidates,
|
|
249
|
+
detail,
|
|
250
|
+
newerVersion,
|
|
251
|
+
nextSlug,
|
|
252
|
+
message: code === 'ready' ? null : refusalText(code, {
|
|
253
|
+
taskId, slug: nextSlug, template, record: read.record, candidates: read.candidates, detail, file: read.file, newerVersion,
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
if (read.status === 'ambiguous') return result('ambiguous');
|
|
257
|
+
if (read.status === 'missing') return result('missing');
|
|
258
|
+
const detail = read.status === 'unreadable' ? read.detail : unusableDetail(read.record, now);
|
|
259
|
+
if (detail) return result('unusable', detail);
|
|
260
|
+
if (read.record.state !== 'READY') return result('not-ready');
|
|
261
|
+
if (read.record.taskClass !== template) return result('wrong-template');
|
|
262
|
+
return result('ready');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* `verify`'s readiness check: whether a run for the task, or for its feature slug, is open with its
|
|
267
|
+
* gated stage pending, and if so whether a READY record exists. Reads only.
|
|
268
|
+
* @param {Object} options
|
|
269
|
+
* @param {string} options.stateRoot
|
|
270
|
+
* @param {string} options.taskId
|
|
271
|
+
* @param {string|null} [options.slug] the feature slug, for a run keyed by it and for the record's namespace
|
|
272
|
+
* @param {Date} [options.now]
|
|
273
|
+
* @param {Function} [options.exec]
|
|
274
|
+
* @returns {Object} `{applies: false, reason}` or `{applies: true, runTaskId, stage, template, ok, code,
|
|
275
|
+
* record, message}`
|
|
276
|
+
*/
|
|
277
|
+
function verifyReadinessCheck({ stateRoot, taskId, slug = null, now = new Date(), exec } ) {
|
|
278
|
+
let found = findRunFile({ stateRoot, taskId, slug, exec });
|
|
279
|
+
if (found.status === 'missing' && slug && slug !== taskId) found = findRunFile({ stateRoot, taskId: slug, slug, exec });
|
|
280
|
+
if (found.status === 'ambiguous') {
|
|
281
|
+
return {
|
|
282
|
+
applies: true, runTaskId: null, stage: null, template: null, ok: false, code: 'ambiguous', record: null,
|
|
283
|
+
message: refusalText('ambiguous-run', { taskId, candidates: found.candidates }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (found.status === 'missing') return { applies: false, reason: 'no-run' };
|
|
287
|
+
const run = readTaskState(fs, found.file);
|
|
288
|
+
if (run.state === 'COMPLETED' || run.state === 'REJECTED') return { applies: false, reason: 'run-finished' };
|
|
289
|
+
const stageNodes = (run.program || []).filter((n ) => n.type === 'stage');
|
|
290
|
+
const stage = gatedStage(stageNodes);
|
|
291
|
+
if (!stage) return { applies: false, reason: 'no-gated-stage' };
|
|
292
|
+
const node = stageNodes.find((n ) => n.id === stage.id);
|
|
293
|
+
if (node.status === 'completed' || node.status === 'skipped') return { applies: false, reason: 'gated-stage-done' };
|
|
294
|
+
|
|
295
|
+
const template = stage.readinessTemplate;
|
|
296
|
+
const check = checkReadiness({ stateRoot, taskId: run.taskId, slug, template, now, exec });
|
|
297
|
+
return {
|
|
298
|
+
applies: true,
|
|
299
|
+
runTaskId: run.taskId,
|
|
300
|
+
stage: stage.id,
|
|
301
|
+
template,
|
|
302
|
+
ok: check.ok,
|
|
303
|
+
code: check.code,
|
|
304
|
+
record: check.record ? {
|
|
305
|
+
file: check.file, origin: check.origin, state: check.record.state ?? null, taskClass: check.record.taskClass ?? null, evaluatedAt: check.record.evaluatedAt ?? null,
|
|
306
|
+
} : null,
|
|
307
|
+
message: check.message,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
gatedStage,
|
|
313
|
+
heldClasses,
|
|
314
|
+
refusalText,
|
|
315
|
+
harnessHookNote,
|
|
316
|
+
checkReadiness,
|
|
317
|
+
verifyReadinessCheck,
|
|
318
|
+
featureSlugFor,
|
|
319
|
+
runSlugOf,
|
|
320
|
+
findRunFile,
|
|
321
|
+
};
|