@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,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `lifecycle --action release` and `--action merged` (IC-020, IC-021, DEC-021, DEC-022, DEC-030,
|
|
7
|
+
* DEC-044). A release is previewed from git and recorded, on `--confirm`, as one local
|
|
8
|
+
* `release.recorded` event; nothing is written into the release commit, nothing is staged or
|
|
9
|
+
* committed, and no ignore rule is written. Each function returns a result object without printing.
|
|
10
|
+
*
|
|
11
|
+
* The integration ref is `facts.integration_ref` from `do-git-state.sh --lifecycle`, the very value
|
|
12
|
+
* the status deriver pins, so a preview before the tag and the overview agree about one ref and this
|
|
13
|
+
* module never resolves a ref a second way.
|
|
14
|
+
*
|
|
15
|
+
* Candidate choice reuses `deriveStatuses` with the release tags minus the tag being recorded: a
|
|
16
|
+
* feature then reads `finished` only when a record names it or an OTHER unrecorded `v*` tag contains
|
|
17
|
+
* its merge (DEC-030), which is the "ignore containment in `--tag`" rule without a change to status.js.
|
|
18
|
+
* A feature is dropped for exactly three reasons (IC-020): a release record names it, another
|
|
19
|
+
* unrecorded `v*` tag contains it, or no evidence of its merge reaches X.
|
|
20
|
+
*
|
|
21
|
+
* Evidence into X is looked for in two places: the derivation against the pinned integration ref
|
|
22
|
+
* (the normal case: features merge into develop, the tag is cut from it) and, when the tag exists, a
|
|
23
|
+
* second derivation against the tag's own first-parent chain (a hotfix merged only into the
|
|
24
|
+
* production branch). Each gives the full merge commit, which must be an ancestor of X. When a branch
|
|
25
|
+
* was merged more than once, the newest merge may lie after the tag; an earlier merge of the same
|
|
26
|
+
* branch (or of a merge naming the slug) that X contains then counts. The ancestry questions about X
|
|
27
|
+
* are answered from one graph read (`readHistory`) of the pinned ref, X and the evidence branch tips,
|
|
28
|
+
* so the preview costs a fixed number of git calls however many features and merges there are.
|
|
29
|
+
* Accepted ceiling: the search for an earlier merge walks, in memory, the integration ref's
|
|
30
|
+
* first-parent merges inside X once per feature that needs it.
|
|
31
|
+
*
|
|
32
|
+
* `evidence.commit` and the pinned sha come from `deriveStatuses` (`evidenceCommits`, `integrationSha`),
|
|
33
|
+
* so this module neither re-resolves the integration ref nor an abbreviated sha.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const nodeFs = require('node:fs');
|
|
37
|
+
const { spawnSync } = require('node:child_process');
|
|
38
|
+
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
39
|
+
const { appendEvents, readFold } = require('./event-store.js');
|
|
40
|
+
const { deriveStatuses, readGitFacts, behindNote, subjectNames, readMerges, readBranchTips, readHistory } = require('./status.js');
|
|
41
|
+
const { FollowupUsageError, oneLine, channelBy } = require('./followup.js');
|
|
42
|
+
|
|
43
|
+
/** The release-tag pattern of IC-020, the one `git-state` filters by. */
|
|
44
|
+
const RELEASE_TAG = /^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/;
|
|
45
|
+
|
|
46
|
+
const refusal = (action , finding , message ) => ({ ok: false, action, finding, message });
|
|
47
|
+
|
|
48
|
+
/** Trimmed stdout of a git call, or null when git exits non-zero. */
|
|
49
|
+
function gitOut(root , args ) {
|
|
50
|
+
const run = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
51
|
+
return run.status === 0 ? run.stdout.trim() : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function commitOf(root , ref ) { return gitOut(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]) || null; }
|
|
55
|
+
|
|
56
|
+
/** `a` is an ancestor of (or equal to) `b`. */
|
|
57
|
+
function isAncestor(root , a , b ) {
|
|
58
|
+
return spawnSync('git', ['merge-base', '--is-ancestor', a, b], { cwd: root, stdio: 'ignore' }).status === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Evidence of an earlier merge of a branch that was merged more than once: a merge X contains, committed
|
|
63
|
+
* since tracking, whose subject names the slug or whose non-first parent is an ancestor of the branch tip.
|
|
64
|
+
* `merges` are the pinned ref's first-parent merges inside X, newest first; `history` holds X and the tip.
|
|
65
|
+
*/
|
|
66
|
+
function earlierMerge({ slug, evidence, tip, lowerBound, merges, bound, history } ) {
|
|
67
|
+
if (tip && history.isAncestor(tip, bound)) return { ...evidence };
|
|
68
|
+
for (const merge of merges) {
|
|
69
|
+
if (merge.ct < lowerBound) continue;
|
|
70
|
+
// The merged side is this branch's own when it is an ancestor of the tip and the merge itself is not: a
|
|
71
|
+
// branch cut from the integration ref after that merge has the merge in its history and did not make it.
|
|
72
|
+
// A parent outside the first parent's history is one this merge introduced.
|
|
73
|
+
const own = (parent ) => history.introducer(parent) === merge.sha && history.isAncestor(parent, tip);
|
|
74
|
+
if (tip && merge.parents.slice(1).some(own) && !history.isAncestor(merge.sha, tip)) return { ...evidence };
|
|
75
|
+
if (subjectNames(merge.subject, slug)) return { kind: 'merge-subject', ref: merge.sha.slice(0, 7) };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The tag a release follows (it only feeds the output): the base tag `git-state` proposes versions
|
|
82
|
+
* from, other than `tag`, when it is a release tag lying in X's history; else the newest release tag
|
|
83
|
+
* other than `tag` merged into X (git's version order). A `v*` tag that is not a release (`vnext`)
|
|
84
|
+
* never qualifies.
|
|
85
|
+
*/
|
|
86
|
+
function previousTagOf(root , { tag, facts, bound } ) {
|
|
87
|
+
const known = new Set(facts.release_tags);
|
|
88
|
+
if (facts.base_tag && facts.base_tag !== tag && known.has(facts.base_tag) && isAncestor(root, `refs/tags/${facts.base_tag}`, bound)) return facts.base_tag;
|
|
89
|
+
const merged = gitOut(root, ['tag', '--list', 'v*', '--sort=-v:refname', '--merged', bound]);
|
|
90
|
+
return (merged || '').split('\n').find((name ) => name && name !== tag && known.has(name)) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function slugList(raw , label ) {
|
|
94
|
+
const list = [...new Set((Array.isArray(raw) ? raw : raw === undefined || raw === null ? [] : [raw]).map(String))];
|
|
95
|
+
for (const slug of list) if (!isSafeSlug(slug)) throw new FollowupUsageError(`${label}: ${invalidSlugRefusal(slug).message}`);
|
|
96
|
+
return list;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* IC-020. Without `confirm` nothing is written; with it one `release.recorded` event is.
|
|
101
|
+
* @param {Object} options
|
|
102
|
+
* @param {string} options.root the IC-001 root
|
|
103
|
+
* @param {string} options.tag `vX.Y.Z[-pre]`
|
|
104
|
+
* @param {boolean} [options.confirm]
|
|
105
|
+
* @param {string[]} [options.features] `--feature` slugs to add
|
|
106
|
+
* @param {string[]} [options.exclude] `--exclude` slugs to leave out
|
|
107
|
+
* @param {string} [options.channel]
|
|
108
|
+
* @param {Object} [options.facts] `readGitFacts` output, for a caller that already has it
|
|
109
|
+
*/
|
|
110
|
+
function releaseFeatures({ root, tag, confirm = false, features, exclude, channel, now = new Date(), fsImpl = nodeFs, facts: givenFacts } ) {
|
|
111
|
+
if (typeof tag !== 'string' || !RELEASE_TAG.test(tag)) throw new FollowupUsageError(`--tag is required and must look like v1.2.3 or v1.2.3-rc.1 (got '${tag ?? ''}')`);
|
|
112
|
+
const added = slugList(features, '--feature');
|
|
113
|
+
const excluded = slugList(exclude, '--exclude');
|
|
114
|
+
const by = channelBy(channel);
|
|
115
|
+
|
|
116
|
+
const facts = givenFacts || readGitFacts(root);
|
|
117
|
+
if (facts.error) return refusal('release', 'no-integration-ref', `git facts are unavailable (${facts.error}), so no release can be previewed or recorded. Nothing was written.`);
|
|
118
|
+
if (!facts.integration_ref) return refusal('release', 'no-integration-ref', 'no integration ref resolves (develop, main, master, origin/HEAD), so no release can be previewed or recorded. Nothing was written.');
|
|
119
|
+
|
|
120
|
+
const fold = readFold(root, { fsImpl, now });
|
|
121
|
+
const trackedSlugs = new Set(fold.features.map((f ) => f.slug));
|
|
122
|
+
// An exclusion is permanent for its tag, so a mistyped slug must not be recorded any more than a mistyped addition.
|
|
123
|
+
const untracked = [...new Set([...added, ...excluded])].filter((slug) => !trackedSlugs.has(slug));
|
|
124
|
+
if (untracked.length) {
|
|
125
|
+
return refusal('release', 'untracked-feature', `${untracked.join(', ')} ${untracked.length === 1 ? 'is' : 'are'} not tracked, so ${untracked.length === 1 ? 'it' : 'they'} cannot be added to or excluded from a release; run doflow-run lifecycle --action init --slug <slug> first. Nothing was written.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// DEC-030: containment in the tag being recorded never finishes a feature before its record exists.
|
|
129
|
+
const otherTags = { ...facts, release_tags: facts.release_tags.filter((name ) => name !== tag) };
|
|
130
|
+
const derived = deriveStatuses({ root, fold, facts: otherTags });
|
|
131
|
+
if (derived.releaseMode === 'unknown' || !derived.integrationSha) return refusal('release', 'no-integration-ref', `${derived.reason || `${facts.integration_ref} does not resolve to a commit`}. Nothing was written.`);
|
|
132
|
+
|
|
133
|
+
// X: the tag's commit once it exists, else the commit the status view pinned the integration ref to (DEC-044).
|
|
134
|
+
const tagCommit = commitOf(root, `refs/tags/${tag}`);
|
|
135
|
+
const bound = tagCommit || derived.integrationSha;
|
|
136
|
+
const previousTag = previousTagOf(root, { tag, facts, bound });
|
|
137
|
+
// A hotfix merged only into the tag's own history has no evidence on the integration chain: derive against the tag as well.
|
|
138
|
+
const atTag = tagCommit && tagCommit !== derived.integrationSha ? deriveStatuses({ root, fold, facts: { ...otherTags, integration_ref: tagCommit } }) : null;
|
|
139
|
+
|
|
140
|
+
const recorded = new Set(fold.releases.flatMap((release ) => release.features.map((f ) => f.slug)));
|
|
141
|
+
const tagged = derived.releaseMode === 'tagged';
|
|
142
|
+
// While X is the pinned commit, every evidence commit is in its history. Once X is a tag elsewhere, one
|
|
143
|
+
// graph read of the pinned ref, X and the evidence branch tips answers every ancestry question about X.
|
|
144
|
+
let read = null;
|
|
145
|
+
const readOfX = () => {
|
|
146
|
+
if (read) return read;
|
|
147
|
+
const branchRefs = new Set(fold.features.map((f ) => derived.statuses[f.slug].evidence).filter((e ) => e && e.kind === 'branch').map((e ) => e.ref));
|
|
148
|
+
// Local branches are listed first, so a ref resolves as refs/heads/<ref> before refs/remotes/<ref>.
|
|
149
|
+
const tips = new Map();
|
|
150
|
+
for (const t of readBranchTips(root)) if (branchRefs.has(t.short) && !tips.has(t.short)) tips.set(t.short, t.sha);
|
|
151
|
+
const history = readHistory(root, derived.integrationSha, [bound, ...tips.values()]);
|
|
152
|
+
const merges = readMerges(root, derived.integrationSha).filter((m ) => history.isAncestor(m.sha, bound)).reverse();
|
|
153
|
+
read = { history, merges, tips };
|
|
154
|
+
return read;
|
|
155
|
+
};
|
|
156
|
+
const inX = (commit ) => bound === derived.integrationSha || readOfX().history.isAncestor(commit, bound);
|
|
157
|
+
const candidates = [];
|
|
158
|
+
const notDetected = [];
|
|
159
|
+
// A git failure while reading X's history is an environment fault: a refusal, never a throw that would
|
|
160
|
+
// reach failure capture (NFR-005), as the status derivation reports its own as `unknown`.
|
|
161
|
+
try {
|
|
162
|
+
for (const feature of fold.features) {
|
|
163
|
+
const { slug } = feature;
|
|
164
|
+
if (recorded.has(slug)) continue;
|
|
165
|
+
const entry = derived.statuses[slug];
|
|
166
|
+
const hotfix = atTag && atTag.statuses[slug];
|
|
167
|
+
// With another v* tag in play, `finished` here means an unrecorded tag already contains the merge.
|
|
168
|
+
if (tagged && (entry.status === 'finished' || (hotfix && hotfix.status === 'finished'))) continue;
|
|
169
|
+
const evidence = entry.evidence;
|
|
170
|
+
let found = null;
|
|
171
|
+
if (evidence) {
|
|
172
|
+
const commit = derived.evidenceCommits[slug];
|
|
173
|
+
if (evidence.kind === 'confirmed' || (commit && inX(commit))) found = evidence;
|
|
174
|
+
}
|
|
175
|
+
if (!found && hotfix && hotfix.evidence) found = hotfix.evidence;
|
|
176
|
+
if (!found && evidence) {
|
|
177
|
+
const { history, merges, tips } = readOfX();
|
|
178
|
+
const tip = evidence.kind === 'branch' ? tips.get(evidence.ref) || null : null;
|
|
179
|
+
const lowerBound = Math.floor(Date.parse(feature.trackedAt) / 1000);
|
|
180
|
+
found = earlierMerge({ slug, evidence, tip, lowerBound, merges, bound, history });
|
|
181
|
+
}
|
|
182
|
+
if (found) candidates.push({ slug, evidence: found.kind, ref: found.ref }); else notDetected.push(slug);
|
|
183
|
+
}
|
|
184
|
+
} catch (error ) {
|
|
185
|
+
return refusal('release', 'no-integration-ref', `git could not answer while reading the history of ${tagCommit ? tag : facts.integration_ref} (git-state-failed): ${String(error.message).split('\n')[0]}. Nothing was written.`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const shipped = [...candidates];
|
|
189
|
+
for (const slug of added) if (!shipped.some((c) => c.slug === slug)) shipped.push({ slug, evidence: 'confirmed', ref: null });
|
|
190
|
+
const finishing = shipped.filter((c) => !excluded.includes(c.slug));
|
|
191
|
+
const finishingSlugs = new Set(finishing.map((c) => c.slug));
|
|
192
|
+
const followupsDone = fold.followups.filter((item ) => item.state === 'taken' && finishingSlugs.has(item.takenBy)).map((item ) => item.id);
|
|
193
|
+
|
|
194
|
+
const base = { ok: true, action: 'release', tag, previousTag, bound: tagCommit ? tag : facts.integration_ref, integrationRef: facts.integration_ref };
|
|
195
|
+
if (derived.integrationBehind > 0) base.note = behindNote(facts.integration_ref, derived.integrationBehind);
|
|
196
|
+
|
|
197
|
+
if (!confirm) {
|
|
198
|
+
const addHint = 'add --feature <slug> for a feature listed under notDetected that shipped';
|
|
199
|
+
return {
|
|
200
|
+
...base, recorded: false, candidates, added, excluded, notDetected, followupsDone,
|
|
201
|
+
next: [tagCommit
|
|
202
|
+
? `Record it: doflow-run lifecycle --action release --tag ${tag} --confirm (${addHint}; --exclude <slug> leaves one out)`
|
|
203
|
+
: `After git tag ${tag}: doflow-run lifecycle --action release --tag ${tag} --confirm (${addHint})`],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (!tagCommit) return refusal('release', 'tag-missing', `${tag} does not exist; create the tag first, then confirm. Nothing was written.`);
|
|
207
|
+
const out = appendEvents(root, [{ type: 'release.recorded', by, data: { tag, commit: tagCommit, features: finishing, excluded } }], { now, fsImpl });
|
|
208
|
+
if (!out.ok) return refusal('release', out.finding, out.message);
|
|
209
|
+
return { ...base, recorded: true, candidates, added, excluded, notDetected, followupsDone, events: out.written.map((w ) => w.file), next: [] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* IC-021 `merged`: the user's confirmation of a merge git cannot show (squash, rebase, fast-forward,
|
|
214
|
+
* cherry-pick). It records an observed fact after the merge, one `feature.merged` event, and the
|
|
215
|
+
* status stays derived from events on read, so nothing is written ahead of the fact (DEC-022).
|
|
216
|
+
*/
|
|
217
|
+
function recordMerged({ root, slug, reason, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
218
|
+
if (!slug) throw new FollowupUsageError('--slug is required for --action merged');
|
|
219
|
+
if (!isSafeSlug(slug)) throw new FollowupUsageError(invalidSlugRefusal(slug).message);
|
|
220
|
+
const problems = [];
|
|
221
|
+
const cleanReason = reason === undefined ? null : oneLine(reason, '--reason', problems);
|
|
222
|
+
if (!problems.length && !cleanReason) problems.push('--reason is required for --action merged: how the merge happened, for example "squash-merged by hand"');
|
|
223
|
+
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
224
|
+
const by = channelBy(channel);
|
|
225
|
+
|
|
226
|
+
const fold = readFold(root, { fsImpl, now });
|
|
227
|
+
if (!fold.features.some((f ) => f.slug === slug)) {
|
|
228
|
+
return refusal('merged', 'untracked-feature', `${slug} is not a tracked feature, so no merge can be confirmed for it; run doflow-run lifecycle --action init --slug ${slug} first. Nothing was written.`);
|
|
229
|
+
}
|
|
230
|
+
const before = deriveStatuses({ root, fold }).statuses[slug];
|
|
231
|
+
if (before.status === 'unknown') return refusal('merged', 'no-integration-ref', `git cannot answer here, so a merge cannot be confirmed against an integration ref. Nothing was written.`);
|
|
232
|
+
if (before.evidence) {
|
|
233
|
+
return refusal('merged', 'already-merged', `${slug} already has merge evidence (${before.evidence.kind}${before.evidence.ref ? ` ${before.evidence.ref}` : ''}); nothing to confirm. Nothing was written.`);
|
|
234
|
+
}
|
|
235
|
+
const out = appendEvents(root, [{ type: 'feature.merged', by, data: { slug, reason: cleanReason } }], { now, fsImpl });
|
|
236
|
+
if (!out.ok) return refusal('merged', out.finding, out.message);
|
|
237
|
+
const after = deriveStatuses({ root, fold: readFold(root, { fsImpl, now }) }).statuses[slug];
|
|
238
|
+
return { ok: true, action: 'merged', slug, status: after.status, evidence: after.evidence, release: after.release, events: out.written.map((w ) => w.file), next: [] };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { releaseFeatures, recordMerged, RELEASE_TAG };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The machine-local report body (IC-005, DEC-014). A report's text can hold a crash trace, a log or
|
|
7
|
+
* pasted output, so it never enters the repository: the body is masked with the body profile (IC-012),
|
|
8
|
+
* cut to at most 1 MiB and written once under the same per-machine home the failure store uses,
|
|
9
|
+
* `<XDG_CONFIG_HOME or $HOME/.config>/doflow/reports/<projectKey>/<FU-id>.txt`, folder 0700 and file
|
|
10
|
+
* 0600. The project event carries only a reference, the size and a masked 2 KB excerpt.
|
|
11
|
+
*
|
|
12
|
+
* `projectKey` is the first 12 hex characters of the SHA-256 of the IC-001 root's real path, so two
|
|
13
|
+
* clones of one project on one machine keep separate bodies, and a body is found again from any
|
|
14
|
+
* worktree that shares the root.
|
|
15
|
+
*
|
|
16
|
+
* The cut is by bytes and backs off to a UTF-8 boundary. Nothing here touches the network.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const nodeFs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const crypto = require('node:crypto');
|
|
22
|
+
const mask = require('../mask.js');
|
|
23
|
+
|
|
24
|
+
/** A `--file` or `--stdin` source is read for at most this many bytes; the rest could not be kept anyway. */
|
|
25
|
+
const MAX_READ_BYTES = 16 * 1024 * 1024;
|
|
26
|
+
/** The most a stored body holds, marker line included. */
|
|
27
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
28
|
+
const TRUNCATED = '[truncated by doflow at 1 MiB]';
|
|
29
|
+
/** Masking sees at most this much of a long body: the limit plus a margin that masking can shrink back into it. */
|
|
30
|
+
const MASK_INPUT_BYTES = MAX_BODY_BYTES + 64 * 1024;
|
|
31
|
+
const FU_ID = /^FU-[0-9a-hjkmnp-tv-z]{6}$/;
|
|
32
|
+
/** CSI (`ESC [ ... final`) and OSC (`ESC ] ... BEL` or `ESC \`) sequences: terminal colour and title codes in a pasted log. */
|
|
33
|
+
const ANSI = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
34
|
+
/** The most of the masked body the project event carries. */
|
|
35
|
+
const EXCERPT_BYTES = 2048;
|
|
36
|
+
|
|
37
|
+
/** A caller mistake in the report input (exit 2); the verb layer maps it with the other usage errors. */
|
|
38
|
+
class ReportInputError extends Error {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The folder holding every project's report bodies, or null when no home resolves. It resolves the
|
|
42
|
+
* base the way IC-011 resolves the failure home (`XDG_CONFIG_HOME` when set, non-empty and absolute,
|
|
43
|
+
* else `$HOME/.config` when `HOME` is absolute) and sits beside it, `doflow/reports`. The rule is
|
|
44
|
+
* repeated here rather than required from the failure module, so the follow-up service still loads in
|
|
45
|
+
* a build or an install where the failure modules cannot be loaded (IC-016).
|
|
46
|
+
*/
|
|
47
|
+
function reportsHome(env = process.env) {
|
|
48
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
49
|
+
if (typeof xdg === 'string' && xdg !== '') return path.isAbsolute(xdg) ? path.join(xdg, 'doflow', 'reports') : null;
|
|
50
|
+
const home = env.HOME;
|
|
51
|
+
return typeof home === 'string' && path.isAbsolute(home) ? path.join(home, '.config', 'doflow', 'reports') : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function realProjectRoot(root , fsImpl = nodeFs) {
|
|
55
|
+
try { return fsImpl.realpathSync(root); } catch { return root; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function projectKey(root , fsImpl = nodeFs) {
|
|
59
|
+
return crypto.createHash('sha256').update(realProjectRoot(root, fsImpl)).digest('hex').slice(0, 12);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @returns {string|null} the body file of a report of this project, or null when no home resolves */
|
|
63
|
+
function bodyPath(root , id , { env = process.env, fsImpl = nodeFs } = {}) {
|
|
64
|
+
const home = reportsHome(env);
|
|
65
|
+
return home === null ? null : path.join(home, projectKey(root, fsImpl), `${id}.txt`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The `hasBody` the fold needs (IC-003): a report whose body file is on this machine. */
|
|
69
|
+
function bodyChecker(root , { env = process.env, fsImpl = nodeFs } = {}) {
|
|
70
|
+
const home = reportsHome(env);
|
|
71
|
+
const folder = home === null ? null : path.join(home, projectKey(root, fsImpl));
|
|
72
|
+
return (item ) => {
|
|
73
|
+
// The id comes from an event file, so it is checked before it becomes part of a path.
|
|
74
|
+
if (folder === null || !FU_ID.test(String(item.id)) || item.bodyRef !== `local:${item.id}`) return false;
|
|
75
|
+
try { return fsImpl.statSync(path.join(folder, `${item.id}.txt`)).isFile(); } catch { return false; }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Reads at most `max` bytes from an open descriptor. */
|
|
80
|
+
function readCapped(fsImpl , fd , max ) {
|
|
81
|
+
const chunks = [];
|
|
82
|
+
let total = 0;
|
|
83
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
84
|
+
while (total < max) {
|
|
85
|
+
const n = fsImpl.readSync(fd, chunk, 0, Math.min(chunk.length, max - total), null);
|
|
86
|
+
if (n === 0) break;
|
|
87
|
+
chunks.push(Buffer.from(chunk.subarray(0, n)));
|
|
88
|
+
total += n;
|
|
89
|
+
}
|
|
90
|
+
return Buffer.concat(chunks);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The raw report text from exactly one of `--file`, `--stdin` or `--text`. A file is read as text, at
|
|
95
|
+
* most 16 MiB (`--file -` reads stdin); a binary one (a NUL byte) is refused.
|
|
96
|
+
* @param {{file?: string, stdin?: boolean, text?: string}} source
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function readReportText({ file, stdin: stdinFlag, text } , { fsImpl = nodeFs } = {}) {
|
|
100
|
+
// `--file -` is stdin, as everywhere else a `-` stands for it.
|
|
101
|
+
const stdin = Boolean(stdinFlag) || file === '-';
|
|
102
|
+
if (file === '-') file = undefined;
|
|
103
|
+
const given = [file !== undefined, Boolean(stdin), text !== undefined].filter(Boolean).length;
|
|
104
|
+
if (given !== 1) throw new ReportInputError('give the report body with exactly one of --file <path>, --stdin or --text "<body>" (--file and --stdin also take text that starts with -)');
|
|
105
|
+
if (text !== undefined) return String(text);
|
|
106
|
+
let bytes ;
|
|
107
|
+
if (stdin) {
|
|
108
|
+
try { bytes = readCapped(fsImpl, 0, MAX_READ_BYTES); } catch (error ) { throw new ReportInputError(`cannot read the report from stdin: ${error.message}`); }
|
|
109
|
+
} else {
|
|
110
|
+
const target = path.resolve(file );
|
|
111
|
+
let fd ;
|
|
112
|
+
try {
|
|
113
|
+
if (!fsImpl.statSync(target).isFile()) throw new ReportInputError(`--file ${file} is not a regular file`);
|
|
114
|
+
fd = fsImpl.openSync(target, 'r');
|
|
115
|
+
bytes = readCapped(fsImpl, fd , MAX_READ_BYTES);
|
|
116
|
+
} catch (error ) {
|
|
117
|
+
if (error instanceof ReportInputError) throw error;
|
|
118
|
+
throw new ReportInputError(`cannot read --file ${file}: ${error.code || error.message}`);
|
|
119
|
+
} finally {
|
|
120
|
+
if (fd !== undefined) { try { fsImpl.closeSync(fd); } catch { /* read already done */ } }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (bytes.includes(0)) throw new ReportInputError('the report body is binary (it holds a NUL byte); give a text file or paste the text');
|
|
124
|
+
return bytes.toString('utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The length of the longest prefix of `buffer` of at most `limit` bytes that ends on a UTF-8 boundary. */
|
|
128
|
+
function boundary(buffer , limit ) {
|
|
129
|
+
let end = Math.min(limit, buffer.length);
|
|
130
|
+
if (end === buffer.length) return end;
|
|
131
|
+
while (end > 0 && (buffer[end] & 0xc0) === 0x80) end -= 1;
|
|
132
|
+
return end;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The text masking will see: ANSI sequences removed, then cut to the limit plus a margin so one very
|
|
137
|
+
* long input cannot make a mask rule recurse too deep. The cut is at the last line end in range, else the
|
|
138
|
+
* last whitespace, else nothing is kept (an unbroken run has no clean boundary), always on a UTF-8
|
|
139
|
+
* boundary; a token straddling the cut goes with the incomplete tail, never half-masked.
|
|
140
|
+
* @returns {{text: string, cut: boolean}}
|
|
141
|
+
*/
|
|
142
|
+
function boundMaskInput(raw ) {
|
|
143
|
+
const stripped = raw.replace(ANSI, '');
|
|
144
|
+
const whole = Buffer.from(stripped, 'utf8');
|
|
145
|
+
if (whole.length <= MASK_INPUT_BYTES) return { text: stripped, cut: false };
|
|
146
|
+
const head = whole.subarray(0, boundary(whole, MASK_INPUT_BYTES));
|
|
147
|
+
let end = head.lastIndexOf(0x0a);
|
|
148
|
+
if (end < 0) for (let i = head.length - 1; i >= 0; i -= 1) if ([0x20, 0x09, 0x0d, 0x0b, 0x0c].includes(head[i])) { end = i; break; }
|
|
149
|
+
return { text: end < 0 ? '' : head.subarray(0, end).toString('utf8'), cut: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Masks a report body with the body profile and keeps at most 1 MiB of it. A longer body is cut at a
|
|
154
|
+
* UTF-8 boundary and ends with the marker line, so the stored text is never over the limit. A body
|
|
155
|
+
* the mask cannot process is refused as a caller mistake, with one line and no echo of the input.
|
|
156
|
+
* @param {string} raw
|
|
157
|
+
* @param {{home?: string}} [options] the home directory the mask replaces with `~`
|
|
158
|
+
* @returns {{buffer: Buffer, masked: number, truncated: boolean}}
|
|
159
|
+
*/
|
|
160
|
+
function prepareBody(raw , { home } = {}) {
|
|
161
|
+
const { text: bounded, cut } = boundMaskInput(raw);
|
|
162
|
+
let result ;
|
|
163
|
+
try {
|
|
164
|
+
result = mask.maskBody(bounded, home === undefined ? undefined : { home });
|
|
165
|
+
} catch {
|
|
166
|
+
throw new ReportInputError('the report body could not be masked safely (its text is too irregular to process); cut it down or file the relevant part');
|
|
167
|
+
}
|
|
168
|
+
const whole = Buffer.from(result.text, 'utf8');
|
|
169
|
+
if (whole.length <= MAX_BODY_BYTES && !cut) return { buffer: whole, masked: result.masked, truncated: false };
|
|
170
|
+
const marker = Buffer.from(`\n${TRUNCATED}`, 'utf8');
|
|
171
|
+
const kept = whole.subarray(0, boundary(whole, MAX_BODY_BYTES - marker.length));
|
|
172
|
+
return { buffer: Buffer.concat([kept, marker]), masked: result.masked, truncated: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The first 2048 bytes of a stored body, cut at a UTF-8 boundary and, when a line end lies inside
|
|
177
|
+
* the limit, at that line end (DEC-014).
|
|
178
|
+
* @param {Buffer} body
|
|
179
|
+
* @returns {string}
|
|
180
|
+
*/
|
|
181
|
+
function excerptOf(body ) {
|
|
182
|
+
if (body.length <= EXCERPT_BYTES) return body.toString('utf8').replace(/\s+$/, '');
|
|
183
|
+
const head = body.subarray(0, boundary(body, EXCERPT_BYTES));
|
|
184
|
+
const lineEnd = head.lastIndexOf(0x0a);
|
|
185
|
+
return (lineEnd > 0 ? head.subarray(0, lineEnd) : head).toString('utf8').replace(/\s+$/, '');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Writes the body once: exclusive create, file 0600, folders 0700, never inside the repository.
|
|
190
|
+
* @returns {{ok: true, file: string}|{ok: false, reason: 'no-home'|'exists'|'error', message?: string}}
|
|
191
|
+
*/
|
|
192
|
+
function writeBody(root , id , buffer , { env = process.env, fsImpl = nodeFs } = {}) {
|
|
193
|
+
const file = bodyPath(root, id, { env, fsImpl });
|
|
194
|
+
if (file === null) return { ok: false, reason: 'no-home' };
|
|
195
|
+
const folder = path.dirname(file);
|
|
196
|
+
try {
|
|
197
|
+
fsImpl.mkdirSync(folder, { recursive: true, mode: 0o700 });
|
|
198
|
+
// `mkdir -p` applies the mode under the umask; the two folders that are ours are set outright.
|
|
199
|
+
fsImpl.chmodSync(folder, 0o700);
|
|
200
|
+
fsImpl.chmodSync(path.dirname(folder), 0o700);
|
|
201
|
+
try {
|
|
202
|
+
fsImpl.writeFileSync(path.join(folder, '.root'), `${realProjectRoot(root, fsImpl)}\n`, { flag: 'wx', mode: 0o600 });
|
|
203
|
+
} catch (error ) {
|
|
204
|
+
if (error.code !== 'EEXIST') throw error;
|
|
205
|
+
}
|
|
206
|
+
fsImpl.writeFileSync(file, buffer, { flag: 'wx', mode: 0o600 });
|
|
207
|
+
return { ok: true, file };
|
|
208
|
+
} catch (error ) {
|
|
209
|
+
return error.code === 'EEXIST' ? { ok: false, reason: 'exists' } : { ok: false, reason: 'error', message: error.code || error.message };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Removes a body this process just wrote, when the event that names it could not be written. */
|
|
214
|
+
function removeBody(file , fsImpl = nodeFs) {
|
|
215
|
+
try { fsImpl.rmSync(file, { force: true }); } catch { /* the failure being reported stands */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
readReportText, prepareBody, excerptOf, writeBody, removeBody, bodyChecker, bodyPath, reportsHome, projectKey,
|
|
220
|
+
ReportInputError, MAX_READ_BYTES, MAX_BODY_BYTES, MASK_INPUT_BYTES, EXCERPT_BYTES, TRUNCATED,
|
|
221
|
+
};
|