@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1057 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +46 -4
- package/src/adapters/antigravity/index.ts +631 -0
- package/src/adapters/claude/index.ts +548 -0
- package/src/adapters/claude/mcp.ts +94 -0
- package/src/adapters/codex/agents.ts +208 -0
- package/src/adapters/codex/config.ts +272 -0
- package/src/adapters/codex/hooks.ts +203 -0
- package/src/adapters/codex/index.ts +707 -0
- package/src/adapters/codex/mcp.ts +271 -0
- package/src/adapters/codex/native-projection.ts +34 -0
- package/src/adapters/copilot/index.ts +413 -0
- package/src/adapters/copy-tree.ts +620 -0
- package/src/adapters/gemini/hooks.ts +210 -0
- package/src/adapters/gemini/index.ts +390 -0
- package/src/adapters/hook-commands.ts +60 -0
- package/src/adapters/index.ts +106 -0
- package/src/adapters/kiro/index.ts +310 -0
- package/src/adapters/mcp-entries.ts +331 -0
- package/src/adapters/opencode/index.ts +466 -0
- package/src/adapters/pi/index.ts +279 -0
- package/src/adapters/pi/json-members.ts +185 -0
- package/src/adapters/pi/mcp.ts +373 -0
- package/src/adapters/resolve-source.ts +19 -0
- package/src/cli/commands/install.ts +218 -0
- package/src/cli/commands/list-backups.ts +21 -0
- package/src/cli/commands/reconcile.ts +120 -0
- package/src/cli/commands/remove.ts +82 -0
- package/src/cli/commands/rollback.ts +172 -0
- package/src/cli/commands/self-update.ts +33 -0
- package/src/cli/commands/status.ts +138 -0
- package/src/cli/commands/tools.ts +92 -0
- package/src/cli/commands/update.ts +162 -0
- package/src/cli/index.ts +568 -0
- package/src/cli/runtime-commands.ts +281 -0
- package/src/cli/shared.ts +218 -0
- package/src/helper/bash-helper.ts +34 -0
- package/src/helper/git.ts +24 -0
- package/src/helper/guardrails.ts +49 -0
- package/src/helper/harness-paths.ts +140 -0
- package/src/helper/marker-merge.ts +116 -0
- package/src/helper/prompt.ts +61 -0
- package/src/helper/repo-root.ts +29 -0
- package/src/helper/settings-merge.ts +151 -0
- package/src/helper/settings-scope.ts +33 -0
- package/src/helper/toml.ts +244 -0
- package/src/install/backup-set.ts +149 -0
- package/src/install/backup.ts +724 -0
- package/src/install/context.ts +58 -0
- package/src/install/manifest.ts +145 -0
- package/src/install/mcp.ts +254 -0
- package/src/install/paths.ts +84 -0
- package/src/install/targets.ts +90 -0
- package/src/install/tool-lifecycle.ts +151 -0
- package/src/lifecycle/bash-availability.ts +25 -0
- package/src/lifecycle/index.ts +812 -0
- package/src/lifecycle/mcp-index.ts +28 -0
- package/src/lifecycle/policies.ts +73 -0
- package/src/lifecycle/view.ts +175 -0
- package/src/registry/index.ts +611 -0
- package/src/release/sync-plugin-versions.ts +66 -0
- package/src/runtime/advice/cli.ts +283 -0
- package/src/runtime/advice/client.ts +324 -0
- package/src/runtime/advice/config.ts +189 -0
- package/src/runtime/advice/doctor.ts +169 -0
- package/src/runtime/advice/endpoint-guard.ts +134 -0
- package/src/runtime/advice/kinds.ts +154 -0
- package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
- package/src/runtime/advice/record.ts +166 -0
- package/src/runtime/advice/registry.ts +170 -0
- package/src/runtime/advice/strength.ts +66 -0
- package/src/runtime/brief-tools.ts +342 -0
- package/src/runtime/c4-project.ts +371 -0
- package/src/runtime/capability-router.ts +484 -0
- package/src/runtime/checkouts.ts +170 -0
- package/src/runtime/claims.ts +468 -0
- package/src/runtime/cli-result.ts +55 -0
- package/src/runtime/cli.ts +678 -0
- package/src/runtime/command-detect.ts +516 -0
- package/src/runtime/context-pack.ts +492 -0
- package/src/runtime/decision-register.ts +493 -0
- package/src/runtime/evidence-ledger.ts +338 -0
- package/src/runtime/failure/capture.ts +272 -0
- package/src/runtime/failure/classifier.ts +68 -0
- package/src/runtime/failure/cli.ts +178 -0
- package/src/runtime/failure/home.ts +64 -0
- package/src/runtime/failure/store.ts +226 -0
- package/src/runtime/feature-resolve.ts +79 -0
- package/src/runtime/freshness.ts +177 -0
- package/src/runtime/health.ts +863 -0
- package/src/runtime/history-compactor.ts +237 -0
- package/src/runtime/implementation-gate.ts +321 -0
- package/src/runtime/inventory/coverage.ts +218 -0
- package/src/runtime/inventory/identity.ts +65 -0
- package/src/runtime/inventory/index.ts +705 -0
- package/src/runtime/inventory/precedence.ts +134 -0
- package/src/runtime/inventory/read-scopes.ts +131 -0
- package/src/runtime/inventory/siblings.ts +192 -0
- package/src/runtime/knowledge/chunker.ts +124 -0
- package/src/runtime/knowledge/hybrid.ts +149 -0
- package/src/runtime/knowledge/index-store.ts +145 -0
- package/src/runtime/knowledge/retrieval.ts +194 -0
- package/src/runtime/leak-scan.ts +185 -0
- package/src/runtime/lifecycle/cli.ts +276 -0
- package/src/runtime/lifecycle/event-store.ts +340 -0
- package/src/runtime/lifecycle/fold.ts +353 -0
- package/src/runtime/lifecycle/followup.ts +432 -0
- package/src/runtime/lifecycle/goal.ts +249 -0
- package/src/runtime/lifecycle/intent-writer.ts +86 -0
- package/src/runtime/lifecycle/overview.ts +292 -0
- package/src/runtime/lifecycle/release.ts +241 -0
- package/src/runtime/lifecycle/report-store.ts +221 -0
- package/src/runtime/lifecycle/retention.ts +212 -0
- package/src/runtime/lifecycle/root.ts +71 -0
- package/src/runtime/lifecycle/status.ts +353 -0
- package/src/runtime/lifecycle/store-upkeep.ts +393 -0
- package/src/runtime/locator-resolve.ts +158 -0
- package/src/runtime/mask.ts +210 -0
- package/src/runtime/mcp-consultations.ts +412 -0
- package/src/runtime/mcp-reach.ts +157 -0
- package/src/runtime/model-router.ts +153 -0
- package/src/runtime/outcome.ts +467 -0
- package/src/runtime/reach.ts +148 -0
- package/src/runtime/readiness-record.ts +150 -0
- package/src/runtime/readiness.ts +435 -0
- package/src/runtime/recovery.ts +282 -0
- package/src/runtime/research-request.ts +231 -0
- package/src/runtime/retrieval-plan.ts +578 -0
- package/src/runtime/scaffold/artifacts.ts +217 -0
- package/src/runtime/scaffold/fingerprint.ts +56 -0
- package/src/runtime/scaffold/generate.ts +911 -0
- package/src/runtime/scaffold/languages.ts +245 -0
- package/src/runtime/storage/cli.ts +64 -0
- package/src/runtime/storage/engine.ts +249 -0
- package/src/runtime/storage/registry.ts +130 -0
- package/src/runtime/storage/roots.ts +63 -0
- package/src/runtime/storage/rules.ts +282 -0
- package/src/runtime/storage/upkeep.ts +243 -0
- package/src/runtime/storage/work-status.ts +326 -0
- package/src/runtime/task-attempts.ts +415 -0
- package/src/runtime/task-classifier.ts +550 -0
- package/src/runtime/task-scope.ts +130 -0
- package/src/runtime/task-state.ts +176 -0
- package/src/runtime/trace/indicators.ts +235 -0
- package/src/runtime/trace/ledger.ts +626 -0
- package/src/runtime/trace/render.ts +76 -0
- package/src/runtime/trace/views.ts +459 -0
- package/src/runtime/verification/contract-runner.ts +341 -0
- package/src/runtime/verification/engine.ts +1461 -0
- package/src/runtime/verification/registry.ts +130 -0
- package/src/runtime/verification/scope-bound.ts +418 -0
- package/src/runtime/verification/test-selection.ts +172 -0
- package/src/runtime/workflow-engine.ts +588 -0
- package/src/runtime/workflow-orchestrator.ts +953 -0
- package/src/runtime/worktree.ts +340 -0
- package/src/state/index.ts +193 -0
- package/src/state/lockfile.ts +136 -0
- package/src/state/run-lock.ts +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Store upkeep, run once by each lifecycle verb before its own work. A project whose store still
|
|
7
|
+
* lives at `agent-docs/lifecycle/events/` has it copied once into `.doflow/state/lifecycle/events/`
|
|
8
|
+
* by the first verb that runs: the files go into a temp folder beside the new store, and one
|
|
9
|
+
* rename of that folder onto `events` commits the copy, so readers see no store or the whole
|
|
10
|
+
* store. The copy then writes a completion marker beside the store; once the marker exists the old
|
|
11
|
+
* folder is never listed or read again, so a store that retention later empties is not filled from
|
|
12
|
+
* it a second time. A store that already holds event files without a marker (one this project
|
|
13
|
+
* started at the new location) gets the marker and no copy. The old folder is only read, never
|
|
14
|
+
* written, and while it exists every verb prints one line saying it is no longer read.
|
|
15
|
+
*
|
|
16
|
+
* With `DOFLOW_RETENTION_HOURS` set, or unset while the project's `prune` setting is on, it then
|
|
17
|
+
* deletes the event files of settled items older than the effective retention window (retention.js
|
|
18
|
+
* decides which). The files to delete are first listed in the retention journal, so every reader hides them from that moment, then unlinked; a pass that stops
|
|
19
|
+
* part way leaves them hidden, and the next verb finishes the removal before anything else.
|
|
20
|
+
*
|
|
21
|
+
* Nothing is locked or created unless there is work to do, so a read in a project with no store
|
|
22
|
+
* leaves the project as it was.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const nodeFs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
const { acquireLock, tryAcquireLock } = require('../task-state.ts');
|
|
28
|
+
const {
|
|
29
|
+
EVENTS_REL, LIFECYCLE_REL, JOURNAL_REL, lockTarget, isEventName, randomChars, readEvents, readJournal, StoreUnsafeError,
|
|
30
|
+
} = require('./event-store.ts');
|
|
31
|
+
const { retentionWindow, selectExpired } = require('./retention.ts');
|
|
32
|
+
|
|
33
|
+
const LEGACY_LIFECYCLE_REL = path.join('agent-docs', 'lifecycle');
|
|
34
|
+
const LEGACY_EVENTS_REL = path.join(LEGACY_LIFECYCLE_REL, 'events');
|
|
35
|
+
/** Written once the store is in place at the new location; its presence alone is what counts. */
|
|
36
|
+
const MARKER_REL = path.join(LIFECYCLE_REL, 'migrated.json');
|
|
37
|
+
const MIGRATING_PREFIX = 'events.migrating-';
|
|
38
|
+
const NOTICE = 'note: the lifecycle store is now .doflow/state/lifecycle/events; agent-docs/lifecycle/ is no longer read and can be deleted';
|
|
39
|
+
const removedLine = (count: any, hours: any) => `retention: removed ${count} event files older than ${hours} h`;
|
|
40
|
+
const invalidLine = (raw: any) => `warning: DOFLOW_RETENTION_HOURS='${raw}' is not a positive whole number of hours; ignored, nothing removed`;
|
|
41
|
+
/** Bounds one pass, so its journal stays far below the reader's cap and its lock is held briefly. */
|
|
42
|
+
const MAX_PASS_FILES = 20000;
|
|
43
|
+
/** Unlinks between two refreshes of the lock's age. */
|
|
44
|
+
const REFRESH_EVERY = 500;
|
|
45
|
+
const unwritableLine = (code: any) => `warning: retention skipped: .doflow/state/lifecycle cannot be written (${code}); nothing was hidden or removed`;
|
|
46
|
+
const unlinkLine = (code: any) => `warning: retention could not remove every file (${code}); they stay hidden and the next lifecycle command finishes the removal`;
|
|
47
|
+
|
|
48
|
+
function lstatOrNull(fsImpl: any, file: any) {
|
|
49
|
+
try { return fsImpl.lstatSync(file); } catch { return null; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The names in a folder, or none when it is absent or not a folder. */
|
|
53
|
+
function namesIn(fsImpl: any, dir: any) {
|
|
54
|
+
try {
|
|
55
|
+
return fsImpl.readdirSync(dir).sort();
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return [];
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function refuseLinks(fsImpl: any, root: any, rels: any) {
|
|
63
|
+
for (const rel of rels) {
|
|
64
|
+
const folder = path.join(root, rel);
|
|
65
|
+
const st = lstatOrNull(fsImpl, folder);
|
|
66
|
+
if (st && st.isSymbolicLink()) throw new StoreUnsafeError(`${folder} is a symbolic link; the lifecycle store is never read or written through one`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The event files of the old folder that a copy takes: event-named regular files, nothing else. */
|
|
71
|
+
function legacyEventFiles(fsImpl: any, root: any) {
|
|
72
|
+
const dir = path.join(root, LEGACY_EVENTS_REL);
|
|
73
|
+
const st = lstatOrNull(fsImpl, dir);
|
|
74
|
+
if (!st || !st.isDirectory()) return [];
|
|
75
|
+
return namesIn(fsImpl, dir).filter((name: any) => {
|
|
76
|
+
if (!isEventName(name)) return false;
|
|
77
|
+
const entry = lstatOrNull(fsImpl, path.join(dir, name));
|
|
78
|
+
return Boolean(entry && entry.isFile());
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hasMarker = (fsImpl: any, root: any) => lstatOrNull(fsImpl, path.join(root, MARKER_REL)) !== null;
|
|
83
|
+
const storeHasEvents = (fsImpl: any, root: any) => namesIn(fsImpl, path.join(root, EVENTS_REL)).some(isEventName);
|
|
84
|
+
|
|
85
|
+
function removeQuietly(fsImpl: any, target: any) {
|
|
86
|
+
try { fsImpl.rmSync(target, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Flushes one copied file. The copy takes the source's mode, so it is made writable by its owner
|
|
91
|
+
* first, like any file the store writes: Windows refuses to flush a read-only handle.
|
|
92
|
+
*/
|
|
93
|
+
function fsyncCopy(fsImpl: any, file: any) {
|
|
94
|
+
fsImpl.chmodSync(file, 0o644);
|
|
95
|
+
const fd = fsImpl.openSync(file, 'r+');
|
|
96
|
+
try { fsImpl.fsyncSync(fd); } finally { fsImpl.closeSync(fd); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Flushes a folder's entries where the platform lets a folder be opened. */
|
|
100
|
+
function fsyncFolder(fsImpl: any, dir: any) {
|
|
101
|
+
let fd;
|
|
102
|
+
try { fd = fsImpl.openSync(dir, 'r'); } catch { return; }
|
|
103
|
+
try { fsImpl.fsyncSync(fd); } finally { fsImpl.closeSync(fd); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const migrationFailed = (code: any) => ({
|
|
107
|
+
ok: false,
|
|
108
|
+
finding: 'store-migration-failed',
|
|
109
|
+
message: `could not copy agent-docs/lifecycle/events to .doflow/state/lifecycle/events (${code}); the old folder is unchanged and the next lifecycle command retries. Nothing was written.`,
|
|
110
|
+
lines: [],
|
|
111
|
+
});
|
|
112
|
+
const legacyUnreadable = (code: any) => ({
|
|
113
|
+
ok: false,
|
|
114
|
+
finding: 'store-migration-failed',
|
|
115
|
+
message: `could not list agent-docs/lifecycle/events (${code}), so the old store cannot be copied to .doflow/state/lifecycle/events; make the folder readable, or delete it if its events are not needed, and the next lifecycle command retries. Nothing was written.`,
|
|
116
|
+
lines: [],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** Writes `text` to `file` by a flushed temp file in the same folder and a rename. */
|
|
120
|
+
function writeAtomic(fsImpl: any, file: any, text: any) {
|
|
121
|
+
const dir = path.dirname(file);
|
|
122
|
+
const tmp = path.join(dir, `.${path.basename(file, '.json')}-${process.pid}-${randomChars(6)}.tmp`);
|
|
123
|
+
try {
|
|
124
|
+
const fd = fsImpl.openSync(tmp, 'wx', 0o644);
|
|
125
|
+
try {
|
|
126
|
+
fsImpl.writeFileSync(fd, text, 'utf8');
|
|
127
|
+
fsImpl.fsyncSync(fd);
|
|
128
|
+
} finally {
|
|
129
|
+
fsImpl.closeSync(fd);
|
|
130
|
+
}
|
|
131
|
+
fsImpl.renameSync(tmp, file);
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
try { fsImpl.unlinkSync(tmp); } catch { /* never written */ }
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
fsyncFolder(fsImpl, dir);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Records that the store is in place; false when it cannot be written, so a later verb tries again. */
|
|
140
|
+
function writeMarker(fsImpl: any, root: any, copied: any, now: any) {
|
|
141
|
+
try {
|
|
142
|
+
writeAtomic(fsImpl, path.join(root, MARKER_REL), `${JSON.stringify({ v: 1, from: 'agent-docs/lifecycle/events', copied, at: now.toISOString() })}\n`);
|
|
143
|
+
return true;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The copy itself, under the store lock, of the event files the caller listed. Returns the number
|
|
151
|
+
* copied, or the `store-migration-failed` refusal.
|
|
152
|
+
*/
|
|
153
|
+
function copyLegacyStore(fsImpl: any, root: any, files: any) {
|
|
154
|
+
const lifecycleDir = path.join(root, LIFECYCLE_REL);
|
|
155
|
+
const eventsDir = path.join(root, EVENTS_REL);
|
|
156
|
+
const legacyDir = path.join(root, LEGACY_EVENTS_REL);
|
|
157
|
+
let tmp = null;
|
|
158
|
+
try {
|
|
159
|
+
// Under the lock no temp folder belongs to a live copier.
|
|
160
|
+
for (const name of namesIn(fsImpl, lifecycleDir)) {
|
|
161
|
+
if (name.startsWith(MIGRATING_PREFIX)) fsImpl.rmSync(path.join(lifecycleDir, name), { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
tmp = path.join(lifecycleDir, `${MIGRATING_PREFIX}${process.pid}-${randomChars(6)}`);
|
|
164
|
+
fsImpl.mkdirSync(tmp);
|
|
165
|
+
for (const name of files) {
|
|
166
|
+
const copy = path.join(tmp, name);
|
|
167
|
+
fsImpl.copyFileSync(path.join(legacyDir, name), copy, nodeFs.constants.COPYFILE_EXCL);
|
|
168
|
+
fsyncCopy(fsImpl, copy);
|
|
169
|
+
}
|
|
170
|
+
fsyncFolder(fsImpl, tmp);
|
|
171
|
+
if (lstatOrNull(fsImpl, eventsDir)) {
|
|
172
|
+
// An `events` here holds no event file; one that holds anything else is kept for its owner.
|
|
173
|
+
const replaced = path.join(lifecycleDir, `events.replaced-${randomChars(6)}`);
|
|
174
|
+
fsImpl.renameSync(eventsDir, replaced);
|
|
175
|
+
try { fsImpl.rmdirSync(replaced); } catch { /* not empty: left in place */ }
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
fsImpl.renameSync(tmp, eventsDir);
|
|
179
|
+
} catch (error: any) {
|
|
180
|
+
if (error.code !== 'EEXIST' && error.code !== 'ENOTEMPTY') throw error;
|
|
181
|
+
removeQuietly(fsImpl, tmp); // another copier committed first
|
|
182
|
+
}
|
|
183
|
+
fsyncFolder(fsImpl, lifecycleDir);
|
|
184
|
+
return files.length;
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
if (tmp) removeQuietly(fsImpl, tmp);
|
|
187
|
+
return migrationFailed(error.code || error.message);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The UTC time an event file name's id starts with. */
|
|
192
|
+
function idTime(name: any) {
|
|
193
|
+
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z-/.exec(name);
|
|
194
|
+
return m ? Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6], +m[7]) : NaN;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Writes the retention journal, flushed to disk, with a new generation. */
|
|
198
|
+
function writeJournal(fsImpl: any, root: any, pending: any) {
|
|
199
|
+
writeAtomic(fsImpl, path.join(root, JOURNAL_REL), `${JSON.stringify({ v: 1, generation: randomChars(16), pending })}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Keeps the held store lock from looking stale: another verb breaks a lock whose folder is older
|
|
204
|
+
* than the lock's stale timeout, so a long pass renews its folder's time as it goes.
|
|
205
|
+
*/
|
|
206
|
+
function refreshLock(fsImpl: any, root: any) {
|
|
207
|
+
const now = new Date();
|
|
208
|
+
try { fsImpl.utimesSync(`${lockTarget(root)}.lock`, now, now); } catch { /* the lock folder is ours; a failed touch only shortens its life */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Unlinks pending event files without clearing the journal until every hidden name is finished. */
|
|
212
|
+
function removePending(fsImpl: any, root: any, names: any, onRemoved = () => {}, { maxFiles = Infinity, shouldStop }: any = {}) {
|
|
213
|
+
const dir = path.join(root, EVENTS_REL);
|
|
214
|
+
let removed = 0;
|
|
215
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
216
|
+
if (shouldStop?.()) return { complete: false, removed };
|
|
217
|
+
const file = path.join(dir, names[index]);
|
|
218
|
+
if (removed >= maxFiles) {
|
|
219
|
+
try { fsImpl.lstatSync(file); return { complete: false, removed }; }
|
|
220
|
+
catch (error: any) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error; continue; }
|
|
221
|
+
}
|
|
222
|
+
if (index % REFRESH_EVERY === 0) refreshLock(fsImpl, root);
|
|
223
|
+
try {
|
|
224
|
+
fsImpl.unlinkSync(file);
|
|
225
|
+
removed += 1;
|
|
226
|
+
onRemoved();
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
if (error.code !== 'ENOENT') throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
writeJournal(fsImpl, root, []);
|
|
232
|
+
return { complete: true, removed };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Selects whole units only; a unit that does not fit waits for the next pass. */
|
|
236
|
+
function passFiles(units: any, { maxFiles = MAX_PASS_FILES, shouldStop }: any = {}) {
|
|
237
|
+
const files = [], limit = Math.max(0, Math.min(MAX_PASS_FILES, Number.isFinite(maxFiles) ? maxFiles : MAX_PASS_FILES));
|
|
238
|
+
let partial = false;
|
|
239
|
+
for (const unit of units) {
|
|
240
|
+
if (shouldStop?.()) { partial = true; break; }
|
|
241
|
+
if (files.length + unit.files.length > limit) { partial = true; break; }
|
|
242
|
+
files.push(...unit.files);
|
|
243
|
+
}
|
|
244
|
+
return { files: files.sort(), partial };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The retention pass, under the store lock: finish a pending journal within the caller's remaining
|
|
249
|
+
* limits, then remove whole eligible units. A stopped deletion keeps the complete name set hidden.
|
|
250
|
+
*/
|
|
251
|
+
function retain(fsImpl: any, root: any, window: any, now: any, { maxFiles = MAX_PASS_FILES, shouldStop }: any = {}) {
|
|
252
|
+
let removedCount = 0;
|
|
253
|
+
const selected: any[] = [];
|
|
254
|
+
try {
|
|
255
|
+
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
256
|
+
const pending = readJournal(root, fsImpl).pending;
|
|
257
|
+
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
258
|
+
if (pending.length) {
|
|
259
|
+
const result = removePending(fsImpl, root, pending, () => { removedCount += 1; }, { maxFiles, shouldStop });
|
|
260
|
+
if (!result.complete) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
261
|
+
}
|
|
262
|
+
const remaining = Math.max(0, maxFiles - removedCount);
|
|
263
|
+
if (window.state !== 'on') return { removed: [], removedCount, warning: [], partial: false, selected };
|
|
264
|
+
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
265
|
+
const snapshot = readEvents(root, { fsImpl, lockFallback: false, shouldStop });
|
|
266
|
+
if (snapshot.incomplete) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
267
|
+
if (snapshot.unreadable.length) throw Object.assign(new Error('unreadable lifecycle events'), { code: 'EIO' });
|
|
268
|
+
refreshLock(fsImpl, root);
|
|
269
|
+
const selection = selectExpired(snapshot.events, { now, windowMs: window.ms, shouldStop });
|
|
270
|
+
if (selection.incomplete) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
271
|
+
const bounded = passFiles(selection.units, { maxFiles: remaining, shouldStop });
|
|
272
|
+
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
273
|
+
for (const name of bounded.files) {
|
|
274
|
+
if (shouldStop?.()) return { removed: [], removedCount, warning: [], partial: true, selected };
|
|
275
|
+
const file = path.join(root, EVENTS_REL, name);
|
|
276
|
+
let info;
|
|
277
|
+
try { info = fsImpl.lstatSync(file); } catch (error: any) { if (error.code !== 'ENOENT') throw error; }
|
|
278
|
+
selected.push({ name, bytes: info?.isFile() ? info.size : 0 });
|
|
279
|
+
}
|
|
280
|
+
refreshLock(fsImpl, root);
|
|
281
|
+
if (bounded.files.length === 0) return { removed: [], removedCount, warning: [], partial: bounded.partial, selected };
|
|
282
|
+
writeJournal(fsImpl, root, bounded.files);
|
|
283
|
+
const result = removePending(fsImpl, root, bounded.files, () => { removedCount += 1; }, { maxFiles: remaining, shouldStop });
|
|
284
|
+
return {
|
|
285
|
+
removed: window.source === 'default' ? [] : [removedLine(bounded.files.length, window.hours)],
|
|
286
|
+
removedCount, warning: [], partial: bounded.partial || !result.complete, selected,
|
|
287
|
+
};
|
|
288
|
+
} catch (error: any) {
|
|
289
|
+
// A failure is not a stopped pass: `partial` means only a stop, a held lock or the removal cap,
|
|
290
|
+
// so throttled upkeep advances its due time instead of retrying a failing pass on every verb.
|
|
291
|
+
return { removed: [], removedCount, warning: [unlinkLine(error.code || error.message)], partial: false, selected, code: error.code || 'EIO' };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Runs a bounded lifecycle retention pass under a non-waiting store lock for the storage engine. */
|
|
296
|
+
function retainLocked(root: any, options: any = {}) {
|
|
297
|
+
const { window, now = new Date(), fsImpl = nodeFs, maxFiles = MAX_PASS_FILES, shouldStop } = options;
|
|
298
|
+
const bounded = Object.hasOwn(options, 'maxFiles') || Object.hasOwn(options, 'shouldStop');
|
|
299
|
+
const outcome = (removed: any, warning: any, partial = false, selected: any = [], code?: any) => bounded
|
|
300
|
+
? { removed, warning, partial, selected, ...(code ? { code } : {}) }
|
|
301
|
+
: { removed, warning };
|
|
302
|
+
const eventsDir = path.join(root, EVENTS_REL);
|
|
303
|
+
const st = lstatOrNull(fsImpl, eventsDir);
|
|
304
|
+
if (!st || !st.isDirectory()) return outcome(0, []);
|
|
305
|
+
if (shouldStop?.()) return outcome(0, [], true);
|
|
306
|
+
const release = tryAcquireLock(fsImpl, lockTarget(root));
|
|
307
|
+
if (!release) return outcome(0, [], true);
|
|
308
|
+
try {
|
|
309
|
+
const result = retain(fsImpl, root, window, now, { maxFiles, shouldStop });
|
|
310
|
+
return outcome(result.removedCount, result.warning, result.partial, result.selected, result.code);
|
|
311
|
+
} finally {
|
|
312
|
+
release();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Whether a lock-free look finds anything for the retention pass to remove. */
|
|
317
|
+
function needsPrune(fsImpl: any, root: any, window: any, now: any) {
|
|
318
|
+
if (window.state !== 'on') return false;
|
|
319
|
+
const cutoff = now.getTime() - window.ms;
|
|
320
|
+
if (!namesIn(fsImpl, path.join(root, EVENTS_REL)).some((name: any) => isEventName(name) && idTime(name) < cutoff)) return false;
|
|
321
|
+
return selectExpired(readEvents(root, { fsImpl }).events, { now, windowMs: window.ms }).files.length > 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Prepares the project store for one verb invocation.
|
|
326
|
+
* @param {string} root projectRoot() result
|
|
327
|
+
* @param {{env?: Object, now?: Date, fsImpl?: Object}} [options] defaults process.env, new Date(), node:fs
|
|
328
|
+
* @returns {{ok: true, lines: string[]}
|
|
329
|
+
* | {ok: false, finding: 'store-migration-failed'|'store-locked', message: string, lines: string[]}}
|
|
330
|
+
* `lines` are for stderr, without the verb prefix.
|
|
331
|
+
* @throws {StoreUnsafeError} a symlinked new store folder, or a symlinked old folder when a copy must read it
|
|
332
|
+
*/
|
|
333
|
+
function prepareStore(root: string, { env = process.env, now = new Date(), fsImpl = nodeFs, registry }: any = {}) {
|
|
334
|
+
refuseLinks(fsImpl, root, [LIFECYCLE_REL, EVENTS_REL]);
|
|
335
|
+
const legacyDir = lstatOrNull(fsImpl, path.join(root, LEGACY_EVENTS_REL));
|
|
336
|
+
const notice = legacyDir && legacyDir.isDirectory() ? [NOTICE] : [];
|
|
337
|
+
const window = retentionWindow({ env, projectRoot: root, registry, fsImpl });
|
|
338
|
+
const invalid = window.state === 'invalid' ? [invalidLine(window.raw)] : [];
|
|
339
|
+
let migrate = false;
|
|
340
|
+
let markerDue = false;
|
|
341
|
+
if (!hasMarker(fsImpl, root)) {
|
|
342
|
+
if (storeHasEvents(fsImpl, root)) {
|
|
343
|
+
markerDue = true;
|
|
344
|
+
} else {
|
|
345
|
+
try { migrate = legacyEventFiles(fsImpl, root).length > 0; } catch (error: any) { return legacyUnreadable(error.code || error.message); }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const retentionDue = readJournal(root, fsImpl).pending.length > 0 || needsPrune(fsImpl, root, window, now);
|
|
349
|
+
if (!migrate && !markerDue && !retentionDue) return { ok: true, lines: [...notice, ...invalid] };
|
|
350
|
+
if (migrate) refuseLinks(fsImpl, root, [LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL]);
|
|
351
|
+
|
|
352
|
+
const lifecycleDir = path.join(root, LIFECYCLE_REL);
|
|
353
|
+
try {
|
|
354
|
+
fsImpl.mkdirSync(lifecycleDir, { recursive: true });
|
|
355
|
+
// An unwritable folder would only show as a lock that never comes; name it for what it is.
|
|
356
|
+
fsImpl.accessSync(lifecycleDir, nodeFs.constants.W_OK);
|
|
357
|
+
} catch (error: any) {
|
|
358
|
+
// Without a copy to make, an unwritable store only stops the retention pass, not the verb.
|
|
359
|
+
if (!migrate) return { ok: true, lines: [...notice, ...invalid, ...(retentionDue ? [unwritableLine(error.code || error.message)] : [])] };
|
|
360
|
+
return migrationFailed(error.code || error.message);
|
|
361
|
+
}
|
|
362
|
+
let release;
|
|
363
|
+
try {
|
|
364
|
+
release = acquireLock(fsImpl, lockTarget(root));
|
|
365
|
+
} catch (error: any) {
|
|
366
|
+
if (/^Could not lock/.test(error.message || '')) return { ok: false, finding: 'store-locked', message: `${error.message} Nothing was written.`, lines: [] };
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
// Decided again under the lock: another verb may have finished the copy while this one waited.
|
|
371
|
+
let marked = hasMarker(fsImpl, root);
|
|
372
|
+
if (!marked && storeHasEvents(fsImpl, root)) {
|
|
373
|
+
marked = writeMarker(fsImpl, root, 0, now);
|
|
374
|
+
} else if (!marked) {
|
|
375
|
+
let files;
|
|
376
|
+
try { files = legacyEventFiles(fsImpl, root); } catch (error: any) { return legacyUnreadable(error.code || error.message); }
|
|
377
|
+
if (files.length) {
|
|
378
|
+
refuseLinks(fsImpl, root, [LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL]);
|
|
379
|
+
const copied = copyLegacyStore(fsImpl, root, files);
|
|
380
|
+
if (typeof copied !== 'number') return copied;
|
|
381
|
+
marked = writeMarker(fsImpl, root, copied, now);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Retention runs only on a marked store, so emptying it can never bring the old folder back.
|
|
385
|
+
if (!marked) return { ok: true, lines: [...notice, ...invalid] };
|
|
386
|
+
const { removed, warning } = retain(fsImpl, root, window, now);
|
|
387
|
+
return { ok: true, lines: [...notice, ...removed, ...invalid, ...warning] };
|
|
388
|
+
} finally {
|
|
389
|
+
release();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = { prepareStore, retainLocked, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Does an evidence locator still point at something?
|
|
10
|
+
*
|
|
11
|
+
* `parseLocator` in cli.js validates a locator's *shape* — that `line` is a positive integer and
|
|
12
|
+
* `file` a non-empty string — and never opens the file. A locator naming line 799 of a 28-line file
|
|
13
|
+
* therefore passed validation, was recorded as support, and the readiness gate counted it. This
|
|
14
|
+
* module answers the question that check never asked (FR-004, FR-005).
|
|
15
|
+
*
|
|
16
|
+
* It reads files and decides nothing else. Whether an unresolvable locator should refuse a write
|
|
17
|
+
* (FR-004) or fail a gate (FR-005) is the caller's policy, which is why both callers can share one
|
|
18
|
+
* implementation — see design C2. This module requires nothing from src/runtime/; that is its
|
|
19
|
+
* defining constraint, same as cli-result.js.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Why a locator did not resolve. `not-checkable` is not a failure — see `resolveLocator`. */
|
|
23
|
+
const RESOLUTION_REASONS = new Set([
|
|
24
|
+
'file-missing',
|
|
25
|
+
'line-beyond-eof',
|
|
26
|
+
'symbol-absent',
|
|
27
|
+
'not-checkable',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Word-boundary search for an identifier, so `parseLocator` does not match inside
|
|
32
|
+
* `notParseLocatorReally`. Deliberately textual rather than syntactic: this module must answer for
|
|
33
|
+
* every language in the repository, and a parser per language is a far larger thing than the
|
|
34
|
+
* question being asked.
|
|
35
|
+
*/
|
|
36
|
+
function findSymbolLines(lines: string[], symbol: string): number[] {
|
|
37
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
|
+
const pattern = new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?:[^A-Za-z0-9_$]|$)`);
|
|
39
|
+
const hits: number[] = [];
|
|
40
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
41
|
+
if (pattern.test(lines[i])) hits.push(i + 1);
|
|
42
|
+
}
|
|
43
|
+
return hits;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads the target file's lines, or reports that it could not be read.
|
|
48
|
+
*
|
|
49
|
+
* Split out of `resolveLocator` so that function is a flat sequence of verdicts rather than a
|
|
50
|
+
* verdict sequence with file I/O threaded through it. Everything about *getting at* the content
|
|
51
|
+
* lives here; everything about *judging* it lives in the caller.
|
|
52
|
+
*
|
|
53
|
+
* @returns {{lines: string[]}|{missing: true}}
|
|
54
|
+
*/
|
|
55
|
+
function readTargetLines(locator: any, root: string, impl: any): { lines: string[] } | { missing: true } {
|
|
56
|
+
const target = path.isAbsolute(locator.file) ? locator.file : path.join(root, locator.file);
|
|
57
|
+
|
|
58
|
+
let content: string;
|
|
59
|
+
try {
|
|
60
|
+
if (!impl.existsSync(target)) return { missing: true };
|
|
61
|
+
content = impl.readFileSync(target, 'utf8');
|
|
62
|
+
} catch {
|
|
63
|
+
// Unreadable for any other reason — a directory, a permission error, a binary that will not
|
|
64
|
+
// decode. Report it as missing rather than inventing a third verdict the callers must handle.
|
|
65
|
+
return { missing: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A trailing newline ends the last line; it does not begin another one.
|
|
69
|
+
const lines = content.split('\n');
|
|
70
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
71
|
+
return { lines };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The symbol arm of `resolveLocator`, returning that function's own result shape.
|
|
76
|
+
*
|
|
77
|
+
* A symbol present but not on the named line still resolves: the code moved, which freshness
|
|
78
|
+
* already reports. Only absence is unresolvable — that distinction is the whole of this function
|
|
79
|
+
* and is easier to see with nothing else around it.
|
|
80
|
+
*/
|
|
81
|
+
function checkSymbol(lines: string[], symbol: string): any {
|
|
82
|
+
const symbolLines = findSymbolLines(lines, symbol);
|
|
83
|
+
if (symbolLines.length === 0) {
|
|
84
|
+
return { resolved: false, reason: 'symbol-absent', actual: { lines: lines.length, symbolLines: [] } };
|
|
85
|
+
}
|
|
86
|
+
return { resolved: true, reason: null, actual: { lines: lines.length, symbolLines } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Nothing here is readable, so no verdict about the repository can honestly be returned. */
|
|
90
|
+
const NOT_CHECKABLE = { resolved: true, reason: 'not-checkable', actual: null };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves one locator against the repository as it stands right now.
|
|
94
|
+
*
|
|
95
|
+
* A `uri` locator with no `file` resolves as `not-checkable`: this module cannot read a URI, and
|
|
96
|
+
* returning a verdict about something it did not read is the invented-verdict failure the whole
|
|
97
|
+
* feature exists to remove (design D4). `resolved` is therefore `true` with a `reason` — the only
|
|
98
|
+
* combination where the two disagree, and it is deliberate.
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} options
|
|
101
|
+
* @param {Object} options.locator a parsed locator: { file?, line?, symbol?, uri? }
|
|
102
|
+
* @param {string} [options.repoRoot] root a relative `file` is resolved against; defaults to cwd
|
|
103
|
+
* @param {Object} [options.fsImpl] injectable fs, for tests
|
|
104
|
+
* @returns {{resolved: boolean, reason: (string|null), actual: (Object|null)}}
|
|
105
|
+
* `actual` names what the file does offer, so a caller can build a message the writer can act on:
|
|
106
|
+
* `{ lines }` for a line miss, `{ symbolLines }` for a symbol miss.
|
|
107
|
+
*/
|
|
108
|
+
function resolveLocator({ locator, repoRoot, fsImpl }: any = {}): { resolved: boolean; reason: string | null; actual: any } {
|
|
109
|
+
if (!locator || typeof locator !== 'object' || Array.isArray(locator)) return { ...NOT_CHECKABLE };
|
|
110
|
+
// A uri-only locator, or an empty locator on a non-extracted item. Nothing readable here.
|
|
111
|
+
if (!locator.file) return { ...NOT_CHECKABLE };
|
|
112
|
+
|
|
113
|
+
const read = readTargetLines(locator, repoRoot || process.cwd(), fsImpl || fs);
|
|
114
|
+
if ('missing' in read) return { resolved: false, reason: 'file-missing', actual: null };
|
|
115
|
+
|
|
116
|
+
const { lines } = read;
|
|
117
|
+
if (locator.line !== undefined && locator.line > lines.length) {
|
|
118
|
+
return { resolved: false, reason: 'line-beyond-eof', actual: { lines: lines.length } };
|
|
119
|
+
}
|
|
120
|
+
if (locator.symbol) return checkSymbol(lines, locator.symbol);
|
|
121
|
+
|
|
122
|
+
return { resolved: true, reason: null, actual: { lines: lines.length } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* One-line explanation of an unresolved verdict, for a caller's error or report text. Returns null
|
|
127
|
+
* for a resolved locator so a caller can use it as the whole message or not at all.
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} locator
|
|
130
|
+
* @param {{resolved: boolean, reason: (string|null), actual: (Object|null)}} result
|
|
131
|
+
* @returns {string|null}
|
|
132
|
+
*/
|
|
133
|
+
function describeResolution(locator: any, result: any): string | null {
|
|
134
|
+
if (!result || result.resolved) return null;
|
|
135
|
+
const file = (locator && locator.file) || '(no file)';
|
|
136
|
+
switch (result.reason) {
|
|
137
|
+
case 'file-missing':
|
|
138
|
+
return `'${file}' cannot be read from this repository`;
|
|
139
|
+
case 'line-beyond-eof':
|
|
140
|
+
return `'${file}' has ${result.actual.lines} line(s); the locator names line ${locator.line}`;
|
|
141
|
+
case 'symbol-absent':
|
|
142
|
+
return `'${file}' contains no symbol '${locator.symbol}'`;
|
|
143
|
+
default:
|
|
144
|
+
// RESOLUTION_REASONS was declared and exported while nothing read it, so this branch could not
|
|
145
|
+
// tell two very different situations apart: a reason this module legitimately produces but has
|
|
146
|
+
// no tailored sentence for, and a reason that exists only because something upstream wrote a
|
|
147
|
+
// string that is not a reason at all. Both rendered identically, and the second is a defect in
|
|
148
|
+
// `resolveLocator` rather than a fact about the locator being described. Consulting the set is
|
|
149
|
+
// what separates them. This function reports and must not throw — it runs while explaining why
|
|
150
|
+
// something else already failed — so an unknown reason is named as suspect, not raised.
|
|
151
|
+
return RESOLUTION_REASONS.has(result.reason)
|
|
152
|
+
? `'${file}' did not resolve (${result.reason})`
|
|
153
|
+
: `'${file}' did not resolve, and the reason given ('${result.reason}') is not one this `
|
|
154
|
+
+ 'resolver defines — treat it as a defect in resolveLocator rather than a fact about the file';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { resolveLocator, describeResolution, RESOLUTION_REASONS };
|