@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,130 @@
|
|
|
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
|
+
const ROOT_BASES = new Set(['project', 'home', 'xdg-config']);
|
|
9
|
+
const ENTRIES = new Set(['file', 'dir', 'record']);
|
|
10
|
+
const RULES = new Map([
|
|
11
|
+
['count-and-age', ['keepNewest', 'maxAgeDays']],
|
|
12
|
+
['work', ['finishedDays']],
|
|
13
|
+
['date-partition', ['maxAgeDays']],
|
|
14
|
+
['recovery', ['keepNewest']],
|
|
15
|
+
['lifecycle-settled', ['defaultHours']],
|
|
16
|
+
['report-orphan', ['orphanGraceHours', 'foreignMaxAgeDays']],
|
|
17
|
+
['age', ['maxAgeDays']],
|
|
18
|
+
['existing', []],
|
|
19
|
+
['control', []],
|
|
20
|
+
]);
|
|
21
|
+
const MOMENTS = new Set(['verb', 'session', 'verify', 'install', 'lifecycle-verb', 'uninstall']);
|
|
22
|
+
const STORE_ID = /^[a-z][a-z0-9-]*$/;
|
|
23
|
+
|
|
24
|
+
function isObject(value ) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
|
25
|
+
function positiveInteger(value ) { return Number.isInteger(value) && value > 0; }
|
|
26
|
+
function error(errors , where , message ) { errors.push(`${where}: ${message}`); }
|
|
27
|
+
|
|
28
|
+
/** Validates the closed IC-001 storage declaration document without touching the filesystem. */
|
|
29
|
+
function validateStorageRegistry(registry ) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
if (!isObject(registry)) return { ok: false, errors: ['registry: must be an object'] };
|
|
32
|
+
if (registry.version !== 1) error(errors, 'version', 'must be 1');
|
|
33
|
+
if (typeof registry.description !== 'string' || !registry.description.trim()) error(errors, 'description', 'must be a non-empty string');
|
|
34
|
+
const settings = registry.settings;
|
|
35
|
+
if (!isObject(settings)) error(errors, 'settings', 'must be an object');
|
|
36
|
+
else {
|
|
37
|
+
if (settings.optOutFile !== 'prune') error(errors, 'settings.optOutFile', 'must be prune');
|
|
38
|
+
for (const field of ['upkeepIntervalHours', 'passBudgetMs', 'maxRemovalsPerPass', 'passLockStaleSeconds', 'orphanGraceHours', 'staleTmpHours']) {
|
|
39
|
+
if (!positiveInteger(settings[field])) error(errors, `settings.${field}`, 'must be a positive integer');
|
|
40
|
+
}
|
|
41
|
+
if (!isObject(settings.ignoreFile) || settings.ignoreFile.name !== '.gitignore'
|
|
42
|
+
|| !Array.isArray(settings.ignoreFile.lines) || settings.ignoreFile.lines.some((line ) => typeof line !== 'string')) {
|
|
43
|
+
error(errors, 'settings.ignoreFile', 'must name .gitignore and contain string lines');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const roots = registry.roots;
|
|
47
|
+
if (!isObject(roots) || Object.keys(roots).length === 0) error(errors, 'roots', 'must be a non-empty object');
|
|
48
|
+
else for (const [id, root] of Object.entries(roots) ) {
|
|
49
|
+
if (!isObject(root) || !ROOT_BASES.has(root.base) || !Array.isArray(root.segments)
|
|
50
|
+
|| root.segments.some((segment ) => typeof segment !== 'string' || !segment || segment === '.' || segment === '..' || segment.includes('/') || segment.includes('\\'))) {
|
|
51
|
+
error(errors, `roots.${id}`, 'must have a known base and safe path segments');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(registry.stores)) error(errors, 'stores', 'must be an array');
|
|
55
|
+
else {
|
|
56
|
+
const ids = new Set();
|
|
57
|
+
registry.stores.forEach((store , index ) => {
|
|
58
|
+
const where = `stores[${index}]`;
|
|
59
|
+
if (!isObject(store)) { error(errors, where, 'must be an object'); return; }
|
|
60
|
+
if (typeof store.id !== 'string' || !STORE_ID.test(store.id)) error(errors, `${where}.id`, 'must match ^[a-z][a-z0-9-]*$');
|
|
61
|
+
else if (ids.has(store.id)) error(errors, `${where}.id`, `duplicate id '${store.id}'`); else ids.add(store.id);
|
|
62
|
+
if (typeof store.root !== 'string' || !roots?.[store.root]) error(errors, `${where}.root`, 'must name a declared root');
|
|
63
|
+
if (typeof store.path !== 'string' || store.path.startsWith('/') || store.path.split('/').some((part ) => part === '..' || !part && store.path !== '')) error(errors, `${where}.path`, 'must be relative, slash-separated, and contain no ..');
|
|
64
|
+
if (!ENTRIES.has(store.entry)) error(errors, `${where}.entry`, 'must be file, dir, or record');
|
|
65
|
+
if (typeof store.match !== 'string' || !store.match.startsWith('^') || !store.match.endsWith('$')) error(errors, `${where}.match`, 'must be an anchored regular expression');
|
|
66
|
+
else { try { new RegExp(store.match); } catch { error(errors, `${where}.match`, 'must compile'); } }
|
|
67
|
+
if (!RULES.has(store.rule)) error(errors, `${where}.rule`, 'must name a known rule kind');
|
|
68
|
+
if (!isObject(store.bound)) error(errors, `${where}.bound`, 'must be an object');
|
|
69
|
+
else {
|
|
70
|
+
for (const field of RULES.get(store.rule) || []) if (!positiveInteger(store.bound[field])) error(errors, `${where}.bound.${field}`, 'must be a positive integer');
|
|
71
|
+
if (['existing', 'control'].includes(store.rule) && (typeof store.bound.text !== 'string' || !store.bound.text)) error(errors, `${where}.bound.text`, 'must be a non-empty string');
|
|
72
|
+
if (store.rule === 'existing' && store.bound.value !== undefined && !positiveInteger(store.bound.value)) error(errors, `${where}.bound.value`, 'must be a positive integer when present');
|
|
73
|
+
}
|
|
74
|
+
if (store.protect !== null && store.protect !== 'open-work') error(errors, `${where}.protect`, 'must be open-work or null');
|
|
75
|
+
if (!Array.isArray(store.moments) || store.moments.some((moment ) => !MOMENTS.has(moment)) || new Set(store.moments).size !== store.moments.length) error(errors, `${where}.moments`, 'must be a unique subset of known moments');
|
|
76
|
+
if (store.rule === 'existing' && store.moments?.length) error(errors, `${where}.moments`, 'must be empty for existing stores');
|
|
77
|
+
if (!Array.isArray(store.writers) || store.writers.some((writer ) => typeof writer !== 'string' || !writer)) error(errors, `${where}.writers`, 'must be an array of repository-relative paths');
|
|
78
|
+
else if (!store.writers.length && store.rule !== 'control' && !store.id.startsWith('user-settings-')) error(errors, `${where}.writers`, 'may be empty only for control or user-settings stores');
|
|
79
|
+
if (store.rule === 'existing') {
|
|
80
|
+
if (!isObject(store.source) || typeof store.source.file !== 'string' || !store.source.file
|
|
81
|
+
|| typeof store.source.pattern !== 'string' || !store.source.pattern) {
|
|
82
|
+
error(errors, `${where}.source`, 'requires a non-empty file and literal or template pattern');
|
|
83
|
+
} else if (store.source.pattern.includes('{value}') && !positiveInteger(store.bound?.value)) {
|
|
84
|
+
error(errors, `${where}.bound.value`, 'must be a positive integer for a {value} source pattern');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(registry.externalWriters)) error(errors, 'externalWriters', 'must be an array');
|
|
90
|
+
else registry.externalWriters.forEach((writer , index ) => {
|
|
91
|
+
if (!isObject(writer) || typeof writer.file !== 'string' || !writer.file || typeof writer.reason !== 'string' || !writer.reason.trim()) error(errors, `externalWriters[${index}]`, 'requires a file and non-empty reason');
|
|
92
|
+
});
|
|
93
|
+
return { ok: errors.length === 0, errors };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Loads and fails closed on an IC-001 storage registry. */
|
|
97
|
+
function loadStorageRegistry({ repoRoot, file, fsImpl = fs } = {}) {
|
|
98
|
+
const target = file || path.join(path.resolve(repoRoot || process.cwd()), 'core', 'registry', 'storage.json');
|
|
99
|
+
let registry ;
|
|
100
|
+
try { registry = JSON.parse(fsImpl.readFileSync(target, 'utf8')); } catch (cause ) { throw new Error(`Could not load storage registry '${target}': ${cause.message}`); }
|
|
101
|
+
const validation = validateStorageRegistry(registry);
|
|
102
|
+
if (!validation.ok) throw new Error(`Invalid storage registry:\n- ${validation.errors.join('\n- ')}`);
|
|
103
|
+
return registry;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Renders IC-001's canonical human-readable bound for a store. */
|
|
107
|
+
/** Renders an existing-store source pattern for G26's literal source check. */
|
|
108
|
+
function renderSourcePattern(store ) {
|
|
109
|
+
const pattern = store?.source?.pattern;
|
|
110
|
+
if (typeof pattern !== 'string') throw new Error('Existing store has no source pattern');
|
|
111
|
+
return pattern.includes('{value}') ? pattern.replaceAll('{value}', String(store.bound.value)) : pattern;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatBound(store ) {
|
|
115
|
+
const bound = store.bound;
|
|
116
|
+
switch (store.rule) {
|
|
117
|
+
case 'count-and-age': return `keep newest ${bound.keepNewest}; prune entries older than ${bound.maxAgeDays} days`;
|
|
118
|
+
case 'work': return `prune finished work after ${bound.finishedDays} days`;
|
|
119
|
+
case 'date-partition': return `prune partitions older than ${bound.maxAgeDays} days`;
|
|
120
|
+
case 'recovery': return `keep newest ${bound.keepNewest}, plus the snapshot named by the ledger`;
|
|
121
|
+
case 'lifecycle-settled': return `retain settled events for ${bound.defaultHours} hours`;
|
|
122
|
+
case 'report-orphan': return `prune orphan bodies after ${bound.orphanGraceHours} hours; foreign folders after ${bound.foreignMaxAgeDays} days`;
|
|
123
|
+
case 'age': return `prune inactive directories after ${bound.maxAgeDays} days`;
|
|
124
|
+
case 'existing':
|
|
125
|
+
case 'control': return bound.text;
|
|
126
|
+
default: throw new Error(`Unknown storage rule '${store.rule}'`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { loadStorageRegistry, validateStorageRegistry, renderSourcePattern, formatBound, RULES };
|
|
@@ -0,0 +1,63 @@
|
|
|
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 os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
function absolute(value ) { return typeof value === 'string' && path.isAbsolute(value) ? value : null; }
|
|
10
|
+
|
|
11
|
+
/** Resolves IC-001's five physical roots; -g/global deliberately omits project roots. */
|
|
12
|
+
function resolveRoots({ registry, projectRoot, global = false, env = process.env } = {}) {
|
|
13
|
+
const home = absolute(env.HOME) || os.homedir();
|
|
14
|
+
const xdgBase = env.XDG_CONFIG_HOME === undefined || env.XDG_CONFIG_HOME === ''
|
|
15
|
+
? path.join(home, '.config') : absolute(env.XDG_CONFIG_HOME);
|
|
16
|
+
const project = global ? null : absolute(projectRoot);
|
|
17
|
+
const bases = { project, home, 'xdg-config': xdgBase };
|
|
18
|
+
return Object.fromEntries(Object.entries(registry.roots).map(([id, root] ) => {
|
|
19
|
+
const base = bases[root.base];
|
|
20
|
+
return [id, base ? path.join(base, ...root.segments) : null];
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readSetting(file , fsImpl ) {
|
|
25
|
+
try {
|
|
26
|
+
const first = fsImpl.readFileSync(file, 'utf8').split(/\r?\n/, 1)[0].trim().toLowerCase();
|
|
27
|
+
return first === 'on' || first === 'off' ? first : 'off';
|
|
28
|
+
} catch { return 'off'; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Reads IC-014 and reports exactly which present file made the decision, if any. */
|
|
32
|
+
function readPruneSetting({ registry, roots, rootId, fsImpl = fs } = {}) {
|
|
33
|
+
const projectFile = roots['project-doflow'] ? path.join(roots['project-doflow'], registry.settings.optOutFile) : null;
|
|
34
|
+
const userFile = roots['config-home'] ? path.join(roots['config-home'], registry.settings.optOutFile) : null;
|
|
35
|
+
const usesProject = rootId === 'project-doflow' || rootId === 'project-state';
|
|
36
|
+
if (usesProject && projectFile && fsImpl.existsSync(projectFile)) return { setting: readSetting(projectFile, fsImpl), decidedBy: projectFile };
|
|
37
|
+
if (userFile && fsImpl.existsSync(userFile)) return { setting: readSetting(userFile, fsImpl), decidedBy: userFile };
|
|
38
|
+
return { setting: 'on', decidedBy: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function linked(file , fsImpl ) {
|
|
42
|
+
try { return fsImpl.lstatSync(file).isSymbolicLink(); } catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the physical directory for a declared store, or null when a DoFlow-owned component is a
|
|
47
|
+
* link. The engine treats null as a refuse-to-touch result, never as a path to repair.
|
|
48
|
+
*/
|
|
49
|
+
function resolveStorePath({ store, roots, fsImpl = fs } = {}) {
|
|
50
|
+
const root = roots[store.root];
|
|
51
|
+
if (!root) return null;
|
|
52
|
+
if (linked(root, fsImpl)) return null;
|
|
53
|
+
const rootParent = path.dirname(root);
|
|
54
|
+
if (path.basename(rootParent) === '.doflow' && linked(rootParent, fsImpl)) return null;
|
|
55
|
+
let current = root;
|
|
56
|
+
for (const segment of store.path ? store.path.split('/') : []) {
|
|
57
|
+
current = path.join(current, segment);
|
|
58
|
+
if (linked(current, fsImpl)) return null;
|
|
59
|
+
}
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { resolveRoots, readPruneSetting, resolveStorePath };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { workStatus, createWorkCache, refreshedWorkCache } = require('./work-status.js');
|
|
8
|
+
const { projectKey } = require('../lifecycle/report-store.js');
|
|
9
|
+
const { readEvents } = require('../lifecycle/event-store.js');
|
|
10
|
+
|
|
11
|
+
const DAY = 86400000;
|
|
12
|
+
const HOUR = 3600000;
|
|
13
|
+
const BODY = /^FU-[0-9a-hjkmnp-tv-z]{6}\.txt$/;
|
|
14
|
+
const LOG = /[0-9]{13}-[A-Za-z0-9._-]+\.log/g;
|
|
15
|
+
const TMP = /^[A-Za-z0-9][A-Za-z0-9._-]*\.json\.[0-9]+\.tmp$/;
|
|
16
|
+
const ruleOrder = ['verification-logs', 'task-evidence', 'task-readiness', 'task-research', 'task-outcome', 'task-retrieval', 'task-attempts', 'task-orchestration', 'runs-project', 'runs-user', 'advice-picks-project', 'advice-picks-user', 'lifecycle-events', 'report-bodies', 'session-dirs', 'project-dirs', 'recovery-user', 'recovery-project'];
|
|
17
|
+
const removableRules = new Set(['count-and-age','work','date-partition','recovery','lifecycle-settled','report-orphan','age']);
|
|
18
|
+
const SAFE_NAMESPACE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
19
|
+
// Open-log protection cannot vouch for a record store that holds a link, so the pass fails closed.
|
|
20
|
+
const LINKED = 'link-in-record-store';
|
|
21
|
+
function stopped(context ) { return Boolean(context.shouldStop?.()); }
|
|
22
|
+
function budgetError() { return Object.assign(new Error('storage pass budget exhausted'), { code: 'EBUDGET', partial: true }); }
|
|
23
|
+
function projectRoot(roots ) { return roots['project-state'] ? path.dirname(path.dirname(roots['project-state'])) : null; }
|
|
24
|
+
function safeStat(fsImpl , file ) { try { return fsImpl.lstatSync(file); } catch { return null; } }
|
|
25
|
+
function names(fsImpl , dir ) { return fsImpl.readdirSync(dir); }
|
|
26
|
+
function ageReason(store , entry , now , fsImpl , storeDir , shouldStop ) {
|
|
27
|
+
const limit = store.bound.maxAgeDays * DAY;
|
|
28
|
+
if (store.rule === 'date-partition') {
|
|
29
|
+
const date = entry.name.slice(0, 10);
|
|
30
|
+
const time = Date.parse(`${date}T00:00:00Z`);
|
|
31
|
+
if (!Number.isFinite(time) || new Date(time).toISOString().slice(0,10) !== date) return null;
|
|
32
|
+
const cutoff = new Date(now - limit).toISOString().slice(0,10);
|
|
33
|
+
return date < cutoff ? 'age' : null;
|
|
34
|
+
}
|
|
35
|
+
let newest = entry.stat.mtimeMs;
|
|
36
|
+
if (store.rule === 'age') {
|
|
37
|
+
try {
|
|
38
|
+
for (const child of names(fsImpl, path.join(storeDir, entry.name))) {
|
|
39
|
+
if (shouldStop?.()) return null;
|
|
40
|
+
const st = safeStat(fsImpl, path.join(storeDir, entry.name, child));
|
|
41
|
+
if (!st || st.isSymbolicLink()) return null;
|
|
42
|
+
newest = Math.max(newest, st.mtimeMs);
|
|
43
|
+
}
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
}
|
|
46
|
+
return now - newest > limit ? 'age' : null;
|
|
47
|
+
}
|
|
48
|
+
function workCandidates(store , entries , context ) {
|
|
49
|
+
const { fsImpl, storeDir, now, roots, settings, shouldStop } = context;
|
|
50
|
+
const project = projectRoot(roots);
|
|
51
|
+
if (!project) return [];
|
|
52
|
+
const cache = context.cache || createWorkCache({ projectRoot: project, fsImpl });
|
|
53
|
+
const recordMatch = new RegExp(store.match), selected = [];
|
|
54
|
+
const visit = (dir , namespace = null) => {
|
|
55
|
+
let children;
|
|
56
|
+
try { children = names(fsImpl, dir); } catch { return true; }
|
|
57
|
+
for (const name of children) {
|
|
58
|
+
if (shouldStop?.()) return false;
|
|
59
|
+
const file = path.join(dir, name), stat = safeStat(fsImpl, file);
|
|
60
|
+
if (!stat || !stat.isFile()) continue;
|
|
61
|
+
if (TMP.test(name) && now - stat.mtimeMs > settings.staleTmpHours * HOUR) {
|
|
62
|
+
selected.push({ name: namespace ? `${namespace}/${name}` : name, reason: 'stale-tmp', bytes: stat.size, mtimeMs: stat.mtimeMs, recordStat: stat });
|
|
63
|
+
} else if (recordMatch.test(name)) {
|
|
64
|
+
const result = workStatus({ projectRoot: project, store: store.id, taskId: name.slice(0, -5), namespace, mtimeMs: stat.mtimeMs, cache, fsImpl, shouldStop });
|
|
65
|
+
if (result.incomplete || shouldStop?.()) return false;
|
|
66
|
+
if (result.state === 'finished' && now - result.since > store.bound.finishedDays * DAY)
|
|
67
|
+
selected.push({ name: namespace ? `${namespace}/${name}` : name, reason: 'finished-work', bytes: stat.size, mtimeMs: result.since, recordStat: stat });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
if (!visit(storeDir)) return null;
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (shouldStop?.()) return null;
|
|
75
|
+
if (entry.stat.isDirectory() && !visit(path.join(storeDir, entry.name), entry.name)) return null;
|
|
76
|
+
}
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
79
|
+
/** Revalidate the subject under its try-lock against its current run and plan, not the selection snapshot. */
|
|
80
|
+
function recordStillCandidate(store , candidate , info , context ) {
|
|
81
|
+
const { fsImpl, roots, now, settings, cache, shouldStop, storeDir } = context;
|
|
82
|
+
const before = candidate.recordStat;
|
|
83
|
+
if (!info.isFile() || !before || ['mtimeMs', 'ctimeMs', 'size', 'ino', 'dev'].some(key => info[key] !== before[key])) return false;
|
|
84
|
+
if (candidate.reason === 'stale-tmp') return now - info.mtimeMs > settings.staleTmpHours * HOUR;
|
|
85
|
+
const parts = candidate.name.split('/'), name = parts.at(-1), namespace = parts.length === 2 ? parts[0] : null;
|
|
86
|
+
// workStatus reuses an orchestration record's parsed run. Re-read that subject here and reject
|
|
87
|
+
// changes rather than mutating the snapshot used by the run's other records.
|
|
88
|
+
if (store.id === 'task-orchestration' && !namespace && !name.endsWith('_claims.json')) {
|
|
89
|
+
let current;
|
|
90
|
+
try { current = JSON.parse(fsImpl.readFileSync(path.join(storeDir, name), 'utf8')); } catch { return false; }
|
|
91
|
+
const snapshot = cache?.runs.get(name.slice(0, -5));
|
|
92
|
+
if (!snapshot?.ok || JSON.stringify(current) !== JSON.stringify(snapshot.value)) return false;
|
|
93
|
+
}
|
|
94
|
+
const fresh = cache ? refreshedWorkCache(cache) : undefined;
|
|
95
|
+
const status = workStatus({ projectRoot: projectRoot(roots), store: store.id, taskId: name.slice(0, -5), namespace, mtimeMs: info.mtimeMs, cache: fresh, fsImpl, shouldStop });
|
|
96
|
+
return !status.incomplete && !shouldStop?.() && status.state === 'finished'
|
|
97
|
+
&& status.since === candidate.mtimeMs && now - status.since > store.bound.finishedDays * DAY;
|
|
98
|
+
}
|
|
99
|
+
function workStorePathStatus(store , roots , fsImpl ) {
|
|
100
|
+
const project = projectRoot(roots), root = roots[store.root];
|
|
101
|
+
if (!project || !root) return 'missing';
|
|
102
|
+
const paths = [path.join(project, '.doflow'), root];
|
|
103
|
+
let current = root;
|
|
104
|
+
for (const segment of store.path.split('/')) { current = path.join(current, segment); paths.push(current); }
|
|
105
|
+
for (const candidate of paths) {
|
|
106
|
+
let info;
|
|
107
|
+
try { info = fsImpl.lstatSync(candidate); }
|
|
108
|
+
catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR' ? 'missing' : 'inaccessible'; }
|
|
109
|
+
if (info.isSymbolicLink()) return 'linked';
|
|
110
|
+
}
|
|
111
|
+
return 'safe';
|
|
112
|
+
}
|
|
113
|
+
function protectedLogs({ registry, roots, fsImpl, cache, shouldStop } ) {
|
|
114
|
+
const project = projectRoot(roots), protectedNames = new Set();
|
|
115
|
+
if (!project) return { names: protectedNames, incomplete: false };
|
|
116
|
+
const workCache = cache || createWorkCache({ projectRoot: project, fsImpl });
|
|
117
|
+
for (const store of registry.stores.filter((s ) => s.root === 'project-state' && s.entry === 'record' && s.rule === 'work')) {
|
|
118
|
+
if (shouldStop?.()) return { names: protectedNames, incomplete: true };
|
|
119
|
+
const pathStatus = workStorePathStatus(store, roots, fsImpl);
|
|
120
|
+
if (pathStatus === 'missing') continue;
|
|
121
|
+
if (pathStatus !== 'safe') return { names: protectedNames, incomplete: true, ...(pathStatus === 'linked' ? { reason: LINKED } : {}) };
|
|
122
|
+
const dir = path.join(roots[store.root], ...store.path.split('/'));
|
|
123
|
+
const recordMatch = new RegExp(store.match);
|
|
124
|
+
let linked = false;
|
|
125
|
+
const visit = (folder , namespace = null) => {
|
|
126
|
+
let list;
|
|
127
|
+
try { list = names(fsImpl, folder); }
|
|
128
|
+
catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR'; }
|
|
129
|
+
for (const name of list) {
|
|
130
|
+
if (shouldStop?.()) return false;
|
|
131
|
+
const file = path.join(folder, name);
|
|
132
|
+
let stat;
|
|
133
|
+
try { stat = fsImpl.lstatSync(file); }
|
|
134
|
+
catch (error ) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') continue; return false; }
|
|
135
|
+
if (stat.isSymbolicLink()) { linked = true; return false; }
|
|
136
|
+
if (namespace === null && stat.isDirectory() && SAFE_NAMESPACE.test(name) && !name.includes('..')) {
|
|
137
|
+
if (!visit(file, name)) return false;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!stat.isFile() || !recordMatch.test(name)) continue;
|
|
141
|
+
const status = workStatus({ projectRoot: project, store: store.id, taskId: name.slice(0, -5), namespace, mtimeMs: stat.mtimeMs, cache: workCache, fsImpl, shouldStop });
|
|
142
|
+
if (status.incomplete || shouldStop?.()) return false;
|
|
143
|
+
if (status.state === 'finished') continue;
|
|
144
|
+
try {
|
|
145
|
+
const text = fsImpl.readFileSync(file, 'utf8');
|
|
146
|
+
if (shouldStop?.()) return false;
|
|
147
|
+
const logPattern = new RegExp(LOG.source, 'g');
|
|
148
|
+
let match;
|
|
149
|
+
while ((match = logPattern.exec(text)) !== null) {
|
|
150
|
+
if (shouldStop?.()) return false;
|
|
151
|
+
protectedNames.add(match[0]);
|
|
152
|
+
}
|
|
153
|
+
} catch { return false; }
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
if (!visit(dir)) return { names: protectedNames, incomplete: true, ...(linked ? { reason: LINKED } : {}) };
|
|
158
|
+
}
|
|
159
|
+
return { names: protectedNames, incomplete: false };
|
|
160
|
+
}
|
|
161
|
+
function lifecyclePathsSafe(project , fsImpl ) {
|
|
162
|
+
for (const rel of [path.join('.doflow'), path.join('.doflow', 'state'), path.join('.doflow', 'state', 'lifecycle'), path.join('.doflow', 'state', 'lifecycle', 'events')]) {
|
|
163
|
+
try { if (fsImpl.lstatSync(path.join(project, rel)).isSymbolicLink()) return false; }
|
|
164
|
+
catch (error ) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') return false; if (rel !== path.join('.doflow', 'state', 'lifecycle', 'events')) return false; }
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
function definitelyMissing(fsImpl , target ) {
|
|
169
|
+
try { fsImpl.lstatSync(target); return false; }
|
|
170
|
+
catch (error ) { return error.code === 'ENOENT' || error.code === 'ENOTDIR'; }
|
|
171
|
+
}
|
|
172
|
+
function reportCandidates(store , entries , context ) {
|
|
173
|
+
const { fsImpl, storeDir, roots, now, settings, shouldStop } = context;
|
|
174
|
+
const project = projectRoot(roots);
|
|
175
|
+
const key = project && projectKey(project, fsImpl), selected = [];
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
if (shouldStop?.()) throw budgetError();
|
|
178
|
+
const folder = path.join(storeDir, entry.name);
|
|
179
|
+
let children, bodyNames = [];
|
|
180
|
+
try {
|
|
181
|
+
children = names(fsImpl, folder);
|
|
182
|
+
for (const name of children) {
|
|
183
|
+
if (shouldStop?.()) throw budgetError();
|
|
184
|
+
if (BODY.test(name) && safeStat(fsImpl, path.join(folder, name))?.isFile()) bodyNames.push(name);
|
|
185
|
+
}
|
|
186
|
+
} catch (error ) { if (error.partial) throw error; continue; }
|
|
187
|
+
if (entry.name === key) {
|
|
188
|
+
const lifecycle = path.join(project, '.doflow/state/lifecycle');
|
|
189
|
+
if (!safeStat(fsImpl, path.join(lifecycle, 'migrated.json'))?.isFile() || !lifecyclePathsSafe(project, fsImpl)) continue;
|
|
190
|
+
let snapshot;
|
|
191
|
+
try { snapshot = readEvents(project, { fsImpl, lockFallback: false, shouldStop }); }
|
|
192
|
+
catch { continue; }
|
|
193
|
+
if (snapshot.incomplete) throw budgetError();
|
|
194
|
+
if (snapshot.unreadable.length) continue;
|
|
195
|
+
const eventIds = new Set();
|
|
196
|
+
for (const event of snapshot.events) {
|
|
197
|
+
if (shouldStop?.()) throw budgetError();
|
|
198
|
+
const ids = [event.data.id, ...(Array.isArray(event.data.ids) ? event.data.ids : [])];
|
|
199
|
+
for (const id of ids) {
|
|
200
|
+
if (shouldStop?.()) throw budgetError();
|
|
201
|
+
if (typeof id === 'string' && /^FU-[0-9a-hjkmnp-tv-z]{6}$/.test(id)) eventIds.add(id);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const body of bodyNames) {
|
|
205
|
+
if (shouldStop?.()) throw budgetError();
|
|
206
|
+
const stat = safeStat(fsImpl, path.join(folder, body));
|
|
207
|
+
if (stat && !eventIds.has(body.slice(0, -4)) && now - stat.mtimeMs > settings.orphanGraceHours * HOUR)
|
|
208
|
+
selected.push({ name: `${entry.name}/${body}`, reason: 'orphan-body', bytes: stat.size, mtimeMs: stat.mtimeMs });
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const marker = path.join(folder, '.root');
|
|
212
|
+
if (!safeStat(fsImpl, marker)?.isFile() || !bodyNames.length) continue;
|
|
213
|
+
let validFolder = true;
|
|
214
|
+
for (const name of children) {
|
|
215
|
+
if (shouldStop?.()) throw budgetError();
|
|
216
|
+
if (name !== '.root' && !bodyNames.includes(name)) { validFolder = false; break; }
|
|
217
|
+
}
|
|
218
|
+
if (!validFolder) continue;
|
|
219
|
+
let root;
|
|
220
|
+
try { root = fsImpl.readFileSync(marker, 'utf8').trim(); } catch { continue; }
|
|
221
|
+
if (!path.isAbsolute(root) || !definitelyMissing(fsImpl, root)) continue;
|
|
222
|
+
let newest = -Infinity;
|
|
223
|
+
for (const body of bodyNames) {
|
|
224
|
+
if (shouldStop?.()) throw budgetError();
|
|
225
|
+
const stat = safeStat(fsImpl, path.join(folder, body));
|
|
226
|
+
if (!stat) { newest = Infinity; break; }
|
|
227
|
+
newest = Math.max(newest, stat.mtimeMs);
|
|
228
|
+
}
|
|
229
|
+
if (now - newest > store.bound.foreignMaxAgeDays * DAY)
|
|
230
|
+
selected.push({ name: entry.name, reason: 'foreign-root-gone', bytes: entry.stat.size, mtimeMs: newest });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return selected;
|
|
234
|
+
}
|
|
235
|
+
function selectCandidates(store , entries , context ) {
|
|
236
|
+
const { now, fsImpl, storeDir, roots, shouldStop } = context;
|
|
237
|
+
switch (store.rule) {
|
|
238
|
+
case 'count-and-age': {
|
|
239
|
+
const ordered = [...entries].sort((a, b) => Number(b.name.slice(0, 13)) - Number(a.name.slice(0, 13)) || a.name.localeCompare(b.name));
|
|
240
|
+
if (stopped(context)) return null;
|
|
241
|
+
const selected = [];
|
|
242
|
+
for (let index = 0; index < ordered.length; index += 1) {
|
|
243
|
+
if (shouldStop?.()) return null;
|
|
244
|
+
const entry = ordered[index], time = Number(entry.name.slice(0, 13));
|
|
245
|
+
if (index >= store.bound.keepNewest || now - time > store.bound.maxAgeDays * DAY)
|
|
246
|
+
selected.push({ name: entry.name, reason: index >= store.bound.keepNewest ? 'count' : 'age', bytes: entry.stat.size, mtimeMs: time });
|
|
247
|
+
}
|
|
248
|
+
return selected;
|
|
249
|
+
}
|
|
250
|
+
case 'date-partition':
|
|
251
|
+
case 'age': {
|
|
252
|
+
const selected = [];
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (shouldStop?.()) return null;
|
|
255
|
+
const reason = ageReason(store, entry, now, fsImpl, storeDir, shouldStop);
|
|
256
|
+
if (shouldStop?.()) return null;
|
|
257
|
+
if (reason) selected.push({ name: entry.name, reason, bytes: entry.stat.size, mtimeMs: entry.stat.mtimeMs });
|
|
258
|
+
}
|
|
259
|
+
return selected;
|
|
260
|
+
}
|
|
261
|
+
case 'work': return workCandidates(store, entries, context);
|
|
262
|
+
case 'report-orphan': return reportCandidates(store, entries, context);
|
|
263
|
+
case 'recovery': {
|
|
264
|
+
const root = roots[store.root]; let ledger;
|
|
265
|
+
try { ledger = JSON.parse(fsImpl.readFileSync(path.join(root, 'ledger.json'), 'utf8')); if (!ledger || typeof ledger !== 'object' || typeof ledger.lastRecoveryId !== 'string') return []; } catch { return []; }
|
|
266
|
+
const ordered = [...entries].sort((a, b) => b.name.localeCompare(a.name));
|
|
267
|
+
if (stopped(context)) return null;
|
|
268
|
+
const selected = [];
|
|
269
|
+
for (let index = store.bound.keepNewest; index < ordered.length; index += 1) {
|
|
270
|
+
if (shouldStop?.()) return null;
|
|
271
|
+
const entry = ordered[index];
|
|
272
|
+
if (entry.name.slice(0, -5) !== ledger.lastRecoveryId)
|
|
273
|
+
selected.push({ name: entry.name, reason: 'count', bytes: entry.stat.size, mtimeMs: entry.stat.mtimeMs });
|
|
274
|
+
}
|
|
275
|
+
return selected;
|
|
276
|
+
}
|
|
277
|
+
case 'lifecycle-settled': return [];
|
|
278
|
+
case 'existing': case 'control': return [];
|
|
279
|
+
default: throw new Error(`Unknown storage rule '${store.rule}'`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
module.exports={selectCandidates,recordStillCandidate,protectedLogs,removableRules,ruleOrder,projectRoot,BODY};
|