@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,243 @@
|
|
|
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 { randomBytes } = require('node:crypto');
|
|
8
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
9
|
+
const { loadStorageRegistry } = require('./registry.js');
|
|
10
|
+
const { resolveRoots, readPruneSetting } = require('./roots.js');
|
|
11
|
+
const engine = require('./engine.js');
|
|
12
|
+
const { RunLedger } = require('../trace/ledger.js');
|
|
13
|
+
|
|
14
|
+
// Load once: a not-due moment only reads the three small markers, never the catalogue again.
|
|
15
|
+
let registry ;
|
|
16
|
+
try { registry = loadStorageRegistry({ repoRoot: REPO_ROOT }); } catch { /* fail closed */ }
|
|
17
|
+
const MOMENT_ROOTS = ['project-state', 'user-state', 'config-home'];
|
|
18
|
+
|
|
19
|
+
function silently(fn ) {
|
|
20
|
+
const exitCode = process.exitCode;
|
|
21
|
+
try { if (registry) return fn(); } catch { /* upkeep must never affect its caller */ }
|
|
22
|
+
finally { process.exitCode = exitCode; }
|
|
23
|
+
}
|
|
24
|
+
function stat(file , fsImpl ) {
|
|
25
|
+
try { return fsImpl.lstatSync(file); }
|
|
26
|
+
catch (error ) { if (error.code === 'ENOENT') return null; throw error; }
|
|
27
|
+
}
|
|
28
|
+
function usableRoot(root , fsImpl ) {
|
|
29
|
+
if (!root) return false;
|
|
30
|
+
const owned = path.basename(root) === 'state' ? path.dirname(root) : root;
|
|
31
|
+
const parent = stat(owned, fsImpl), info = stat(root, fsImpl);
|
|
32
|
+
return !!parent?.isDirectory() && !parent.isSymbolicLink() && !!info?.isDirectory() && !info.isSymbolicLink();
|
|
33
|
+
}
|
|
34
|
+
function atomicWrite(file , text , fsImpl , { preserveExisting = false } = {}) {
|
|
35
|
+
const temp = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
36
|
+
try {
|
|
37
|
+
fsImpl.writeFileSync(temp, text, { flag: 'wx' });
|
|
38
|
+
if (preserveExisting) {
|
|
39
|
+
// IC-016: a link never replaces a name, so a file created since the caller's check stays.
|
|
40
|
+
try { fsImpl.linkSync(temp, file); } catch (error ) { if (error.code === 'EEXIST') return false; throw error; }
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
fsImpl.renameSync(temp, file);
|
|
44
|
+
return true;
|
|
45
|
+
} finally {
|
|
46
|
+
try { fsImpl.unlinkSync(temp); } catch { /* renamed; linked, so the name keeps the bytes; or a failed write */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function ignoreBytes() { return `${registry.settings.ignoreFile.lines.join('\n')}\n`; }
|
|
50
|
+
function ensureIgnore({ roots, rootId, projectRoot, fsImpl } ) {
|
|
51
|
+
const root = roots[rootId];
|
|
52
|
+
if (!usableRoot(root, fsImpl)) return;
|
|
53
|
+
const file = path.join(root, registry.settings.ignoreFile.name);
|
|
54
|
+
if (fsImpl.existsSync(file) || stat(file, fsImpl)) return;
|
|
55
|
+
if (!atomicWrite(file, ignoreBytes(), fsImpl, { preserveExisting: true })) return;
|
|
56
|
+
if (rootId === 'project-state' && usableRoot(roots['user-state'], fsImpl)) {
|
|
57
|
+
const index = path.join(roots['user-state'], 'upkeep-roots');
|
|
58
|
+
if (stat(index, fsImpl)?.isSymbolicLink()) return;
|
|
59
|
+
fsImpl.appendFileSync(index, `${fsImpl.realpathSync(projectRoot)}\n`, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* IC-017 reconciliation: a project whose ignore file was created before user state existed was
|
|
64
|
+
* never appended. Its own pass (at most daily) adds it once user state exists, so a global
|
|
65
|
+
* uninstall can still find its controls (IC-019).
|
|
66
|
+
*/
|
|
67
|
+
function registerProject(roots , fsImpl ) {
|
|
68
|
+
const projectState = roots['project-state'], userState = roots['user-state'];
|
|
69
|
+
if (!usableRoot(projectState, fsImpl) || !usableRoot(userState, fsImpl)) return;
|
|
70
|
+
if (!stat(path.join(projectState, registry.settings.ignoreFile.name), fsImpl)?.isFile()) return;
|
|
71
|
+
const index = path.join(userState, 'upkeep-roots');
|
|
72
|
+
const info = stat(index, fsImpl);
|
|
73
|
+
if (info && (info.isSymbolicLink() || !info.isFile())) return;
|
|
74
|
+
const project = fsImpl.realpathSync(path.dirname(path.dirname(projectState)));
|
|
75
|
+
if ((indexedRoots(index, fsImpl) || []).includes(project)) return;
|
|
76
|
+
fsImpl.appendFileSync(index, `${project}\n`, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
function indexedRoots(file , fsImpl ) {
|
|
79
|
+
const info = stat(file, fsImpl);
|
|
80
|
+
if (!info || info.isSymbolicLink() || !info.isFile()) return null;
|
|
81
|
+
return fsImpl.readFileSync(file, 'utf8').split('\n').filter((root ) => path.isAbsolute(root));
|
|
82
|
+
}
|
|
83
|
+
function rewriteRoots(roots , fsImpl , forget = null) {
|
|
84
|
+
const file = path.join(roots['user-state'], 'upkeep-roots');
|
|
85
|
+
if (!usableRoot(roots['user-state'], fsImpl)) return;
|
|
86
|
+
const lines = indexedRoots(file, fsImpl);
|
|
87
|
+
if (!lines) return;
|
|
88
|
+
const kept = forget === null
|
|
89
|
+
? [...new Set(lines)].filter(root => stat(path.join(root, '.doflow/state/.gitignore'), fsImpl))
|
|
90
|
+
: lines.filter((root ) => root !== forget);
|
|
91
|
+
atomicWrite(file, kept.length ? `${kept.join('\n')}\n` : '', fsImpl);
|
|
92
|
+
}
|
|
93
|
+
function due(root , now , fsImpl ) {
|
|
94
|
+
try {
|
|
95
|
+
const text = fsImpl.readFileSync(path.join(root, 'upkeep-due'), 'utf8').trim();
|
|
96
|
+
return !/^\d+$/.test(text) || !Number.isFinite(Number(text)) || Number(text) * 1000 <= now;
|
|
97
|
+
} catch { return true; }
|
|
98
|
+
}
|
|
99
|
+
function takeLock(root , now , fsImpl ) {
|
|
100
|
+
const lock = path.join(root, 'upkeep.lock');
|
|
101
|
+
try { fsImpl.mkdirSync(lock); return true; }
|
|
102
|
+
catch (error ) {
|
|
103
|
+
if (error.code !== 'EEXIST') throw error;
|
|
104
|
+
const info = stat(lock, fsImpl);
|
|
105
|
+
if (info?.isDirectory() && !info.isSymbolicLink() && now - info.mtimeMs > registry.settings.passLockStaleSeconds * 1000) {
|
|
106
|
+
fsImpl.rmdirSync(lock);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function writeDue(root , time , fsImpl ) {
|
|
112
|
+
const file = path.join(root, 'upkeep-due');
|
|
113
|
+
if (stat(file, fsImpl)?.isSymbolicLink()) throw new Error('linked upkeep marker');
|
|
114
|
+
atomicWrite(file, `${Math.floor(time / 1000)}\n`, fsImpl);
|
|
115
|
+
}
|
|
116
|
+
function passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled = false } ) {
|
|
117
|
+
const root = roots[rootId];
|
|
118
|
+
if (!usableRoot(root, fsImpl)) return;
|
|
119
|
+
const setting = readPruneSetting({ registry, roots, rootId, fsImpl });
|
|
120
|
+
if (setting.setting === 'off' || !takeLock(root, now, fsImpl)) return;
|
|
121
|
+
let ran = false, result, outcome = 'error';
|
|
122
|
+
const started = performance.now();
|
|
123
|
+
try {
|
|
124
|
+
if (throttled) writeDue(root, now + registry.settings.upkeepIntervalHours * 3600000, fsImpl);
|
|
125
|
+
ran = true;
|
|
126
|
+
result = engine.runPass({ registry, roots, stores, mode: 'prune', setting: { [rootId]: 'on' }, now, env, fsImpl });
|
|
127
|
+
outcome = result.partial ? 'partial' : result.stores.some((store ) => store.failed.length) ? 'error' : 'ok';
|
|
128
|
+
if (rootId === 'user-state') rewriteRoots(roots, fsImpl);
|
|
129
|
+
if (rootId === 'project-state') { try { registerProject(roots, fsImpl); } catch { /* the index is best effort */ } }
|
|
130
|
+
} catch { outcome = 'error'; /* the record never includes failure content */ }
|
|
131
|
+
finally {
|
|
132
|
+
try { fsImpl.rmdirSync(path.join(root, 'upkeep.lock')); } catch { /* best effort */ }
|
|
133
|
+
}
|
|
134
|
+
if (!ran) return;
|
|
135
|
+
if (throttled && result?.partial) {
|
|
136
|
+
try { writeDue(root, now, fsImpl); } catch { outcome = 'error'; }
|
|
137
|
+
}
|
|
138
|
+
// XDG has no run store: both user roots use the registry's existing runs-user ledger.
|
|
139
|
+
const configDir = roots[rootId === 'project-state' ? 'project-doflow' : 'user-doflow'];
|
|
140
|
+
try {
|
|
141
|
+
new RunLedger({ configDir, fsImpl, clock: () => new Date(now) }).append({
|
|
142
|
+
verb: 'storage', kind: 'upkeep', outcome, duration_ms: result?.durationMs ?? performance.now() - started,
|
|
143
|
+
});
|
|
144
|
+
} catch { /* telemetry must also be silent */ }
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** IC-009: silent throttled upkeep after a verb, using that verb's resolved store root. */
|
|
149
|
+
function upkeepAfterVerb({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
|
|
150
|
+
return silently(() => {
|
|
151
|
+
const roots = resolveRoots({ registry, projectRoot, env });
|
|
152
|
+
for (const rootId of ['project-state', 'user-state']) {
|
|
153
|
+
try { ensureIgnore({ roots, rootId, projectRoot, fsImpl }); } catch { /* continue other roots */ }
|
|
154
|
+
}
|
|
155
|
+
for (const rootId of MOMENT_ROOTS) {
|
|
156
|
+
try {
|
|
157
|
+
const root = roots[rootId];
|
|
158
|
+
if (!usableRoot(root, fsImpl) || !due(root, now, fsImpl)) continue;
|
|
159
|
+
const stores = registry.stores.filter((store ) => store.root === rootId && store.moments.includes('verb')).map((store ) => store.id);
|
|
160
|
+
passForRoot({ roots, rootId, stores, now, env, fsImpl, throttled: true });
|
|
161
|
+
} catch { /* one inaccessible root cannot stop the others */ }
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** IC-010: the verify moment is project-only and unthrottled. Wiring belongs to D.3. */
|
|
167
|
+
function upkeepAfterVerify({ projectRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
|
|
168
|
+
return silently(() => {
|
|
169
|
+
const roots = resolveRoots({ registry, projectRoot, env });
|
|
170
|
+
return passForRoot({ roots, rootId: 'project-state', stores: ['verification-logs'], now, env, fsImpl });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** IC-011: only this scope's recovery store; install/update call sites belong to D.4. */
|
|
175
|
+
function storageAfterInstall({ scope, scopeRoot, now = Date.now(), env = process.env, fsImpl = fs } = {}) {
|
|
176
|
+
return silently(() => {
|
|
177
|
+
if (!['project', 'global'].includes(scope)) return;
|
|
178
|
+
const global = scope === 'global';
|
|
179
|
+
const roots = resolveRoots({ registry, projectRoot: global ? null : scopeRoot, global, env: global ? { ...env, HOME: scopeRoot } : env });
|
|
180
|
+
const rootId = global ? 'user-state' : 'project-state';
|
|
181
|
+
try { ensureIgnore({ roots, rootId, projectRoot: scopeRoot, fsImpl }); } catch { /* pruning is independent */ }
|
|
182
|
+
return passForRoot({ roots, rootId, stores: [global ? 'recovery-user' : 'recovery-project'], now, env, fsImpl });
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** IC-019: user-initiated cleanup removes owned controls, never state or user settings. */
|
|
187
|
+
function storageForgetScope({ scope, scopeRoot, env = process.env, fsImpl = fs } = {}) {
|
|
188
|
+
const exitCode = process.exitCode;
|
|
189
|
+
let removed = 0;
|
|
190
|
+
const failures = [];
|
|
191
|
+
const attempt = (file , fn ) => {
|
|
192
|
+
try { fn(); } catch (error ) { if (error.code !== 'ENOENT') failures.push(`${file}: ${error.code || error.message}`); }
|
|
193
|
+
};
|
|
194
|
+
const removeFile = (file , exact = false) => attempt(file, () => {
|
|
195
|
+
const info = stat(file, fsImpl);
|
|
196
|
+
if (!info?.isFile() || info.isSymbolicLink()) return;
|
|
197
|
+
if (exact && fsImpl.readFileSync(file, 'utf8') !== ignoreBytes()) return;
|
|
198
|
+
fsImpl.unlinkSync(file); removed++;
|
|
199
|
+
});
|
|
200
|
+
const removeControls = (root , ignore = true) => {
|
|
201
|
+
if (!usableRoot(root, fsImpl)) return;
|
|
202
|
+
if (ignore) removeFile(path.join(root, '.gitignore'), true);
|
|
203
|
+
removeFile(path.join(root, 'upkeep-due'));
|
|
204
|
+
const lock = path.join(root, 'upkeep.lock');
|
|
205
|
+
attempt(lock, () => {
|
|
206
|
+
const info = stat(lock, fsImpl);
|
|
207
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) return;
|
|
208
|
+
fsImpl.rmdirSync(lock); removed++;
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
if (!registry || !['project', 'global'].includes(scope)) return;
|
|
213
|
+
const global = scope === 'global';
|
|
214
|
+
const roots = resolveRoots({ registry, projectRoot: global ? null : scopeRoot, global, env: global ? { ...env, HOME: scopeRoot } : env });
|
|
215
|
+
const index = path.join(roots['user-state'], 'upkeep-roots');
|
|
216
|
+
if (!global) {
|
|
217
|
+
attempt(roots['project-state'], () => removeControls(roots['project-state']));
|
|
218
|
+
attempt(index, () => {
|
|
219
|
+
let realRoot;
|
|
220
|
+
try { realRoot = fsImpl.realpathSync(scopeRoot); } catch { realRoot = path.resolve(scopeRoot); }
|
|
221
|
+
rewriteRoots(roots, fsImpl, realRoot);
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
attempt(index, () => {
|
|
225
|
+
if (!usableRoot(roots['user-state'], fsImpl)) return;
|
|
226
|
+
for (const root of new Set(indexedRoots(index, fsImpl) || [])) {
|
|
227
|
+
attempt(root, () => {
|
|
228
|
+
if (!stat(path.join(root, '.doflow/doflow.lock'), fsImpl)) removeControls(path.join(root, '.doflow/state'));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
attempt(roots['user-state'], () => removeControls(roots['user-state']));
|
|
233
|
+
attempt(roots['config-home'], () => removeControls(roots['config-home'], false));
|
|
234
|
+
if (usableRoot(roots['user-state'], fsImpl)) removeFile(index);
|
|
235
|
+
}
|
|
236
|
+
if (failures.length) for (const failure of failures) console.error(`[WARN] Storage: ${failure}`);
|
|
237
|
+
else if (removed > 0) console.log(`[INFO] Storage: removed ${removed} DoFlow control file(s)`);
|
|
238
|
+
} catch (error ) {
|
|
239
|
+
try { console.error(`[WARN] Storage: ${error.code || error.message}`); } catch { /* no exit change */ }
|
|
240
|
+
} finally { process.exitCode = exitCode; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = { upkeepAfterVerb, upkeepAfterVerify, storageAfterInstall, storageForgetScope };
|
|
@@ -0,0 +1,326 @@
|
|
|
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 { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
8
|
+
const { loadStorageRegistry } = require('./registry.js');
|
|
9
|
+
|
|
10
|
+
const RUN_STATES = new Set(['RUNNING', 'AWAITING_GATE', 'COMPLETED', 'REJECTED']);
|
|
11
|
+
const RUN_OPEN_STATES = Object.freeze(['RUNNING', 'AWAITING_GATE']);
|
|
12
|
+
const UNCHECKED = /^\s*- \[ \] /m;
|
|
13
|
+
const UNCHECKED_TASK = /^\s*- \[ \] (\S+)/gm;
|
|
14
|
+
const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
15
|
+
|
|
16
|
+
function recordStorePaths(fsImpl ) {
|
|
17
|
+
try {
|
|
18
|
+
const registry = loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl });
|
|
19
|
+
return new Map(registry.stores
|
|
20
|
+
.filter((store ) => store.root === 'project-state' && store.entry === 'record' && store.rule === 'work')
|
|
21
|
+
.map((store ) => [store.id, store.path]));
|
|
22
|
+
} catch {
|
|
23
|
+
return new Map();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createWorkCache({ projectRoot, fsImpl = fs } ) {
|
|
28
|
+
return {
|
|
29
|
+
projectRoot: path.resolve(projectRoot),
|
|
30
|
+
fsImpl,
|
|
31
|
+
recordStorePaths: recordStorePaths(fsImpl),
|
|
32
|
+
runNames: null,
|
|
33
|
+
runListError: false,
|
|
34
|
+
featureNames: null,
|
|
35
|
+
featureListError: false,
|
|
36
|
+
runs: new Map(),
|
|
37
|
+
plans: new Map(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A cache for one locked recheck: it keeps the pass's resolved store paths but drops every run,
|
|
43
|
+
* plan and listing, so the recheck reads the work's current run and plan rather than the snapshot
|
|
44
|
+
* taken at selection. A run reopened or a plan task unchecked after selection keeps the record.
|
|
45
|
+
*/
|
|
46
|
+
function refreshedWorkCache(cache ) {
|
|
47
|
+
return { ...cache, runNames: null, runListError: false, featureNames: null, featureListError: false,
|
|
48
|
+
runs: new Map(), plans: new Map() };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cacheFor({ projectRoot, fsImpl, cache } ) {
|
|
52
|
+
const resolved = path.resolve(projectRoot);
|
|
53
|
+
if (cache && cache.projectRoot === resolved && cache.fsImpl === fsImpl
|
|
54
|
+
&& cache.recordStorePaths instanceof Map && cache.runs instanceof Map && cache.plans instanceof Map) return cache;
|
|
55
|
+
return createWorkCache({ projectRoot: resolved, fsImpl });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stateRoot(projectRoot ) {
|
|
59
|
+
return path.join(projectRoot, '.doflow', 'state');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function safeSegment(value ) {
|
|
63
|
+
return typeof value === 'string' && SAFE_SEGMENT.test(value) && !value.includes('..');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function listRunNames(cache , shouldStop ) {
|
|
67
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
68
|
+
if (cache.runNames !== null) return { names: cache.runNames, incomplete: false };
|
|
69
|
+
const dir = path.join(stateRoot(cache.projectRoot), 'orchestration');
|
|
70
|
+
let entries ;
|
|
71
|
+
try { entries = cache.fsImpl.readdirSync(dir, { withFileTypes: true }); }
|
|
72
|
+
catch (error ) {
|
|
73
|
+
if (error?.code === 'ENOENT') entries = [];
|
|
74
|
+
else { cache.runNames = []; cache.runListError = true; return { names: cache.runNames, incomplete: false }; }
|
|
75
|
+
}
|
|
76
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
77
|
+
const names = [];
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
80
|
+
if (entry.name.endsWith('.json')) names.push(entry.name.slice(0, -5));
|
|
81
|
+
}
|
|
82
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
83
|
+
cache.runNames = names;
|
|
84
|
+
return { names, incomplete: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readRun(cache , runId ) {
|
|
88
|
+
if (cache.runs.has(runId)) return cache.runs.get(runId);
|
|
89
|
+
const file = path.join(stateRoot(cache.projectRoot), 'orchestration', `${runId}.json`);
|
|
90
|
+
let result ;
|
|
91
|
+
try {
|
|
92
|
+
const value = JSON.parse(cache.fsImpl.readFileSync(file, 'utf8'));
|
|
93
|
+
result = value && typeof value === 'object' && !Array.isArray(value) && RUN_STATES.has(value.state)
|
|
94
|
+
? { ok: true, value }
|
|
95
|
+
: { ok: false };
|
|
96
|
+
} catch {
|
|
97
|
+
result = { ok: false };
|
|
98
|
+
}
|
|
99
|
+
cache.runs.set(runId, result);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveRun(cache , taskId , namespace , shouldStop ) {
|
|
104
|
+
const listed = listRunNames(cache, shouldStop);
|
|
105
|
+
if (listed.incomplete) return { id: null, unreadable: false, incomplete: true };
|
|
106
|
+
if (cache.runListError) return { id: null, unreadable: true };
|
|
107
|
+
let exact = false, namespaced = false, prefix = null;
|
|
108
|
+
for (const candidate of listed.names) {
|
|
109
|
+
if (shouldStop?.()) return { id: null, unreadable: false, incomplete: true };
|
|
110
|
+
if (candidate === taskId) exact = true;
|
|
111
|
+
else if (safeSegment(namespace) && candidate === namespace) namespaced = true;
|
|
112
|
+
else if ((taskId.startsWith(`${candidate}-`) || taskId.startsWith(`${candidate}.`))
|
|
113
|
+
&& (!prefix || candidate.length > prefix.length || (candidate.length === prefix.length && candidate < prefix))) prefix = candidate;
|
|
114
|
+
}
|
|
115
|
+
const id = exact ? taskId : namespaced ? namespace : prefix;
|
|
116
|
+
if (!id) return { id: null, value: null, unreadable: false };
|
|
117
|
+
if (shouldStop?.()) return { id: null, unreadable: false, incomplete: true };
|
|
118
|
+
const read = readRun(cache, id);
|
|
119
|
+
if (shouldStop?.()) return { id: null, unreadable: false, incomplete: true };
|
|
120
|
+
return read.ok ? { id, value: read.value, unreadable: false } : { id, value: null, unreadable: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readRecord({ projectRoot, storePath, recordId, namespace, mtimeMs, fsImpl } ) {
|
|
124
|
+
const storeSegments = typeof storePath === 'string' && storePath !== '' ? storePath.split('/') : [];
|
|
125
|
+
if (typeof storePath !== 'string' || storeSegments.some((segment ) => !safeSegment(segment))
|
|
126
|
+
|| !safeSegment(recordId)
|
|
127
|
+
|| (namespace !== null && namespace !== undefined && !safeSegment(namespace))) {
|
|
128
|
+
return { ok: false };
|
|
129
|
+
}
|
|
130
|
+
const file = path.join(stateRoot(projectRoot), ...storeSegments, ...(namespace ? [namespace] : []), `${recordId}.json`);
|
|
131
|
+
try {
|
|
132
|
+
const value = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
133
|
+
const mtime = mtimeMs === undefined ? fsImpl.statSync(file).mtimeMs : mtimeMs;
|
|
134
|
+
if (!Number.isFinite(mtime)) return { ok: false };
|
|
135
|
+
return { ok: true, value, mtimeMs: mtime };
|
|
136
|
+
} catch {
|
|
137
|
+
return { ok: false };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function listFeatureNames(cache , shouldStop ) {
|
|
142
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
143
|
+
if (cache.featureNames !== null) return { names: cache.featureNames, incomplete: false };
|
|
144
|
+
const dir = path.join(cache.projectRoot, 'agent-docs', 'doflow');
|
|
145
|
+
let entries ;
|
|
146
|
+
try { entries = cache.fsImpl.readdirSync(dir, { withFileTypes: true }); }
|
|
147
|
+
catch (error ) {
|
|
148
|
+
if (error?.code === 'ENOENT') entries = [];
|
|
149
|
+
else { cache.featureNames = []; cache.featureListError = true; return { names: cache.featureNames, incomplete: false }; }
|
|
150
|
+
}
|
|
151
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
152
|
+
const names = [];
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
155
|
+
if (entry.isDirectory() && safeSegment(entry.name)) names.push(entry.name);
|
|
156
|
+
}
|
|
157
|
+
if (shouldStop?.()) return { names: [], incomplete: true };
|
|
158
|
+
cache.featureNames = names;
|
|
159
|
+
return { names, incomplete: false };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function featureFromFolders(cache , taskId , shouldStop ) {
|
|
163
|
+
const listed = listFeatureNames(cache, shouldStop);
|
|
164
|
+
if (listed.incomplete) return { feature: null, incomplete: true };
|
|
165
|
+
let best = null;
|
|
166
|
+
for (const name of listed.names) {
|
|
167
|
+
if (shouldStop?.()) return { feature: null, incomplete: true };
|
|
168
|
+
if ((taskId === name || taskId.startsWith(`${name}-`) || taskId.startsWith(`${name}.`))
|
|
169
|
+
&& (!best || name.length > best.length || (name.length === best.length && name < best))) best = name;
|
|
170
|
+
}
|
|
171
|
+
return { feature: best, incomplete: false };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function stringField(record , field ) {
|
|
175
|
+
const value = record && typeof record === 'object' ? record[field] : null;
|
|
176
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** true, false when the folder is definitely absent, or null when it cannot be told. */
|
|
180
|
+
function folderExists(fsImpl , folder ) {
|
|
181
|
+
try { fsImpl.lstatSync(folder); return true; }
|
|
182
|
+
catch (error ) { return error?.code === 'ENOENT' || error?.code === 'ENOTDIR' ? false : null; }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readPlan(cache , feature , shouldStop ) {
|
|
186
|
+
if (shouldStop?.()) return { exists: false, ok: false, text: null, incomplete: true };
|
|
187
|
+
if (cache.plans.has(feature)) return cache.plans.get(feature);
|
|
188
|
+
if (!safeSegment(feature)) {
|
|
189
|
+
const invalid = { exists: true, ok: false, text: null };
|
|
190
|
+
cache.plans.set(feature, invalid);
|
|
191
|
+
return invalid;
|
|
192
|
+
}
|
|
193
|
+
const folder = path.join(cache.projectRoot, 'agent-docs', 'doflow', feature);
|
|
194
|
+
let result ;
|
|
195
|
+
try {
|
|
196
|
+
result = { exists: true, ok: true, text: cache.fsImpl.readFileSync(path.join(folder, 'plan.md'), 'utf8'), folder: true };
|
|
197
|
+
} catch (error ) {
|
|
198
|
+
result = error?.code === 'ENOENT'
|
|
199
|
+
? { exists: false, ok: true, text: null, folder: folderExists(cache.fsImpl, folder) }
|
|
200
|
+
: { exists: true, ok: false, text: null };
|
|
201
|
+
}
|
|
202
|
+
if (result.folder === null) result = { exists: true, ok: false, text: null };
|
|
203
|
+
if (shouldStop?.()) return { exists: result.exists, ok: result.ok, text: null, incomplete: true };
|
|
204
|
+
cache.plans.set(feature, result);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function planWithUncheckedTask(cache , taskId , shouldStop ) {
|
|
209
|
+
const listed = listFeatureNames(cache, shouldStop);
|
|
210
|
+
if (listed.incomplete) return { found: false, unreadable: false, incomplete: true };
|
|
211
|
+
if (cache.featureListError) return { found: false, unreadable: true };
|
|
212
|
+
for (const feature of listed.names) {
|
|
213
|
+
if (shouldStop?.()) return { found: false, unreadable: false, incomplete: true };
|
|
214
|
+
const plan = readPlan(cache, feature, shouldStop);
|
|
215
|
+
if (plan.incomplete) return { found: false, unreadable: false, incomplete: true };
|
|
216
|
+
if (!plan.ok) return { found: false, unreadable: true };
|
|
217
|
+
if (!plan.exists) continue;
|
|
218
|
+
UNCHECKED_TASK.lastIndex = 0;
|
|
219
|
+
let match ;
|
|
220
|
+
while ((match = UNCHECKED_TASK.exec(plan.text)) !== null) {
|
|
221
|
+
if (shouldStop?.()) return { found: false, unreadable: false, incomplete: true };
|
|
222
|
+
if (match[1] === taskId) return { found: true, unreadable: false };
|
|
223
|
+
}
|
|
224
|
+
if (shouldStop?.()) return { found: false, unreadable: false, incomplete: true };
|
|
225
|
+
}
|
|
226
|
+
return { found: false, unreadable: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function answer(state , reason , run , feature , since = null) {
|
|
230
|
+
return { state, since, reason, run, feature };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function incompleteAnswer(run = null, feature = null) {
|
|
234
|
+
return { ...answer('undecidable', 'pass-budget-exhausted', run, feature), incomplete: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The one definition of whether a per-task record is still open work (IC-004).
|
|
239
|
+
* Reads fail closed: an unreadable run, record, or relevant plan is never called finished. An
|
|
240
|
+
* optional pass deadline makes discovery cancellable; incomplete results must not be treated as
|
|
241
|
+
* finished by storage selection.
|
|
242
|
+
*/
|
|
243
|
+
function workStatus({
|
|
244
|
+
projectRoot, store, taskId, namespace = null, mtimeMs, cache, fsImpl, shouldStop,
|
|
245
|
+
} ) {
|
|
246
|
+
const project = path.resolve(projectRoot);
|
|
247
|
+
const fileSystem = fsImpl || cache?.fsImpl || fs;
|
|
248
|
+
const pass = cacheFor({ projectRoot: project, fsImpl: fileSystem, cache });
|
|
249
|
+
const recordId = taskId;
|
|
250
|
+
const workId = typeof taskId === 'string' ? taskId.replace(/_claims$/, '') : taskId;
|
|
251
|
+
|
|
252
|
+
if (shouldStop?.()) return incompleteAnswer();
|
|
253
|
+
if (!safeSegment(workId)) return answer('undecidable', 'record-unreadable', null, null);
|
|
254
|
+
|
|
255
|
+
// W1-W2: an exact run wins over the namespace and delimited prefixes, even when unreadable.
|
|
256
|
+
const resolvedRun = resolveRun(pass, workId, namespace, shouldStop);
|
|
257
|
+
if (resolvedRun.incomplete || shouldStop?.()) return incompleteAnswer();
|
|
258
|
+
if (resolvedRun.unreadable) return answer('undecidable', 'run-unreadable', resolvedRun.id, null);
|
|
259
|
+
const run = resolvedRun.value;
|
|
260
|
+
|
|
261
|
+
// W3: `store` is the registry id, while the record lives under that declaration's physical
|
|
262
|
+
// path. An orchestration record is also its exact run file, so reuse the cached parse rather than
|
|
263
|
+
// reading that file twice.
|
|
264
|
+
const storePath = pass.recordStorePaths.get(store);
|
|
265
|
+
let record ;
|
|
266
|
+
if (storePath === 'orchestration' && !namespace && recordId === workId && resolvedRun.id === workId && run) {
|
|
267
|
+
try {
|
|
268
|
+
const file = path.join(stateRoot(project), storePath, `${recordId}.json`);
|
|
269
|
+
const recordMtime = mtimeMs === undefined ? fileSystem.statSync(file).mtimeMs : mtimeMs;
|
|
270
|
+
record = Number.isFinite(recordMtime)
|
|
271
|
+
? { ok: true, value: run, mtimeMs: recordMtime }
|
|
272
|
+
: { ok: false };
|
|
273
|
+
} catch {
|
|
274
|
+
record = { ok: false };
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
record = readRecord({ projectRoot: project, storePath, recordId, namespace, mtimeMs, fsImpl: fileSystem });
|
|
278
|
+
}
|
|
279
|
+
if (shouldStop?.()) return incompleteAnswer(resolvedRun.id);
|
|
280
|
+
if (!record.ok) return answer('undecidable', 'record-unreadable', resolvedRun.id, null);
|
|
281
|
+
|
|
282
|
+
// W4: old timestamps never close a live or gate-paused run.
|
|
283
|
+
if (run && RUN_OPEN_STATES.includes(run.state)) {
|
|
284
|
+
return answer('open', 'run-open', resolvedRun.id, null);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// W5: feature metadata is ordered; folder-name inference is the final fallback.
|
|
288
|
+
let feature = stringField(run, 'featureSlug')
|
|
289
|
+
|| stringField(record.value, 'featureSlug')
|
|
290
|
+
|| stringField(record.value, 'slug')
|
|
291
|
+
|| (safeSegment(namespace) ? namespace : null);
|
|
292
|
+
if (!feature) {
|
|
293
|
+
const inferred = featureFromFolders(pass, workId, shouldStop);
|
|
294
|
+
if (inferred.incomplete || shouldStop?.()) return incompleteAnswer(resolvedRun.id);
|
|
295
|
+
feature = inferred.feature;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// W6-W7 (AM22): a named feature finishes only through its plan. A feature folder without plan.md
|
|
299
|
+
// is paused before planning, so its work stays open; a named feature whose folder is gone
|
|
300
|
+
// (renamed or removed) cannot be judged, so its records are kept. Unfinished work is never
|
|
301
|
+
// pruned by age.
|
|
302
|
+
if (feature) {
|
|
303
|
+
const plan = readPlan(pass, feature, shouldStop);
|
|
304
|
+
if (plan.incomplete || shouldStop?.()) return incompleteAnswer(resolvedRun.id, feature);
|
|
305
|
+
if (!plan.ok) return answer('undecidable', 'plan-unreadable', resolvedRun.id, feature);
|
|
306
|
+
if (!plan.exists && !plan.folder) return answer('undecidable', 'feature-missing', resolvedRun.id, feature);
|
|
307
|
+
if (!plan.exists) return answer('open', 'plan-missing', resolvedRun.id, feature);
|
|
308
|
+
const unchecked = UNCHECKED.test(plan.text);
|
|
309
|
+
if (shouldStop?.()) return incompleteAnswer(resolvedRun.id, feature);
|
|
310
|
+
if (unchecked) return answer('open', 'plan-unchecked', resolvedRun.id, feature);
|
|
311
|
+
} else {
|
|
312
|
+
// W8: an exact first token is enough to associate legacy work with an unchecked task.
|
|
313
|
+
const unchecked = planWithUncheckedTask(pass, workId, shouldStop);
|
|
314
|
+
if (unchecked.incomplete || shouldStop?.()) return incompleteAnswer(resolvedRun.id);
|
|
315
|
+
if (unchecked.unreadable) return answer('undecidable', 'plan-unreadable', resolvedRun.id, null);
|
|
316
|
+
if (unchecked.found) return answer('open', 'task-unchecked', resolvedRun.id, null);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (shouldStop?.()) return incompleteAnswer(resolvedRun.id, feature);
|
|
320
|
+
// W9: invalid or absent legacy run timestamps are omitted; the record mtime remains authoritative.
|
|
321
|
+
const runEnd = run ? Date.parse(run.updatedAt) : NaN;
|
|
322
|
+
const since = Number.isFinite(runEnd) ? Math.max(runEnd, record.mtimeMs) : record.mtimeMs;
|
|
323
|
+
return answer('finished', run ? 'run-finished' : 'no-run', resolvedRun.id, feature, since);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { workStatus, createWorkCache, refreshedWorkCache, RUN_OPEN_STATES };
|