@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,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which event files the retention switch may delete. `DOFLOW_RETENTION_HOURS` overrides the window;
|
|
7
|
+
* when it is unset, the declared default applies only while the project's `prune` setting is on.
|
|
8
|
+
* Events are grouped into deletion units, the follow-ups and goals they name
|
|
9
|
+
* joined by any event that names more than one, and a unit goes only as a whole: when every item in
|
|
10
|
+
* it is settled in the fold (a follow-up done or dismissed, a goal done) and its newest event is
|
|
11
|
+
* older than the window. A unit whose removal would change anything else the fold shows (another
|
|
12
|
+
* item, a feature, a release, a conflict) is kept.
|
|
13
|
+
*
|
|
14
|
+
* `parseWindow` remains pure. `retentionWindow` composes its unset case with the declared storage
|
|
15
|
+
* default and the project's prune setting; explicit environment values retain their existing meaning.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const nodeFs = require('node:fs');
|
|
19
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
20
|
+
const { REPO_ROOT } = require('../../helper/repo-root.ts');
|
|
21
|
+
const { loadStorageRegistry } = require('../storage/registry.ts');
|
|
22
|
+
const { resolveRoots, readPruneSetting } = require('../storage/roots.ts');
|
|
23
|
+
const { foldInto, finalize } = require('./fold.ts');
|
|
24
|
+
|
|
25
|
+
const HOUR_MS = 3600000;
|
|
26
|
+
|
|
27
|
+
/** @returns {{state: 'off'} | {state: 'on', hours: number, ms: number} | {state: 'invalid', raw: string}} */
|
|
28
|
+
function parseWindow(env: Record<string, string | undefined> = process.env): { state: 'off' } | { state: 'on'; hours: number; ms: number } | { state: 'invalid'; raw: string } {
|
|
29
|
+
const raw = env.DOFLOW_RETENTION_HOURS;
|
|
30
|
+
if (raw === undefined || raw === '') return { state: 'off' };
|
|
31
|
+
if (/^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
32
|
+
const hours = Number(raw);
|
|
33
|
+
return { state: 'on', hours, ms: hours * HOUR_MS };
|
|
34
|
+
}
|
|
35
|
+
return { state: 'invalid', raw: String(raw) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Composes IC-013's explicit environment override with the declared lifecycle default. */
|
|
39
|
+
function retentionWindow({ env = process.env, projectRoot, registry, fsImpl = nodeFs }: any = {}): any {
|
|
40
|
+
const parsed = parseWindow(env);
|
|
41
|
+
if (parsed.state === 'invalid') return parsed;
|
|
42
|
+
if (parsed.state === 'on') return { ...parsed, source: 'env' };
|
|
43
|
+
let activeRegistry: any;
|
|
44
|
+
try {
|
|
45
|
+
activeRegistry = registry || loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl });
|
|
46
|
+
} catch {
|
|
47
|
+
return { state: 'off' };
|
|
48
|
+
}
|
|
49
|
+
const roots = resolveRoots({ registry: activeRegistry, projectRoot, env });
|
|
50
|
+
if (readPruneSetting({ registry: activeRegistry, roots, rootId: 'project-state', fsImpl }).setting !== 'on') return { state: 'off' };
|
|
51
|
+
const lifecycleStore = activeRegistry.stores.find((store: any) => store.id === 'lifecycle-events');
|
|
52
|
+
if (!lifecycleStore) throw new Error('Storage registry has no lifecycle-events store');
|
|
53
|
+
const hours = lifecycleStore.bound.defaultHours;
|
|
54
|
+
return { state: 'on', hours, ms: hours * HOUR_MS, source: 'default' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isText(value: any): boolean { return typeof value === 'string' && value.trim() !== ''; }
|
|
58
|
+
|
|
59
|
+
/** The item keys an event names, or none when it is not about a follow-up or a goal. */
|
|
60
|
+
function itemsOf(event: any): string[] {
|
|
61
|
+
const d = event.data;
|
|
62
|
+
const ids = (list: any): string[] => (Array.isArray(list) && list.length > 0 && list.every(isText) ? list : []);
|
|
63
|
+
switch (event.type) {
|
|
64
|
+
case 'followup.added': return isText(d.id) ? [`followup:${d.id}`] : [];
|
|
65
|
+
case 'followup.taken':
|
|
66
|
+
case 'followup.promoted': return ids(d.ids).map((id: string) => `followup:${id}`);
|
|
67
|
+
case 'followup.settled': return (isText(d.id) ? [d.id] : ids(d.ids)).map((id: string) => `followup:${id}`);
|
|
68
|
+
case 'goal.added':
|
|
69
|
+
case 'goal.item-added':
|
|
70
|
+
case 'goal.checked':
|
|
71
|
+
case 'goal.done':
|
|
72
|
+
case 'goal.linked': return isText(d.goal) ? [`goal:${d.goal}`] : [];
|
|
73
|
+
default: return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Groups the items into units: items named together by one event share a unit. */
|
|
78
|
+
function unitsOf(events: any[], shouldStop?: () => boolean): any[] | null {
|
|
79
|
+
const parent = new Map<string, string>();
|
|
80
|
+
const find = (key: string): string | null => {
|
|
81
|
+
while (parent.get(key) !== key) {
|
|
82
|
+
if (shouldStop?.()) return null;
|
|
83
|
+
parent.set(key, parent.get(parent.get(key) as string) as string);
|
|
84
|
+
key = parent.get(key) as string;
|
|
85
|
+
}
|
|
86
|
+
return key;
|
|
87
|
+
};
|
|
88
|
+
const named: Array<{ event: any; items: string[] }> = [];
|
|
89
|
+
for (const event of events) {
|
|
90
|
+
if (shouldStop?.()) return null;
|
|
91
|
+
named.push({ event, items: [...new Set(itemsOf(event))] });
|
|
92
|
+
}
|
|
93
|
+
for (const { items } of named) {
|
|
94
|
+
if (shouldStop?.()) return null;
|
|
95
|
+
for (const key of items) if (!parent.has(key)) parent.set(key, key);
|
|
96
|
+
for (const key of items.slice(1)) {
|
|
97
|
+
const root = find(key), head = find(items[0]);
|
|
98
|
+
if (root === null || head === null) return null;
|
|
99
|
+
parent.set(root, head);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const units = new Map<string, any>();
|
|
103
|
+
for (const { event, items } of named) {
|
|
104
|
+
if (shouldStop?.()) return null;
|
|
105
|
+
if (items.length === 0) continue;
|
|
106
|
+
const head = find(items[0]);
|
|
107
|
+
if (head === null) return null;
|
|
108
|
+
if (!units.has(head)) units.set(head, { items: new Set(), events: [] });
|
|
109
|
+
const unit = units.get(head);
|
|
110
|
+
for (const key of items) unit.items.add(key);
|
|
111
|
+
unit.events.push(event);
|
|
112
|
+
}
|
|
113
|
+
return [...units.values()];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isSettled(state: any, key: string): boolean {
|
|
117
|
+
const [kind, id] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
|
|
118
|
+
if (kind === 'followup') {
|
|
119
|
+
const item = state.items.get(id);
|
|
120
|
+
return Boolean(item) && (item.state === 'done' || item.state === 'dismissed');
|
|
121
|
+
}
|
|
122
|
+
const goal = state.goals.get(id);
|
|
123
|
+
return Boolean(goal) && goal.status === 'done';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The parts of a fold that removing a unit must leave unchanged. */
|
|
127
|
+
function comparable(result: any): any {
|
|
128
|
+
const { followups, features, merged, goals, releases, conflicts } = result;
|
|
129
|
+
return { followups, features, merged, goals, releases, conflicts };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Whether dropping `removed` units changes nothing but the removed items themselves. */
|
|
133
|
+
function refoldEqual(events: any[], full: any, removed: any[], now: Date, shouldStop?: () => boolean): boolean | null {
|
|
134
|
+
if (shouldStop?.()) return null;
|
|
135
|
+
const gone = new Set<string>(), keys = new Set<string>();
|
|
136
|
+
for (const unit of removed) {
|
|
137
|
+
if (shouldStop?.()) return null;
|
|
138
|
+
for (const event of unit.events) gone.add(event.id);
|
|
139
|
+
for (const key of unit.items) keys.add(key);
|
|
140
|
+
}
|
|
141
|
+
const expected = {
|
|
142
|
+
...full,
|
|
143
|
+
followups: full.followups.filter((f: any) => !keys.has(`followup:${f.id}`)),
|
|
144
|
+
goals: full.goals.filter((g: any) => !keys.has(`goal:${g.goal}`)),
|
|
145
|
+
conflicts: full.conflicts.filter((c: any) => !gone.has(c.event)),
|
|
146
|
+
};
|
|
147
|
+
const remaining: any[] = [];
|
|
148
|
+
for (const event of events) {
|
|
149
|
+
if (shouldStop?.()) return null;
|
|
150
|
+
if (!gone.has(event.id)) remaining.push(event);
|
|
151
|
+
}
|
|
152
|
+
const state = foldInto(remaining, { now, shouldStop });
|
|
153
|
+
if (state.incomplete) return null;
|
|
154
|
+
return isDeepStrictEqual(comparable(finalize(state)), comparable(expected));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {Object[]} events IC-002 envelopes, as readEvents returns them
|
|
159
|
+
* @param {{now: Date, windowMs: number, shouldStop?: Function}} options
|
|
160
|
+
* @returns {{files: string[], units: Array<{items: string[], newestAt: string, files: string[]}>, incomplete?: true}}
|
|
161
|
+
* files: `<id>.json` names, sorted
|
|
162
|
+
*/
|
|
163
|
+
function selectExpired(events: any[], { now, windowMs, shouldStop }: any = {}): any {
|
|
164
|
+
const incomplete = (): any => ({ files: [], units: [], incomplete: true });
|
|
165
|
+
if (shouldStop?.()) return incomplete();
|
|
166
|
+
const state = foldInto(events, { now, shouldStop });
|
|
167
|
+
if (state.incomplete || shouldStop?.()) return incomplete();
|
|
168
|
+
const cutoff = now.getTime() - windowMs;
|
|
169
|
+
const served = new Set<string>();
|
|
170
|
+
for (const feature of state.tracked.values()) {
|
|
171
|
+
if (shouldStop?.()) return incomplete();
|
|
172
|
+
if (feature.goal) served.add(feature.goal);
|
|
173
|
+
}
|
|
174
|
+
const named = unitsOf(events, shouldStop);
|
|
175
|
+
if (!named || shouldStop?.()) return incomplete();
|
|
176
|
+
const candidates: any[] = [];
|
|
177
|
+
for (const unit of named) {
|
|
178
|
+
if (shouldStop?.()) return incomplete();
|
|
179
|
+
const newest = unit.events.reduce((max: string, event: any) => (Date.parse(event.at) > Date.parse(max) ? event.at : max), unit.events[0].at);
|
|
180
|
+
if (shouldStop?.()) return incomplete();
|
|
181
|
+
let eligible = Date.parse(newest) < cutoff;
|
|
182
|
+
if (eligible) for (const key of unit.items) {
|
|
183
|
+
if (shouldStop?.()) return incomplete();
|
|
184
|
+
if (!isSettled(state, key) || (key.startsWith('goal:') && served.has(key.slice('goal:'.length)))) { eligible = false; break; }
|
|
185
|
+
}
|
|
186
|
+
if (eligible) candidates.push({ ...unit, newestAt: newest });
|
|
187
|
+
}
|
|
188
|
+
const full = finalize(state);
|
|
189
|
+
let selected = candidates;
|
|
190
|
+
const allSafe = refoldEqual(events, full, candidates, now, shouldStop);
|
|
191
|
+
if (allSafe === null || shouldStop?.()) return incomplete();
|
|
192
|
+
if (!allSafe) {
|
|
193
|
+
selected = [];
|
|
194
|
+
const ordered = [...candidates].sort((a: any, b: any) => Date.parse(a.newestAt) - Date.parse(b.newestAt));
|
|
195
|
+
if (shouldStop?.()) return incomplete();
|
|
196
|
+
for (const unit of ordered) {
|
|
197
|
+
if (shouldStop?.()) return incomplete();
|
|
198
|
+
const safe = refoldEqual(events, full, [...selected, unit], now, shouldStop);
|
|
199
|
+
if (safe === null) return incomplete();
|
|
200
|
+
if (safe) selected.push(unit);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const units: any[] = [];
|
|
204
|
+
for (const unit of selected) {
|
|
205
|
+
if (shouldStop?.()) return incomplete();
|
|
206
|
+
units.push({ items: [...unit.items].sort(), newestAt: unit.newestAt, files: unit.events.map((event: any) => `${event.id}.json`).sort() });
|
|
207
|
+
}
|
|
208
|
+
if (shouldStop?.()) return incomplete();
|
|
209
|
+
return { files: units.flatMap((unit: any) => unit.files).sort(), units };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { parseWindow, retentionWindow, selectExpired };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The project root every lifecycle record is keyed by (IC-001, DEC-043). Paths are real paths,
|
|
7
|
+
* because git reports the resolved spelling and a temp directory is often reached through a symlink.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { execFileSync } = require('node:child_process');
|
|
13
|
+
|
|
14
|
+
interface WorktreeEntry { path: string; bare: boolean; prunable: boolean }
|
|
15
|
+
|
|
16
|
+
function git(cwd: string, args: string[]): string {
|
|
17
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function real(target: string): string {
|
|
21
|
+
try { return fs.realpathSync(target); } catch { return path.resolve(target); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The entries of `git worktree list --porcelain`: `{path, bare, prunable}`, in git's order. */
|
|
25
|
+
function worktreeEntries(porcelain: string): WorktreeEntry[] {
|
|
26
|
+
const entries: WorktreeEntry[] = [];
|
|
27
|
+
for (const block of porcelain.split(/\n\s*\n/)) {
|
|
28
|
+
const lines = block.split('\n').map((line) => line.replace(/\r$/, ''));
|
|
29
|
+
const head = lines.find((line) => line.startsWith('worktree '));
|
|
30
|
+
if (!head) continue;
|
|
31
|
+
entries.push({
|
|
32
|
+
path: head.slice('worktree '.length),
|
|
33
|
+
bare: lines.some((line) => line.trim() === 'bare'),
|
|
34
|
+
prunable: lines.some((line) => line === 'prunable' || line.startsWith('prunable ')),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return entries;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A working tree that exists and that git itself calls one: a submodule's or a separate git dir's
|
|
41
|
+
* entry points at a git directory, where `--is-inside-work-tree` prints `false`. */
|
|
42
|
+
function isRealWorkTree(entry: WorktreeEntry): boolean {
|
|
43
|
+
if (entry.bare || entry.prunable || !fs.existsSync(entry.path)) return false;
|
|
44
|
+
try { return git(entry.path, ['rev-parse', '--is-inside-work-tree']).trim() === 'true'; } catch { return false; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function showToplevel(cwd: string): string | null {
|
|
48
|
+
try {
|
|
49
|
+
const top = git(cwd, ['rev-parse', '--show-toplevel']).trim();
|
|
50
|
+
return top ? real(top) : null;
|
|
51
|
+
} catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* DEC-043. The first entry of `git worktree list --porcelain` when it is a real working tree, so
|
|
56
|
+
* every linked worktree of one clone shares one store. When the first entry is the bare repository
|
|
57
|
+
* of a bare-clone layout, there is no main working tree to share, and the current worktree's own
|
|
58
|
+
* root is used. In every other case, and outside a repository, `--show-toplevel` and then the
|
|
59
|
+
* working directory.
|
|
60
|
+
* @param {string} [cwd] defaults to the process working directory
|
|
61
|
+
* @returns {string} an absolute, real path
|
|
62
|
+
*/
|
|
63
|
+
function projectRoot(cwd: string = process.cwd()): string {
|
|
64
|
+
let entries: WorktreeEntry[] = [];
|
|
65
|
+
try { entries = worktreeEntries(git(cwd, ['worktree', 'list', '--porcelain'])); } catch { /* not a repository */ }
|
|
66
|
+
const first = entries[0];
|
|
67
|
+
if (first && !first.bare && isRealWorkTree(first)) return real(first.path);
|
|
68
|
+
return showToplevel(cwd) || real(cwd);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { projectRoot, worktreeEntries };
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Feature status deriver (IC-021, DEC-022, DEC-044, DEC-030, DEC-035). A tracked feature is
|
|
7
|
+
* `finished`, `awaiting-release`, `in-progress` or `unknown`, derived on every read from git and the
|
|
8
|
+
* release records, and never written. A later read can change a status (design R9).
|
|
9
|
+
*
|
|
10
|
+
* The git facts that are not about one feature (the integration ref, the feature branch prefixes,
|
|
11
|
+
* the release tags and the base tag) come from `do-git-state.sh --lifecycle`, so bash and Node never
|
|
12
|
+
* derive them twice. The rest is a fixed number of plain `git` calls, however many features are
|
|
13
|
+
* tracked: one `rev-parse` pins the ref, one `for-each-ref` lists the branches, one
|
|
14
|
+
* `git log --first-parent --merges` lists the merges, one `git tag --list` (tagged mode) gives the
|
|
15
|
+
* release tags' commits, and one `git rev-list --parents` reads the commit graph of the pinned ref
|
|
16
|
+
* and those tags. Every ancestry question (which merge brought a branch tip in, which tag contains a
|
|
17
|
+
* merge) is then answered from that graph in memory, not by a git call per feature.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here writes, fetches or reaches a network.
|
|
20
|
+
*
|
|
21
|
+
* Accepted ceilings, stated rather than hidden: a feature's merge evidence is only as good as the
|
|
22
|
+
* merge commit's subject and the branch name (a squash, a rebase and a fast-forward leave none,
|
|
23
|
+
* so they reach the user through `notDetected` and `lifecycle --action merged`); the merge log and
|
|
24
|
+
* the graph are read in full, without `--since` or a lower cut (a skewed clock must not hide a
|
|
25
|
+
* merge), so their cost grows with the history of the integration ref, once per call; git dates
|
|
26
|
+
* have one-second resolution, so the lower bound is floored to the second.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const { execFileSync } = require('node:child_process');
|
|
32
|
+
const { REPO_ROOT } = require('../../helper/repo-root.ts');
|
|
33
|
+
const { resolveBashHelper } = require('../../helper/bash-helper.ts');
|
|
34
|
+
|
|
35
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
/** Where `do-git-state.sh` is: the shared resolver (package copy first, else the one projected beside the runtime). */
|
|
38
|
+
function resolveGitStateHelper(repoRoot: string = REPO_ROOT, existsImpl: (file: string) => boolean = fs.existsSync): string | null {
|
|
39
|
+
return resolveBashHelper('do-git-state.sh', repoRoot, existsImpl);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function git(root: string, args: string[]): string {
|
|
43
|
+
return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: MAX_BUFFER, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The facts from `do-git-state.sh --lifecycle`, read in `root`; `runtimeRoot` is where the runtime
|
|
48
|
+
* lives, which only a test moves.
|
|
49
|
+
* @returns {{integration_ref:string|null, feature_prefixes:string[], release_tags:string[], base_tag:string|null}
|
|
50
|
+
* |{error:string}}
|
|
51
|
+
*/
|
|
52
|
+
function readGitFacts(root: string, runtimeRoot: string = REPO_ROOT): any {
|
|
53
|
+
const helper = resolveGitStateHelper(runtimeRoot);
|
|
54
|
+
if (!helper) return { error: 'git-state-helper-missing' };
|
|
55
|
+
try {
|
|
56
|
+
const out = execFileSync('bash', [helper, '--lifecycle'], { cwd: root, encoding: 'utf8', maxBuffer: MAX_BUFFER, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
57
|
+
const facts = JSON.parse(out);
|
|
58
|
+
if (facts.error) return { error: facts.error };
|
|
59
|
+
return facts;
|
|
60
|
+
} catch (error: any) {
|
|
61
|
+
return { error: error.code === 'ENOENT' ? 'bash-not-found' : 'git-state-failed' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeRegExp(text: string): string { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
66
|
+
|
|
67
|
+
/** The slug appears in the subject bounded by its ends or by characters outside `[a-z0-9-]`. */
|
|
68
|
+
function subjectNames(subject: string, slug: string): boolean {
|
|
69
|
+
return new RegExp(`(?:^|[^a-z0-9-])${escapeRegExp(slug)}(?:$|[^a-z0-9-])`).test(subject);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** First-parent merges of `ref`, oldest first. */
|
|
73
|
+
function readMerges(root: string, ref: string): Array<{ sha: string; parents: string[]; ct: number; subject: string }> {
|
|
74
|
+
const out = git(root, ['log', '--first-parent', '--merges', '--format=%H%x09%P%x09%ct%x09%s', ref]);
|
|
75
|
+
return out.split('\n').filter(Boolean).map((line) => {
|
|
76
|
+
const [sha, parents, ct, ...subject] = line.split('\t');
|
|
77
|
+
return { sha, parents: parents.split(' ').filter(Boolean), ct: Number(ct), subject: subject.join('\t') };
|
|
78
|
+
}).reverse();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Branch refs by short name (`feat/x`, `origin/feat/x`) with their tips; local branches first. */
|
|
82
|
+
function readBranchTips(root: string): Array<{ short: string; local: string; sha: string }> {
|
|
83
|
+
const out = git(root, ['for-each-ref', '--format=%(refname)%09%(objectname)', 'refs/heads', 'refs/remotes']);
|
|
84
|
+
const tips: Array<{ short: string; local: string; sha: string }> = [];
|
|
85
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
86
|
+
const [refname, sha] = line.split('\t');
|
|
87
|
+
if (refname.startsWith('refs/heads/')) tips.push({ short: refname.slice('refs/heads/'.length), local: refname.slice('refs/heads/'.length), sha });
|
|
88
|
+
else if (refname.startsWith('refs/remotes/')) {
|
|
89
|
+
const short = refname.slice('refs/remotes/'.length);
|
|
90
|
+
tips.push({ short, local: short.slice(short.indexOf('/') + 1), sha });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return tips;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The commit a tag names, peeled through any number of tag objects, or null when it is no commit. */
|
|
97
|
+
function peelTag(root: string, refname: string): string | null {
|
|
98
|
+
try { return git(root, ['rev-parse', '--verify', '--quiet', `${refname}^{commit}`]).trim() || null; } catch { return null; }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The `v*` tags named in `wanted` with their commits, in the order `git tag` lists them (it honours
|
|
103
|
+
* `tag.sort`, as `git tag --contains` did), so the first containing tag is the one it would name.
|
|
104
|
+
* @returns {Array<{name:string, commit:string|null}>}
|
|
105
|
+
*/
|
|
106
|
+
function readTagCommits(root: string, wanted: Set<string>): Array<{ name: string; commit: string | null }> {
|
|
107
|
+
const out = git(root, ['tag', '--list', 'v*', '--format=%(refname)%09%(objecttype)%09%(objectname)%09%(*objecttype)%09%(*objectname)']);
|
|
108
|
+
const tags: Array<{ name: string; commit: string | null }> = [];
|
|
109
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
110
|
+
const [refname, type, object, peeledType, peeled] = line.split('\t');
|
|
111
|
+
const name = refname.slice('refs/tags/'.length);
|
|
112
|
+
if (!wanted.has(name)) continue;
|
|
113
|
+
let commit: string | null = null;
|
|
114
|
+
if (type === 'commit') commit = object;
|
|
115
|
+
else if (type === 'tag' && peeledType === 'commit') commit = peeled;
|
|
116
|
+
else if (type === 'tag') commit = peelTag(root, refname); // a tag of a tag: rare, one call each
|
|
117
|
+
tags.push({ name, commit });
|
|
118
|
+
}
|
|
119
|
+
return tags;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The commit graph reachable from `pinned` and `revs`, read by one `git rev-list --parents`, with the
|
|
124
|
+
* ancestry questions the status and the release ask answered in memory.
|
|
125
|
+
*
|
|
126
|
+
* The first-parent chain of `pinned` is numbered oldest first. Each commit is marked with the oldest
|
|
127
|
+
* chain commit whose history holds it (its `introducer`): for a commit that came in through a merge
|
|
128
|
+
* that is the merge, and for a commit on the chain it is the commit itself. Each commit also knows the
|
|
129
|
+
* newest chain commit in its own history (`chainMax`), so "is chain commit C an ancestor of X" is one
|
|
130
|
+
* comparison: the chain commits in X's history are always a prefix of the chain.
|
|
131
|
+
*
|
|
132
|
+
* Every commit asked about must be in the read: `pinned`, one of `revs`, or an ancestor of one.
|
|
133
|
+
* @param {string} root
|
|
134
|
+
* @param {string} pinned a full commit sha
|
|
135
|
+
* @param {string[]} [revs] further full commit shas whose history is read too
|
|
136
|
+
*/
|
|
137
|
+
function readHistory(root: string, pinned: string, revs: string[] = []): { introducer: (sha: string) => string | undefined; isAncestor: (a: string, b: string) => boolean } {
|
|
138
|
+
const input = `${[pinned, ...revs].join('\n')}\n`;
|
|
139
|
+
const out = execFileSync('git', ['rev-list', '--parents', '--topo-order', '--stdin'], { cwd: root, input, encoding: 'utf8', maxBuffer: MAX_BUFFER, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
140
|
+
const parents = new Map<string, string[]>();
|
|
141
|
+
const order: string[] = [];
|
|
142
|
+
for (const line of out.split('\n')) {
|
|
143
|
+
if (!line) continue;
|
|
144
|
+
const [sha, ...rest] = line.split(' ');
|
|
145
|
+
parents.set(sha, rest);
|
|
146
|
+
order.push(sha);
|
|
147
|
+
}
|
|
148
|
+
const chain: string[] = [];
|
|
149
|
+
for (let sha = pinned; parents.has(sha); sha = (parents.get(sha) as string[])[0]) chain.push(sha);
|
|
150
|
+
chain.reverse();
|
|
151
|
+
const chainIndex = new Map<string, number>(chain.map((sha, i): [string, number] => [sha, i]));
|
|
152
|
+
|
|
153
|
+
const introducer = new Map<string, string>();
|
|
154
|
+
for (const head of chain) {
|
|
155
|
+
const stack: string[] = [head];
|
|
156
|
+
while (stack.length) {
|
|
157
|
+
const sha = stack.pop() as string;
|
|
158
|
+
if (introducer.has(sha) || !parents.has(sha)) continue;
|
|
159
|
+
introducer.set(sha, head);
|
|
160
|
+
stack.push(...(parents.get(sha) as string[]));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --topo-order lists no parent before its children, so walking it backwards meets parents first.
|
|
165
|
+
const chainMax = new Map<string, number>();
|
|
166
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
167
|
+
const sha = order[i];
|
|
168
|
+
let max: number = chainIndex.has(sha) ? (chainIndex.get(sha) as number) : -1;
|
|
169
|
+
if (max === -1) for (const parent of parents.get(sha) as string[]) max = Math.max(max, chainMax.get(parent) ?? -1);
|
|
170
|
+
chainMax.set(sha, max);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const reached = new Map<string, Set<string>>();
|
|
174
|
+
function ancestorsOf(sha: string): Set<string> {
|
|
175
|
+
let seen = reached.get(sha);
|
|
176
|
+
if (seen) return seen;
|
|
177
|
+
seen = new Set<string>();
|
|
178
|
+
const stack: string[] = [sha];
|
|
179
|
+
while (stack.length) {
|
|
180
|
+
const next = stack.pop() as string;
|
|
181
|
+
if (seen.has(next) || !parents.has(next)) continue;
|
|
182
|
+
seen.add(next);
|
|
183
|
+
stack.push(...(parents.get(next) as string[]));
|
|
184
|
+
}
|
|
185
|
+
reached.set(sha, seen);
|
|
186
|
+
return seen;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
/** The chain commit that brought `sha` into `pinned`'s history, or undefined when it is not in it. */
|
|
191
|
+
introducer: (sha: string) => introducer.get(sha),
|
|
192
|
+
/** `a` is `b` or an ancestor of it; `a` must be in the read. */
|
|
193
|
+
isAncestor(a: string, b: string): boolean {
|
|
194
|
+
const index = chainIndex.get(a);
|
|
195
|
+
if (index !== undefined) return index <= (chainMax.get(b) ?? -1);
|
|
196
|
+
return ancestorsOf(b).has(a);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The merge that brought `tip` into the chain, if the evidence rule holds: the oldest first-parent
|
|
203
|
+
* merge having the tip as an ancestor, committed at or after the lower bound, with the tip on a
|
|
204
|
+
* non-first parent and not already on the first. A tip that lies on the first-parent chain itself
|
|
205
|
+
* (a fast-forward, or a branch with no commits of its own) is its own introducer, so it never counts;
|
|
206
|
+
* a tip the merge holds but its first parent does not came in on a non-first parent.
|
|
207
|
+
*/
|
|
208
|
+
function introducingMerge(history: () => any, mergeBySha: Map<string, any>, tip: string, lowerBound: number): any {
|
|
209
|
+
const merge = mergeBySha.get(history().introducer(tip));
|
|
210
|
+
if (!merge || merge.sha === tip || merge.ct < lowerBound) return null;
|
|
211
|
+
return merge;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Evidence of a merge of `slug` into the pinned ref, tried in IC-021's order, or null. */
|
|
215
|
+
function findEvidence({ slug, lowerBound, merges, mergeBySha, newestCt, history, tips, prefixes, confirmed }: any): { kind: string; ref: string | null; commit: string | null } | null {
|
|
216
|
+
const branchNames = new Set(prefixes.map((prefix: string) => `${prefix}/${slug}`));
|
|
217
|
+
// No merge is recent enough for this feature: no branch can have one, so the graph need not be read for it.
|
|
218
|
+
const branchTips = newestCt < lowerBound ? [] : tips.filter((t: any) => branchNames.has(t.local));
|
|
219
|
+
for (const tip of branchTips) {
|
|
220
|
+
const merge = introducingMerge(history, mergeBySha, tip.sha, lowerBound);
|
|
221
|
+
if (merge) return { kind: 'branch', ref: tip.short, commit: merge.sha };
|
|
222
|
+
}
|
|
223
|
+
for (let i = merges.length - 1; i >= 0; i -= 1) {
|
|
224
|
+
if (merges[i].ct >= lowerBound && subjectNames(merges[i].subject, slug)) return { kind: 'merge-subject', ref: merges[i].sha.slice(0, 7), commit: merges[i].sha };
|
|
225
|
+
}
|
|
226
|
+
if (confirmed) return { kind: 'confirmed', ref: null, commit: null };
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A value computed on first use, so a call that needs no graph never reads one. */
|
|
231
|
+
function lazy<T>(make: () => T): () => T {
|
|
232
|
+
let value: T;
|
|
233
|
+
let made = false;
|
|
234
|
+
return () => {
|
|
235
|
+
if (!made) { value = make(); made = true; }
|
|
236
|
+
return value;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const BUCKET: Record<string, string> = { finished: 'finished', 'awaiting-release': 'awaitingRelease', 'in-progress': 'inProgress', unknown: 'unknown' };
|
|
241
|
+
|
|
242
|
+
function emptyBuckets(): Record<string, string[]> { return { finished: [], awaitingRelease: [], inProgress: [], unknown: [] }; }
|
|
243
|
+
|
|
244
|
+
/** Slugs grouped in the four buckets IC-007 and IC-022 show. */
|
|
245
|
+
function bucketize(statuses: any, slugs: string[] = Object.keys(statuses)): Record<string, string[]> {
|
|
246
|
+
const buckets = emptyBuckets();
|
|
247
|
+
for (const slug of slugs) {
|
|
248
|
+
const entry = statuses[slug];
|
|
249
|
+
if (entry) buckets[BUCKET[entry.status]].push(slug);
|
|
250
|
+
}
|
|
251
|
+
return buckets;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function result(releaseMode: string, integrationRef: string | null, reason: string | null, statuses: any, notDetected: string[] = [], behind: number = 0): any {
|
|
255
|
+
return { releaseMode, integrationRef, integrationBehind: behind, reason, statuses, features: bucketize(statuses), notDetected };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The one-line note a stale local integration branch earns (DEC-044), or null. */
|
|
259
|
+
function behindNote(ref: string, behind: number): string | null {
|
|
260
|
+
return behind > 0
|
|
261
|
+
? `local ${ref} is ${behind} commit${behind === 1 ? '' : 's'} behind origin/${ref}; a merge that exists only on origin is not seen until you pull`
|
|
262
|
+
: null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Derives the status of every tracked feature.
|
|
267
|
+
*
|
|
268
|
+
* @param {Object} options
|
|
269
|
+
* @param {string} options.root the IC-001 root, where git is run
|
|
270
|
+
* @param {Object} options.fold a fold result (tracked features, merge confirmations, release records)
|
|
271
|
+
* @param {Object} [options.facts] `readGitFacts` output, for a caller that already has it
|
|
272
|
+
* @returns {{releaseMode:'tagged'|'untagged'|'unknown', integrationRef:string|null, integrationBehind:number, reason:string|null,
|
|
273
|
+
* integrationSha:string|null (the commit the ref was pinned to; absent when nothing was derived),
|
|
274
|
+
* evidenceCommits:Object<string,string|null> (slug to the full merge commit of its evidence; absent when nothing was derived),
|
|
275
|
+
* statuses:Object<string,{status:string, evidence:{kind:string,ref:string|null}|null, release:string|null}>,
|
|
276
|
+
* features:Object<string,string[]>, notDetected:string[],
|
|
277
|
+
* failure?:string (git, or the git-facts helper, failed while deriving: `git-state-failed`, `bash-not-found`,
|
|
278
|
+
* `git-state-helper-missing`; an environment fault, so every status is `unknown`)}}
|
|
279
|
+
*/
|
|
280
|
+
function deriveStatuses({ root, fold, facts = readGitFacts(root) }: { root: string; fold: any; facts?: any }): any {
|
|
281
|
+
const tracked = fold.features;
|
|
282
|
+
const unknown = (reason: string) => result('unknown', facts.integration_ref ?? null, reason,
|
|
283
|
+
Object.fromEntries(tracked.map((f: any) => [f.slug, { status: 'unknown', evidence: null, release: null }])));
|
|
284
|
+
if (facts.error) return { ...unknown(`git facts unavailable: ${facts.error}`), failure: facts.error };
|
|
285
|
+
if (!facts.integration_ref) return unknown('no integration ref resolves (develop, main, master, origin/HEAD)');
|
|
286
|
+
|
|
287
|
+
const ref = facts.integration_ref;
|
|
288
|
+
const releaseMode = facts.release_tags.length > 0 ? 'tagged' : 'untagged';
|
|
289
|
+
const behind = Number(facts.integration_local_behind_remote) || 0;
|
|
290
|
+
if (tracked.length === 0) {
|
|
291
|
+
let sha = null;
|
|
292
|
+
try { sha = git(root, ['rev-parse', '--verify', `${ref}^{commit}`]).trim(); } catch { /* the ref does not name a commit */ }
|
|
293
|
+
return { ...result(releaseMode, ref, null, {}, [], behind), integrationSha: sha, evidenceCommits: {} };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// Pin the ref once: every query below names the same commit.
|
|
298
|
+
const pinned = git(root, ['rev-parse', '--verify', `${ref}^{commit}`]).trim();
|
|
299
|
+
const merges = readMerges(root, pinned);
|
|
300
|
+
const mergeBySha = new Map(merges.map((m): [string, any] => [m.sha, m]));
|
|
301
|
+
const newestCt = merges.reduce((max, m) => Math.max(max, m.ct), -Infinity);
|
|
302
|
+
const tips = readBranchTips(root);
|
|
303
|
+
const mergedSlugs = new Set<string>(fold.merged.map((m: any) => m.slug));
|
|
304
|
+
const recorded = new Map<string, string>();
|
|
305
|
+
for (const release of fold.releases) for (const f of release.features) if (!recorded.has(f.slug)) recorded.set(f.slug, release.tag);
|
|
306
|
+
const recordedTags = new Set(fold.releases.map((r: any) => r.tag));
|
|
307
|
+
const unrecordedTags = new Set<string>(facts.release_tags.filter((tag: string) => !recordedTags.has(tag)));
|
|
308
|
+
// Only a tag with no release record can finish a feature by containment (DEC-030), so only those are read.
|
|
309
|
+
const tags = lazy(() => (releaseMode === 'tagged' && unrecordedTags.size > 0 ? readTagCommits(root, unrecordedTags) : []));
|
|
310
|
+
const history = lazy(() => readHistory(root, pinned, tags().map((t) => t.commit).filter(Boolean) as string[]));
|
|
311
|
+
|
|
312
|
+
const statuses: Record<string, any> = {};
|
|
313
|
+
const evidenceCommits: Record<string, string | null> = {};
|
|
314
|
+
const notDetected: string[] = [];
|
|
315
|
+
for (const feature of tracked) {
|
|
316
|
+
const lowerBound = Math.floor(Date.parse(feature.trackedAt) / 1000);
|
|
317
|
+
const evidence = findEvidence({
|
|
318
|
+
slug: feature.slug, lowerBound, merges, mergeBySha, newestCt, history, tips, prefixes: facts.feature_prefixes, confirmed: mergedSlugs.has(feature.slug),
|
|
319
|
+
});
|
|
320
|
+
const shown = evidence && { kind: evidence.kind, ref: evidence.ref };
|
|
321
|
+
evidenceCommits[feature.slug] = evidence ? evidence.commit : null;
|
|
322
|
+
let status: string;
|
|
323
|
+
let release: string | null = null;
|
|
324
|
+
if (releaseMode === 'untagged') {
|
|
325
|
+
status = evidence ? 'finished' : 'in-progress';
|
|
326
|
+
} else if (recorded.has(feature.slug)) {
|
|
327
|
+
// A record finishes the feature by itself: no containment is looked for.
|
|
328
|
+
status = 'finished';
|
|
329
|
+
release = recorded.get(feature.slug) as string;
|
|
330
|
+
} else {
|
|
331
|
+
// DEC-030: a merge inside a `v*` tag that has no release record also finishes the feature. A
|
|
332
|
+
// tag with a record uses only that record, and a feature excluded from it is already outside it.
|
|
333
|
+
// The evidence commit is a first-parent merge of the pinned ref, so it is in the graph read.
|
|
334
|
+
const containing = evidence && evidence.commit && unrecordedTags.size > 0
|
|
335
|
+
? tags().filter((t) => t.commit && history().isAncestor(evidence.commit as string, t.commit as string)).map((t) => t.name)
|
|
336
|
+
: [];
|
|
337
|
+
if (containing.length > 0) { status = 'finished'; release = containing[0]; }
|
|
338
|
+
else status = evidence ? 'awaiting-release' : 'in-progress';
|
|
339
|
+
}
|
|
340
|
+
if (status === 'in-progress') notDetected.push(feature.slug);
|
|
341
|
+
statuses[feature.slug] = { status, evidence: shown, release };
|
|
342
|
+
}
|
|
343
|
+
// `integrationSha` and `evidenceCommits` are for the release preview, which must use the very commit
|
|
344
|
+
// pinned here (DEC-044) and the full merge commit an evidence names, not its 7-character `ref`.
|
|
345
|
+
return { ...result(releaseMode, ref, null, statuses, notDetected, behind), integrationSha: pinned, evidenceCommits };
|
|
346
|
+
} catch (error: any) {
|
|
347
|
+
// A git failure is an environment fault, not a DoFlow defect: it is reported, never thrown.
|
|
348
|
+
// `failure` tells a caller git itself failed, which it reports (overview) or refuses on (release, merged).
|
|
349
|
+
return { ...unknown(`git could not answer (git-state-failed): ${String(error.message).split('\n')[0]}`), failure: 'git-state-failed' };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = { deriveStatuses, readGitFacts, resolveGitStateHelper, bucketize, subjectNames, behindNote, readMerges, readBranchTips, readHistory };
|