@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,432 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The follow-up service (IC-006 without `report`, IC-003 transitions): add, list, take, settle and
|
|
7
|
+
* promote, over the lifecycle event store. Every function takes the IC-001 root, writes only events
|
|
8
|
+
* (and, for promote, one new intent file), and returns a result object without printing, so the verb
|
|
9
|
+
* layer and other callers share it.
|
|
10
|
+
*
|
|
11
|
+
* - A refusal is returned `{ok:false, action, finding, message}` and never thrown (exit 1).
|
|
12
|
+
* - A caller mistake throws `FollowupUsageError` (exit 2), before anything is written.
|
|
13
|
+
* - Every free-text field is masked (line profile) before it is checked or stored; the checks run
|
|
14
|
+
* on the masked text, because that is what is written.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const nodeFs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { maskLine } = require('../mask.js');
|
|
21
|
+
const { STAGES: CHAIN_STAGES } = require('../decision-register.js');
|
|
22
|
+
const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.js');
|
|
23
|
+
const { resolveActiveFeature } = require('../feature-resolve.js');
|
|
24
|
+
const { SETTLE_AS, withDerivedDone } = require('./fold.js');
|
|
25
|
+
const { appendEvents, readFold, byFromChannel, randomChars } = require('./event-store.js');
|
|
26
|
+
const { deriveStatuses } = require('./status.js');
|
|
27
|
+
const { writeIntent, kebabTitle } = require('./intent-writer.js');
|
|
28
|
+
const reportStore = require('./report-store.js');
|
|
29
|
+
|
|
30
|
+
/** The decision register's stages plus the two lifecycle stages (IC-006). */
|
|
31
|
+
const STAGES = [...CHAIN_STAGES, 'release', 'maintain'];
|
|
32
|
+
const STATES = ['open', 'taken', 'done', 'dismissed'];
|
|
33
|
+
const SOURCE_KINDS = ['run', 'release', 'manual'];
|
|
34
|
+
const STATEMENT_MAX = 280;
|
|
35
|
+
const FU_ID = /^FU-[0-9a-hjkmnp-tv-z]{6}$/;
|
|
36
|
+
const WORD = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
37
|
+
const LINE_BREAK = /[\r\n\u2028\u2029\u0085]/;
|
|
38
|
+
/** DEC-046: C0 and C1 controls (tab allowed; line breaks are caught first) and the bidirectional overrides. */
|
|
39
|
+
const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/;
|
|
40
|
+
/** Refusal findings a fold conflict maps to; every other conflict is an `illegal-transition`. */
|
|
41
|
+
const CONFLICT_FINDINGS = new Set(['unknown-id', 'untracked-feature', 'intent-exists', 'reason-required', 'evidence-required']);
|
|
42
|
+
|
|
43
|
+
/** A caller mistake (exit 2). */
|
|
44
|
+
class FollowupUsageError extends Error {}
|
|
45
|
+
|
|
46
|
+
function refusal(action , finding , message ) { return { ok: false, action, finding, message }; }
|
|
47
|
+
|
|
48
|
+
/** Maps an `appendEvents` failure to a refusal result. */
|
|
49
|
+
function refusalFrom(action , failed ) {
|
|
50
|
+
const finding = failed.conflict && CONFLICT_FINDINGS.has(failed.conflict.code) ? failed.conflict.code : failed.finding;
|
|
51
|
+
return refusal(action, finding, failed.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Masks a free-text field and checks it is one non-empty line of at most `max` characters. */
|
|
55
|
+
function oneLine(raw , label , problems , { max = STATEMENT_MAX } = {}) {
|
|
56
|
+
// The length is checked before masking as well, so the masking patterns never see more than a bounded input.
|
|
57
|
+
const rawLength = String(raw ?? '').length;
|
|
58
|
+
if (rawLength > max * 4) { problems.push(`${label} is ${rawLength} characters; the limit is ${max}`); return null; }
|
|
59
|
+
const text = maskLine(raw).text.trim();
|
|
60
|
+
if (text === '') { problems.push(`${label} is empty`); return null; }
|
|
61
|
+
if (LINE_BREAK.test(text)) { problems.push(`${label} must be one line`); return null; }
|
|
62
|
+
if (CONTROL.test(text)) { problems.push(`${label} contains a control or bidirectional-override character`); return null; }
|
|
63
|
+
if (text.length > max) { problems.push(`${label} is ${text.length} characters; the limit is ${max}`); return null; }
|
|
64
|
+
return text;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function newFollowupId(taken ) {
|
|
68
|
+
for (;;) {
|
|
69
|
+
const id = `FU-${randomChars(6)}`;
|
|
70
|
+
if (!taken.has(id)) { taken.add(id); return id; }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function channelBy(channel ) {
|
|
75
|
+
const by = byFromChannel(channel || 'default');
|
|
76
|
+
if (!by) throw new FollowupUsageError(`unknown --channel '${channel}'. Valid: question, gate, prompt, default`);
|
|
77
|
+
return by;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseIds(raw , label = '--ids') {
|
|
81
|
+
const list = Array.isArray(raw) ? raw : String(raw ?? '').split(',');
|
|
82
|
+
const ids = [...new Set(list.flatMap((entry) => String(entry).split(',')).map((id) => id.trim()).filter(Boolean))];
|
|
83
|
+
if (ids.length === 0) throw new FollowupUsageError(`${label} is required: one or more follow-up ids, comma separated`);
|
|
84
|
+
const bad = ids.filter((id) => !FU_ID.test(id));
|
|
85
|
+
if (bad.length) throw new FollowupUsageError(`${label} names ${bad.join(', ')}, which is not a follow-up id (FU- and six characters)`);
|
|
86
|
+
return ids;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The folded store, with a taken item shown `done` while its feature derives finished (IC-003, IC-021). */
|
|
90
|
+
function loadFollowups(root , { hasBody, fsImpl = nodeFs, statuses, now = new Date() } = {}) {
|
|
91
|
+
const fold = readFold(root, { hasBody: hasBody || reportStore.bodyChecker(root, { fsImpl }), fsImpl, now });
|
|
92
|
+
const needsStatus = fold.followups.some((item ) => item.state === 'taken');
|
|
93
|
+
const derived = needsStatus ? (statuses || deriveStatuses({ root, fold })) : null;
|
|
94
|
+
const followups = needsStatus ? withDerivedDone(fold.followups, Object.fromEntries(Object.entries(derived.statuses).map(([slug, s] ) => [slug, s.status]))) : fold.followups;
|
|
95
|
+
return { fold, followups };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── add ────────────────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/** The active feature slug: `--slug`, else the branch through the existing resolver; null when neither. */
|
|
101
|
+
function resolveFeatureSlug({ cwd, slug } ) {
|
|
102
|
+
if (slug) {
|
|
103
|
+
const bad = invalidSlugRefusal(slug);
|
|
104
|
+
if (bad) throw new FollowupUsageError(bad.message);
|
|
105
|
+
return slug;
|
|
106
|
+
}
|
|
107
|
+
const found = resolveActiveFeature({ projectRoot: cwd });
|
|
108
|
+
// A missing helper is an install problem, not "no feature": it must not read as a branch that names none.
|
|
109
|
+
if (found.error === 'resolver-missing') throw new FollowupUsageError('the DoFlow helper scripts are missing from this install; reinstall DoFlow, or pass --slug, or --source run|release|manual');
|
|
110
|
+
const resolved = found.error ? null : found.paths && found.paths.feature_slug;
|
|
111
|
+
if (!resolved || !isSafeSlug(resolved)) return null;
|
|
112
|
+
// A branch names a feature only when its folder exists; otherwise the item would cite a feature that does not (IC-002).
|
|
113
|
+
let isFolder = false;
|
|
114
|
+
try { isFolder = nodeFs.statSync(found.featureDir).isDirectory(); } catch { /* absent */ }
|
|
115
|
+
if (!isFolder) {
|
|
116
|
+
throw new FollowupUsageError(`branch '${found.paths.branch}' names '${resolved}', which has no feature folder (agent-docs/doflow/${resolved}); pass --slug <existing feature>, or --source run --task-class <class> --task-id <id>`);
|
|
117
|
+
}
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sourceFor(kind , fields , label , problems ) {
|
|
122
|
+
const word = (value , name ) => {
|
|
123
|
+
if (typeof value === 'string' && WORD.test(value)) return value;
|
|
124
|
+
problems.push(`${label}: ${name} must be letters, digits, dot, underscore or dash`);
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
if (kind === 'stage') {
|
|
128
|
+
const feature = word(fields.feature, 'the feature');
|
|
129
|
+
return feature && { kind, feature, stage: fields.stage };
|
|
130
|
+
}
|
|
131
|
+
if (kind === 'run') {
|
|
132
|
+
const taskClass = word(fields.taskClass, '--task-class');
|
|
133
|
+
const taskId = word(fields.taskId, '--task-id');
|
|
134
|
+
return taskClass && taskId && { kind, taskClass, taskId, stage: fields.stage };
|
|
135
|
+
}
|
|
136
|
+
if (kind === 'release') {
|
|
137
|
+
if (fields.release === undefined || fields.release === null) { problems.push(`${label}: --release <tag> is required for --source release`); return null; }
|
|
138
|
+
const release = word(fields.release, '--release');
|
|
139
|
+
return release && { kind, release };
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'manual' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validates every item and builds its `followup.added` data; nothing is written unless all pass.
|
|
146
|
+
* @returns {Array<{statement:string, source:Object}>}
|
|
147
|
+
*/
|
|
148
|
+
function buildItems({ cwd, items, defaults } ) {
|
|
149
|
+
const problems = [];
|
|
150
|
+
let featureSlug ;
|
|
151
|
+
const feature = () => {
|
|
152
|
+
if (featureSlug === undefined) featureSlug = resolveFeatureSlug({ cwd, slug: defaults.slug });
|
|
153
|
+
return featureSlug;
|
|
154
|
+
};
|
|
155
|
+
const built = [];
|
|
156
|
+
items.forEach((raw , index ) => {
|
|
157
|
+
const label = items.length > 1 || defaults.batch ? `item ${index + 1}` : 'the follow-up';
|
|
158
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { problems.push(`${label} must be an object`); return; }
|
|
159
|
+
const statement = oneLine(raw.statement, `${label}: statement`, problems);
|
|
160
|
+
const stage = raw.stage ?? defaults.stage;
|
|
161
|
+
const given = raw.source ?? defaults.source;
|
|
162
|
+
const sourceObject = given && typeof given === 'object' ? given : null;
|
|
163
|
+
const kind = sourceObject ? sourceObject.kind : (given || null);
|
|
164
|
+
if (kind !== null && kind !== 'stage' && !SOURCE_KINDS.includes(kind)) {
|
|
165
|
+
problems.push(`${label}: source must be one of ${['stage', ...SOURCE_KINDS].join(', ')} (got '${kind}')`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const resolvedKind = kind || 'stage';
|
|
169
|
+
if ((resolvedKind === 'stage' || resolvedKind === 'run') && !STAGES.includes(stage)) {
|
|
170
|
+
problems.push(`${label}: stage must be one of ${STAGES.join(', ')}${stage ? ` (got '${stage}')` : ''}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let source;
|
|
174
|
+
if (resolvedKind === 'stage') {
|
|
175
|
+
const slug = sourceObject && sourceObject.feature ? sourceObject.feature : feature();
|
|
176
|
+
if (!slug) { problems.push(`${label}: no feature resolved from the branch; pass --slug, or --source run|release|manual`); return; }
|
|
177
|
+
source = sourceFor('stage', { feature: slug, stage }, label, problems);
|
|
178
|
+
} else {
|
|
179
|
+
source = sourceFor(resolvedKind, {
|
|
180
|
+
stage,
|
|
181
|
+
taskClass: sourceObject ? sourceObject.taskClass : defaults.taskClass,
|
|
182
|
+
taskId: sourceObject ? sourceObject.taskId : defaults.taskId,
|
|
183
|
+
release: sourceObject ? sourceObject.release : defaults.release,
|
|
184
|
+
}, label, problems);
|
|
185
|
+
}
|
|
186
|
+
if (statement && source) built.push({ statement, source });
|
|
187
|
+
});
|
|
188
|
+
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
189
|
+
return built;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reads a `--batch` file: a JSON array of `{statement, stage, source}` objects. Like `--file`, it
|
|
194
|
+
* must be a regular file of at most 16 MiB, so a device or a FIFO is refused, never read.
|
|
195
|
+
* @returns {Object[]}
|
|
196
|
+
*/
|
|
197
|
+
function readBatchFile(file , fsImpl = nodeFs) {
|
|
198
|
+
let parsed;
|
|
199
|
+
let fd;
|
|
200
|
+
try {
|
|
201
|
+
const target = path.resolve(file);
|
|
202
|
+
if (!fsImpl.statSync(target).isFile()) throw new Error('not a regular file');
|
|
203
|
+
fd = fsImpl.openSync(target, nodeFs.constants.O_RDONLY | (nodeFs.constants.O_NONBLOCK || 0));
|
|
204
|
+
const st = fsImpl.fstatSync(fd);
|
|
205
|
+
if (!st.isFile()) throw new Error('not a regular file');
|
|
206
|
+
if (st.size > reportStore.MAX_READ_BYTES) throw new Error('larger than 16 MiB');
|
|
207
|
+
parsed = JSON.parse(fsImpl.readFileSync(fd, 'utf8'));
|
|
208
|
+
} catch (error ) {
|
|
209
|
+
throw new FollowupUsageError(`cannot read --batch ${file}: ${error.message}`);
|
|
210
|
+
} finally {
|
|
211
|
+
if (fd !== undefined) { try { fsImpl.closeSync(fd); } catch { /* read already done */ } }
|
|
212
|
+
}
|
|
213
|
+
if (!Array.isArray(parsed) || parsed.length === 0) throw new FollowupUsageError(`--batch ${file} must hold a non-empty JSON array of {statement, stage, source} objects`);
|
|
214
|
+
return parsed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* IC-006 `add`. `items` are `{statement, stage?, source?}`; `defaults` holds the flag values
|
|
219
|
+
* (`stage`, `slug`, `source`, `taskClass`, `taskId`, `release`, `batch`).
|
|
220
|
+
*/
|
|
221
|
+
function addFollowups({ root, cwd = process.cwd(), items, defaults = {}, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
222
|
+
const by = channelBy(channel);
|
|
223
|
+
const built = buildItems({ cwd, items, defaults });
|
|
224
|
+
const taken = new Set(readFold(root, { fsImpl, now }).followups.map((f ) => f.id));
|
|
225
|
+
const drafts = built.map(({ statement, source }) => ({ type: 'followup.added', by, data: { id: newFollowupId(taken), statement, source } }));
|
|
226
|
+
const out = appendEvents(root, drafts, { now, fsImpl });
|
|
227
|
+
if (!out.ok) return refusalFrom('add', out);
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
action: 'add',
|
|
231
|
+
created: out.written.map(({ event } ) => ({ id: event.data.id, statement: event.data.statement, source: event.data.source, state: 'open' })),
|
|
232
|
+
events: out.written.map((w ) => w.file),
|
|
233
|
+
next: [],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── report ─────────────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/** `--release` and `--feature` of a report: letters, digits, dot, underscore or dash, as for `--source release`. */
|
|
240
|
+
function reportSource({ release, feature } ) {
|
|
241
|
+
const problems = [];
|
|
242
|
+
const source = { kind: 'report' };
|
|
243
|
+
if (release !== undefined && release !== null) {
|
|
244
|
+
if (typeof release === 'string' && WORD.test(release)) source.release = release; else problems.push('--release must be letters, digits, dot, underscore or dash');
|
|
245
|
+
}
|
|
246
|
+
if (feature !== undefined && feature !== null) {
|
|
247
|
+
const list = Array.isArray(feature) ? feature : [feature];
|
|
248
|
+
if (list.length !== 1) problems.push('--feature takes one feature slug for a report');
|
|
249
|
+
else if (invalidSlugRefusal(list[0])) problems.push(invalidSlugRefusal(list[0]).message);
|
|
250
|
+
else source.feature = list[0];
|
|
251
|
+
}
|
|
252
|
+
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
253
|
+
return source;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* IC-006 `report` (IC-005): masks the statement and the body, keeps the body on this machine, then
|
|
258
|
+
* writes one `followup.added` event with `source.kind: report` that carries only a reference, the
|
|
259
|
+
* size and a masked 2 KB excerpt. Nothing is written when any check fails; the body file is removed
|
|
260
|
+
* when its event cannot be written.
|
|
261
|
+
*
|
|
262
|
+
* With no resolvable home the body is not kept (IC-005): the event says `bodyRef: null` and the
|
|
263
|
+
* result says so in `next`.
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} options
|
|
266
|
+
* @param {string} options.root the IC-001 root
|
|
267
|
+
* @param {string} options.statement one line, at most 280 characters after masking
|
|
268
|
+
* @param {{file?: string, stdin?: boolean, text?: string}} options.input exactly one source of the body
|
|
269
|
+
* @param {string} [options.release]
|
|
270
|
+
* @param {string|string[]} [options.feature]
|
|
271
|
+
*/
|
|
272
|
+
function reportFollowup({ root, statement, input, release, feature, channel, now = new Date(), fsImpl = nodeFs, env = process.env } ) {
|
|
273
|
+
const by = channelBy(channel);
|
|
274
|
+
const source = reportSource({ release, feature });
|
|
275
|
+
const problems = [];
|
|
276
|
+
const rawStatement = String(statement ?? '');
|
|
277
|
+
const statementMasked = maskLine(rawStatement).masked;
|
|
278
|
+
const cleanStatement = oneLine(rawStatement, '--statement', problems);
|
|
279
|
+
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
280
|
+
let raw;
|
|
281
|
+
try { raw = reportStore.readReportText(input, { fsImpl }); } catch (error ) {
|
|
282
|
+
if (error instanceof reportStore.ReportInputError) throw new FollowupUsageError(error.message);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
let body;
|
|
286
|
+
try { body = reportStore.prepareBody(raw, { home: env.HOME || os.homedir() }); } catch (error ) {
|
|
287
|
+
if (error instanceof reportStore.ReportInputError) throw new FollowupUsageError(error.message);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
if (!/\S/.test(body.buffer.toString('utf8'))) throw new FollowupUsageError('the report body is empty');
|
|
291
|
+
const excerpt = reportStore.excerptOf(body.buffer);
|
|
292
|
+
// The excerpt is stored in the project event, so it gets the statement's hygiene; a line break is allowed in it.
|
|
293
|
+
// ANSI colour and title sequences were removed before masking; what is left is a real control or bidirectional character.
|
|
294
|
+
if (CONTROL.test(excerpt)) throw new FollowupUsageError('the first 2 KB of the report body contain a control or bidirectional-override character; remove it and file the report again');
|
|
295
|
+
|
|
296
|
+
const taken = new Set(readFold(root, { fsImpl, now }).followups.map((f ) => f.id));
|
|
297
|
+
let id;
|
|
298
|
+
let stored = { ok: false, reason: 'no-home' };
|
|
299
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
300
|
+
id = newFollowupId(taken);
|
|
301
|
+
stored = reportStore.writeBody(root, id, body.buffer, { env, fsImpl });
|
|
302
|
+
if (stored.reason !== 'exists') break;
|
|
303
|
+
}
|
|
304
|
+
if (!stored.ok && stored.reason !== 'no-home') {
|
|
305
|
+
return refusal('report', stored.reason === 'exists' ? 'id-collision' : 'body-not-written', `the report body could not be stored on this machine (${stored.message || 'name taken'}). Nothing was written.`);
|
|
306
|
+
}
|
|
307
|
+
const kept = stored.ok;
|
|
308
|
+
const data = { id, statement: cleanStatement, source, excerpt, bodyRef: kept ? `local:${id}` : null, bodyBytes: body.buffer.length };
|
|
309
|
+
let out;
|
|
310
|
+
try {
|
|
311
|
+
out = appendEvents(root, [{ type: 'followup.added', by, data }], { now, fsImpl });
|
|
312
|
+
} catch (error ) {
|
|
313
|
+
if (kept) reportStore.removeBody(stored.file, fsImpl);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
if (!out.ok) {
|
|
317
|
+
if (kept) reportStore.removeBody(stored.file, fsImpl);
|
|
318
|
+
return refusalFrom('report', out);
|
|
319
|
+
}
|
|
320
|
+
const next = ['Settle it now or at /do maintain; to start a fix, route it as a bug run'];
|
|
321
|
+
if (!kept) next.push('No home folder resolves (set HOME or an absolute XDG_CONFIG_HOME), so the body was not kept; only the masked excerpt is in the event');
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
action: 'report',
|
|
325
|
+
created: [{
|
|
326
|
+
id, statement: cleanStatement, source, state: 'open',
|
|
327
|
+
excerptBytes: Buffer.byteLength(excerpt, 'utf8'), bodyBytes: body.buffer.length,
|
|
328
|
+
body: kept ? 'on-this-machine' : 'not-on-this-machine', masked: statementMasked + body.masked,
|
|
329
|
+
}],
|
|
330
|
+
events: out.written.map((w ) => w.file),
|
|
331
|
+
next,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── list ───────────────────────────────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
/** IC-006 `list`: `state` is open (default), taken, done, dismissed or all. */
|
|
338
|
+
function listFollowups({ root, state = 'open', hasBody, fsImpl = nodeFs, statuses, now = new Date() } ) {
|
|
339
|
+
if (state !== 'all' && !STATES.includes(state)) throw new FollowupUsageError(`--state must be one of ${[...STATES, 'all'].join(', ')} (got '${state}')`);
|
|
340
|
+
const { fold, followups } = loadFollowups(root, { hasBody, fsImpl, statuses, now });
|
|
341
|
+
const items = state === 'all' ? followups : followups.filter((item ) => item.state === state);
|
|
342
|
+
return { ok: true, action: 'list', state, count: items.length, items, conflicts: fold.conflicts, unreadable: fold.unreadable, unreadableReasons: fold.unreadableReasons, next: [] };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── take, settle, promote ──────────────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/** IC-006 `take`: one `followup.taken` event; every id must be open and the feature tracked. */
|
|
348
|
+
function takeFollowups({ root, ids, slug, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
349
|
+
const list = parseIds(ids);
|
|
350
|
+
if (!slug) throw new FollowupUsageError('--slug is required for --action take');
|
|
351
|
+
const bad = invalidSlugRefusal(slug);
|
|
352
|
+
if (bad) throw new FollowupUsageError(bad.message);
|
|
353
|
+
const out = appendEvents(root, [{ type: 'followup.taken', by: channelBy(channel), data: { ids: list, feature: slug } }], { now, fsImpl });
|
|
354
|
+
if (!out.ok) return refusalFrom('take', out);
|
|
355
|
+
return { ok: true, action: 'take', ids: list, feature: slug, events: out.written.map((w ) => w.file), next: [] };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** IC-006 `settle`: one `followup.settled` event per id. Each `as` value's required field is in IC-003. */
|
|
359
|
+
function settleFollowups({ root, ids, as, reason, evidence, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
360
|
+
const list = parseIds(ids);
|
|
361
|
+
if (!SETTLE_AS.includes(as)) throw new FollowupUsageError(`--as must be one of ${SETTLE_AS.join(', ')}${as ? ` (got '${as}')` : ''}`);
|
|
362
|
+
const problems = [];
|
|
363
|
+
const cleanReason = reason === undefined ? null : oneLine(reason, '--reason', problems);
|
|
364
|
+
const cleanEvidence = evidence === undefined ? null : oneLine(evidence, '--evidence', problems);
|
|
365
|
+
// `kept` needs a reason only when it releases a taken item or reopens a dismissed one; the fold knows the state.
|
|
366
|
+
if (!problems.length && (as === 'dismissed' || as === 'fix') && !cleanReason) problems.push(`--reason is required for --as ${as}`);
|
|
367
|
+
if (!problems.length && as === 'done' && !cleanEvidence) problems.push('--evidence is required for --as done');
|
|
368
|
+
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
369
|
+
const by = channelBy(channel);
|
|
370
|
+
// `list` shows a taken item whose feature finished as `done`, but the fold still holds it `taken`: say what the user was shown.
|
|
371
|
+
const derivedDone = new Map(loadFollowups(root, { fsImpl, now }).followups.filter((item ) => item.derived).map((item ) => [item.id, item.takenBy]));
|
|
372
|
+
if (as !== 'kept') {
|
|
373
|
+
const shown = list.find((id) => derivedDone.has(id));
|
|
374
|
+
if (shown) {
|
|
375
|
+
return refusal('settle', 'illegal-transition', `${shown} is done because feature ${derivedDone.get(shown)} finished (derived from git and the release records, not recorded on the item); only an open item can be settled as ${as}. Nothing was written.`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const drafts = list.map((id) => ({ type: 'followup.settled', by, data: { id, as, reason: cleanReason, evidence: cleanEvidence } }));
|
|
379
|
+
const out = appendEvents(root, drafts, { now, fsImpl });
|
|
380
|
+
if (!out.ok) return refusalFrom('settle', out);
|
|
381
|
+
const reopened = as === 'kept' ? list.filter((id) => derivedDone.has(id)) : [];
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
action: 'settle',
|
|
385
|
+
ids: list,
|
|
386
|
+
as,
|
|
387
|
+
events: out.written.map((w ) => w.file),
|
|
388
|
+
next: reopened.map((id) => `${id} was shown as done because feature ${derivedDone.get(id)} finished; keeping it released it from that feature, so it is open again`),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* IC-006 `promote` (IC-024): creates a new intent that names the items, then writes one
|
|
394
|
+
* `followup.promoted` event. It never adds items to an existing intent.
|
|
395
|
+
*/
|
|
396
|
+
function promoteFollowups({ root, ids, title, channel, now = new Date(), fsImpl = nodeFs } ) {
|
|
397
|
+
const list = parseIds(ids);
|
|
398
|
+
const problems = [];
|
|
399
|
+
const cleanTitle = oneLine(title, '--title', problems, { max: 80 });
|
|
400
|
+
if (problems.length) throw new FollowupUsageError(problems.join('; '));
|
|
401
|
+
const by = channelBy(channel);
|
|
402
|
+
// Check the items before the file exists, so a refusal leaves nothing behind; the event write checks again under the lock.
|
|
403
|
+
const { followups } = loadFollowups(root, { fsImpl, now });
|
|
404
|
+
const items = [];
|
|
405
|
+
for (const id of list) {
|
|
406
|
+
const item = followups.find((f ) => f.id === id);
|
|
407
|
+
if (!item) return refusal('promote', 'unknown-id', `${id} is not a follow-up. Nothing was written.`);
|
|
408
|
+
if (item.state !== 'open') return refusal('promote', 'illegal-transition', `${id} is ${item.state}; only an open item can be promoted. Nothing was written.`);
|
|
409
|
+
if (item.intent) return refusal('promote', 'intent-exists', `${id} was already promoted to ${item.intent}. Nothing was written.`);
|
|
410
|
+
items.push(item);
|
|
411
|
+
}
|
|
412
|
+
// A title with no ASCII letter or digit still promotes, under a name built from the first item's id (DEC-046).
|
|
413
|
+
const fallbackName = kebabTitle(cleanTitle) ? null : `followups-${list[0].toLowerCase()}`;
|
|
414
|
+
const written = writeIntent(root, { title: cleanTitle, fallbackName, by, date: now.toISOString().slice(0, 10), items }, { fsImpl });
|
|
415
|
+
if (!written.ok) return refusal('promote', written.finding, written.message);
|
|
416
|
+
let committed = false;
|
|
417
|
+
try {
|
|
418
|
+
const out = appendEvents(root, [{ type: 'followup.promoted', by, data: { ids: list, intent: written.path } }], { now, fsImpl });
|
|
419
|
+
if (!out.ok) return refusalFrom('promote', out);
|
|
420
|
+
committed = true;
|
|
421
|
+
return { ok: true, action: 'promote', intent: written.path, ids: list, events: out.written.map((w ) => w.file), next: [] };
|
|
422
|
+
} finally {
|
|
423
|
+
// The file is the one this call just created exclusively, so removing it undoes a half-done promotion on every failure path.
|
|
424
|
+
if (!committed) { try { fsImpl.rmSync(written.file, { force: true }); } catch { /* the failure being reported stands */ } }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
addFollowups, reportFollowup, listFollowups, takeFollowups, settleFollowups, promoteFollowups,
|
|
430
|
+
loadFollowups, readBatchFile, resolveFeatureSlug, parseIds, oneLine, channelBy,
|
|
431
|
+
FollowupUsageError, STAGES, STATES, SOURCE_KINDS, STATEMENT_MAX,
|
|
432
|
+
};
|