@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/bin/doflow.js +44 -0
- package/core/.antigravity-plugin/hooks.json +39 -0
- package/core/.antigravity-plugin/mcp_config.json +18 -0
- package/core/.antigravity-plugin/plugin.json +31 -0
- package/core/.claude-plugin/marketplace.json +20 -0
- package/core/.claude-plugin/plugin.json +21 -0
- package/core/.codex-plugin/marketplace.json +20 -0
- package/core/.codex-plugin/plugin.json +35 -0
- package/core/.plugin/plugin.json +19 -0
- package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
- package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
- package/core/harnesses/claude/hooks/lib.sh +304 -0
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
- package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
- package/core/harnesses/claude/hooks/post-compact.sh +74 -0
- package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
- package/core/harnesses/claude/hooks/session-end.sh +89 -0
- package/core/harnesses/claude/hooks/session-start.sh +5 -0
- package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -0
- package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
- package/core/harnesses/claude/settings/keybindings.json +20 -0
- package/core/harnesses/claude/settings/settings.json +201 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
- package/core/harnesses/codex/agents/core-implementer.toml +7 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
- package/core/harnesses/codex/agents/research-writer.toml +8 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
- package/core/harnesses/codex/agents/system-architect.toml +9 -0
- package/core/harnesses/codex/config/config.toml +10 -0
- package/core/harnesses/codex/hooks/hooks.json +122 -0
- package/core/harnesses/codex/hooks/lib.sh +335 -0
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
- package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
- package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
- package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
- package/core/harnesses/codex/hooks/session-end.sh +5 -0
- package/core/harnesses/codex/hooks/session-start.sh +5 -0
- package/core/harnesses/codex/hooks/stop-check.sh +8 -0
- package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
- package/core/harnesses/gemini/hooks/hooks.json +77 -0
- package/core/harnesses/gemini/hooks/lib.sh +302 -0
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
- package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
- package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
- package/core/harnesses/gemini/hooks/session-end.sh +5 -0
- package/core/harnesses/gemini/hooks/session-start.sh +5 -0
- package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
- package/core/harnesses/kiro/agents/doflow.json +58 -0
- package/core/harnesses/kiro/hooks/doflow.json +53 -0
- package/core/harnesses/kiro/hooks/lib.sh +324 -0
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
- package/core/harnesses/kiro/hooks/session-start.sh +9 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
- package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
- package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
- package/core/harnesses/shared/hooks/hooks.json +47 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
- package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
- package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
- package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
- package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
- package/core/harnesses/shared/locator/doflow-run +87 -0
- package/core/registry/advice.json +101 -0
- package/core/registry/assets.json +792 -0
- package/core/registry/capabilities.json +120 -0
- package/core/registry/contracts.json +189 -0
- package/core/registry/external-tools.json +191 -0
- package/core/registry/harnesses.json +2062 -0
- package/core/registry/lifecycle.json +231 -0
- package/core/registry/mcp.json +18 -0
- package/core/registry/models.json +79 -0
- package/core/registry/readiness-templates.json +118 -0
- package/core/registry/routes.json +47 -0
- package/core/registry/storage.json +1057 -0
- package/core/registry/verification.json +258 -0
- package/core/registry/workflows.json +790 -0
- package/core/shared/agent-specs/core-implementer.md +73 -0
- package/core/shared/agent-specs/quality-guardian.md +28 -0
- package/core/shared/agent-specs/research-writer.md +31 -0
- package/core/shared/agent-specs/spec-analyst.md +33 -0
- package/core/shared/agent-specs/system-architect.md +29 -0
- package/core/shared/guidance/DOFLOW_CORE.md +8 -0
- package/core/shared/guidance/FLAGS.md +25 -0
- package/core/shared/guidance/PRINCIPLES.md +61 -0
- package/core/shared/guidance/VERSION +1 -0
- package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
- package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
- package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
- package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
- package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
- package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
- package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
- package/core/shared/guidance/pointers/claude-gemini.md +2 -0
- package/core/shared/guidance/pointers/codex.md +6 -0
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
- package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
- package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
- package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
- package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
- package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
- package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
- package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
- package/core/shared/scripts/doflow/bin/doflow-run +592 -0
- package/core/shared/skills/do/SKILL.md +128 -0
- package/core/shared/skills/do/references/estimation.md +13 -0
- package/core/shared/skills/do/references/pm_routing.md +25 -0
- package/core/shared/skills/do/references/tool_matrix.md +52 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
- package/core/shared/skills/do-code-review/README.md +49 -0
- package/core/shared/skills/do-code-review/SKILL.md +333 -0
- package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
- package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
- package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
- package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
- package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
- package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
- package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
- package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
- package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
- package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
- package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
- package/core/shared/skills/do-code-review/content-types/config.md +83 -0
- package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
- package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
- package/core/shared/skills/do-code-review/languages/c.md +103 -0
- package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
- package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
- package/core/shared/skills/do-code-review/languages/dart.md +108 -0
- package/core/shared/skills/do-code-review/languages/go.md +92 -0
- package/core/shared/skills/do-code-review/languages/java.md +103 -0
- package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
- package/core/shared/skills/do-code-review/languages/php.md +109 -0
- package/core/shared/skills/do-code-review/languages/python.md +94 -0
- package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
- package/core/shared/skills/do-code-review/languages/rust.md +99 -0
- package/core/shared/skills/do-code-review/languages/shell.md +90 -0
- package/core/shared/skills/do-code-review/languages/swift.md +88 -0
- package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
- package/core/shared/skills/do-code-review/review-policy.json +47 -0
- package/core/shared/skills/do-code-review/rules/universal.md +49 -0
- package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
- package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
- package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
- package/core/shared/skills/do-constitution/SKILL.md +72 -0
- package/core/shared/skills/do-design/SKILL.md +212 -0
- package/core/shared/skills/do-diagnose/SKILL.md +156 -0
- package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
- package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
- package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
- package/core/shared/skills/do-document/SKILL.md +81 -0
- package/core/shared/skills/do-document/references/api-reference.md +42 -0
- package/core/shared/skills/do-document/references/deep_research.md +18 -0
- package/core/shared/skills/do-document/references/embedded-research.md +11 -0
- package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
- package/core/shared/skills/do-document/references/index_generator.md +9 -0
- package/core/shared/skills/do-document/references/user-guide.md +22 -0
- package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
- package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
- package/core/shared/skills/do-flow/SKILL.md +186 -0
- package/core/shared/skills/do-flow/references/task_classes.md +134 -0
- package/core/shared/skills/do-git/SKILL.md +115 -0
- package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
- package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
- package/core/shared/skills/do-git/references/release-ritual.md +64 -0
- package/core/shared/skills/do-implement/SKILL.md +222 -0
- package/core/shared/skills/do-plan/SKILL.md +219 -0
- package/core/shared/skills/do-test/SKILL.md +113 -0
- package/core/shared/templates/doflow/constitution-template.md +28 -0
- package/core/shared/templates/doflow/data-model-template.md +169 -0
- package/core/shared/templates/doflow/design-template.md +193 -0
- package/core/shared/templates/doflow/external-contract-template.md +35 -0
- package/core/shared/templates/doflow/intent-template.md +45 -0
- package/core/shared/templates/doflow/plan-template.md +151 -0
- package/core/shared/templates/doflow/question-log-template.md +51 -0
- package/core/shared/templates/doflow/requirement-template.md +126 -0
- package/core/shared/templates/doflow/specs-template.md +55 -0
- package/core/shared/templates/doflow/state-template.md +59 -0
- package/core/shared/workflows/do-flow-chain.md +17 -0
- package/package.json +46 -4
- package/src/adapters/antigravity/index.ts +631 -0
- package/src/adapters/claude/index.ts +548 -0
- package/src/adapters/claude/mcp.ts +94 -0
- package/src/adapters/codex/agents.ts +208 -0
- package/src/adapters/codex/config.ts +272 -0
- package/src/adapters/codex/hooks.ts +203 -0
- package/src/adapters/codex/index.ts +707 -0
- package/src/adapters/codex/mcp.ts +271 -0
- package/src/adapters/codex/native-projection.ts +34 -0
- package/src/adapters/copilot/index.ts +413 -0
- package/src/adapters/copy-tree.ts +620 -0
- package/src/adapters/gemini/hooks.ts +210 -0
- package/src/adapters/gemini/index.ts +390 -0
- package/src/adapters/hook-commands.ts +60 -0
- package/src/adapters/index.ts +106 -0
- package/src/adapters/kiro/index.ts +310 -0
- package/src/adapters/mcp-entries.ts +331 -0
- package/src/adapters/opencode/index.ts +466 -0
- package/src/adapters/pi/index.ts +279 -0
- package/src/adapters/pi/json-members.ts +185 -0
- package/src/adapters/pi/mcp.ts +373 -0
- package/src/adapters/resolve-source.ts +19 -0
- package/src/cli/commands/install.ts +218 -0
- package/src/cli/commands/list-backups.ts +21 -0
- package/src/cli/commands/reconcile.ts +120 -0
- package/src/cli/commands/remove.ts +82 -0
- package/src/cli/commands/rollback.ts +172 -0
- package/src/cli/commands/self-update.ts +33 -0
- package/src/cli/commands/status.ts +138 -0
- package/src/cli/commands/tools.ts +92 -0
- package/src/cli/commands/update.ts +162 -0
- package/src/cli/index.ts +568 -0
- package/src/cli/runtime-commands.ts +281 -0
- package/src/cli/shared.ts +218 -0
- package/src/helper/bash-helper.ts +34 -0
- package/src/helper/git.ts +24 -0
- package/src/helper/guardrails.ts +49 -0
- package/src/helper/harness-paths.ts +140 -0
- package/src/helper/marker-merge.ts +116 -0
- package/src/helper/prompt.ts +61 -0
- package/src/helper/repo-root.ts +29 -0
- package/src/helper/settings-merge.ts +151 -0
- package/src/helper/settings-scope.ts +33 -0
- package/src/helper/toml.ts +244 -0
- package/src/install/backup-set.ts +149 -0
- package/src/install/backup.ts +724 -0
- package/src/install/context.ts +58 -0
- package/src/install/manifest.ts +145 -0
- package/src/install/mcp.ts +254 -0
- package/src/install/paths.ts +84 -0
- package/src/install/targets.ts +90 -0
- package/src/install/tool-lifecycle.ts +151 -0
- package/src/lifecycle/bash-availability.ts +25 -0
- package/src/lifecycle/index.ts +812 -0
- package/src/lifecycle/mcp-index.ts +28 -0
- package/src/lifecycle/policies.ts +73 -0
- package/src/lifecycle/view.ts +175 -0
- package/src/registry/index.ts +611 -0
- package/src/release/sync-plugin-versions.ts +66 -0
- package/src/runtime/advice/cli.ts +283 -0
- package/src/runtime/advice/client.ts +324 -0
- package/src/runtime/advice/config.ts +189 -0
- package/src/runtime/advice/doctor.ts +169 -0
- package/src/runtime/advice/endpoint-guard.ts +134 -0
- package/src/runtime/advice/kinds.ts +154 -0
- package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
- package/src/runtime/advice/record.ts +166 -0
- package/src/runtime/advice/registry.ts +170 -0
- package/src/runtime/advice/strength.ts +66 -0
- package/src/runtime/brief-tools.ts +342 -0
- package/src/runtime/c4-project.ts +371 -0
- package/src/runtime/capability-router.ts +484 -0
- package/src/runtime/checkouts.ts +170 -0
- package/src/runtime/claims.ts +468 -0
- package/src/runtime/cli-result.ts +55 -0
- package/src/runtime/cli.ts +678 -0
- package/src/runtime/command-detect.ts +516 -0
- package/src/runtime/context-pack.ts +492 -0
- package/src/runtime/decision-register.ts +493 -0
- package/src/runtime/evidence-ledger.ts +338 -0
- package/src/runtime/failure/capture.ts +272 -0
- package/src/runtime/failure/classifier.ts +68 -0
- package/src/runtime/failure/cli.ts +178 -0
- package/src/runtime/failure/home.ts +64 -0
- package/src/runtime/failure/store.ts +226 -0
- package/src/runtime/feature-resolve.ts +79 -0
- package/src/runtime/freshness.ts +177 -0
- package/src/runtime/health.ts +863 -0
- package/src/runtime/history-compactor.ts +237 -0
- package/src/runtime/implementation-gate.ts +321 -0
- package/src/runtime/inventory/coverage.ts +218 -0
- package/src/runtime/inventory/identity.ts +65 -0
- package/src/runtime/inventory/index.ts +705 -0
- package/src/runtime/inventory/precedence.ts +134 -0
- package/src/runtime/inventory/read-scopes.ts +131 -0
- package/src/runtime/inventory/siblings.ts +192 -0
- package/src/runtime/knowledge/chunker.ts +124 -0
- package/src/runtime/knowledge/hybrid.ts +149 -0
- package/src/runtime/knowledge/index-store.ts +145 -0
- package/src/runtime/knowledge/retrieval.ts +194 -0
- package/src/runtime/leak-scan.ts +185 -0
- package/src/runtime/lifecycle/cli.ts +276 -0
- package/src/runtime/lifecycle/event-store.ts +340 -0
- package/src/runtime/lifecycle/fold.ts +353 -0
- package/src/runtime/lifecycle/followup.ts +432 -0
- package/src/runtime/lifecycle/goal.ts +249 -0
- package/src/runtime/lifecycle/intent-writer.ts +86 -0
- package/src/runtime/lifecycle/overview.ts +292 -0
- package/src/runtime/lifecycle/release.ts +241 -0
- package/src/runtime/lifecycle/report-store.ts +221 -0
- package/src/runtime/lifecycle/retention.ts +212 -0
- package/src/runtime/lifecycle/root.ts +71 -0
- package/src/runtime/lifecycle/status.ts +353 -0
- package/src/runtime/lifecycle/store-upkeep.ts +393 -0
- package/src/runtime/locator-resolve.ts +158 -0
- package/src/runtime/mask.ts +210 -0
- package/src/runtime/mcp-consultations.ts +412 -0
- package/src/runtime/mcp-reach.ts +157 -0
- package/src/runtime/model-router.ts +153 -0
- package/src/runtime/outcome.ts +467 -0
- package/src/runtime/reach.ts +148 -0
- package/src/runtime/readiness-record.ts +150 -0
- package/src/runtime/readiness.ts +435 -0
- package/src/runtime/recovery.ts +282 -0
- package/src/runtime/research-request.ts +231 -0
- package/src/runtime/retrieval-plan.ts +578 -0
- package/src/runtime/scaffold/artifacts.ts +217 -0
- package/src/runtime/scaffold/fingerprint.ts +56 -0
- package/src/runtime/scaffold/generate.ts +911 -0
- package/src/runtime/scaffold/languages.ts +245 -0
- package/src/runtime/storage/cli.ts +64 -0
- package/src/runtime/storage/engine.ts +249 -0
- package/src/runtime/storage/registry.ts +130 -0
- package/src/runtime/storage/roots.ts +63 -0
- package/src/runtime/storage/rules.ts +282 -0
- package/src/runtime/storage/upkeep.ts +243 -0
- package/src/runtime/storage/work-status.ts +326 -0
- package/src/runtime/task-attempts.ts +415 -0
- package/src/runtime/task-classifier.ts +550 -0
- package/src/runtime/task-scope.ts +130 -0
- package/src/runtime/task-state.ts +176 -0
- package/src/runtime/trace/indicators.ts +235 -0
- package/src/runtime/trace/ledger.ts +626 -0
- package/src/runtime/trace/render.ts +76 -0
- package/src/runtime/trace/views.ts +459 -0
- package/src/runtime/verification/contract-runner.ts +341 -0
- package/src/runtime/verification/engine.ts +1461 -0
- package/src/runtime/verification/registry.ts +130 -0
- package/src/runtime/verification/scope-bound.ts +418 -0
- package/src/runtime/verification/test-selection.ts +172 -0
- package/src/runtime/workflow-engine.ts +588 -0
- package/src/runtime/workflow-orchestrator.ts +953 -0
- package/src/runtime/worktree.ts +340 -0
- package/src/state/index.ts +193 -0
- package/src/state/lockfile.ts +136 -0
- package/src/state/run-lock.ts +408 -0
- package/types/node-commonjs.d.ts +68 -0
- package/types/runtime.d.ts +91 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `followup`, `lifecycle` and `goal` verb handlers (IC-006 to IC-009, IC-022). All work on the
|
|
7
|
+
* project store at the IC-001 root, so `-g` is refused with exit 2. `--json` prints the result
|
|
8
|
+
* object unmodified; exit 0 = answered or written, 1 = a refusal, 2 = usage. A refusal is returned
|
|
9
|
+
* by the service and printed here, never thrown, so it cannot reach failure capture (NFR-005).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { finishRuntime, usageError } = require('../cli-result.ts');
|
|
13
|
+
const { projectRoot } = require('./root.ts');
|
|
14
|
+
const followup = require('./followup.ts');
|
|
15
|
+
const { StoreUnsafeError, StoreLockedError } = require('./event-store.ts');
|
|
16
|
+
const { prepareStore } = require('./store-upkeep.ts');
|
|
17
|
+
const { printSafe } = require('../mask.ts');
|
|
18
|
+
const { buildOverview, initFeature, featureStatus } = require('./overview.ts');
|
|
19
|
+
const { releaseFeatures, recordMerged } = require('./release.ts');
|
|
20
|
+
const goal = require('./goal.ts');
|
|
21
|
+
|
|
22
|
+
const FOLLOWUP_ACTIONS = ['add', 'list', 'take', 'settle', 'promote', 'report'];
|
|
23
|
+
const LIFECYCLE_ACTIONS = ['overview', 'init', 'release', 'status', 'merged'];
|
|
24
|
+
const GOAL_ACTIONS = ['add', 'item', 'check', 'link', 'list', 'done'];
|
|
25
|
+
/** The flags each goal action takes, besides --goal, --channel and --json; any other one given is a usage error. */
|
|
26
|
+
const GOAL_FLAGS: Record<string, any> = {
|
|
27
|
+
add: ['statement', 'item'], item: ['text'], check: ['item', 'evidence', 'unmet'],
|
|
28
|
+
link: ['slug', 'replace'], list: [], done: ['reason'],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function sourceText(source: any) {
|
|
32
|
+
return [source.kind, source.feature, source.taskClass, source.taskId, source.release, source.stage, source.ref].filter(Boolean).join(' ');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function followupLines(result: any) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
switch (result.action) {
|
|
38
|
+
case 'add': for (const c of result.created) lines.push(`added ${c.id} (${sourceText(c.source)}): ${c.statement}`); break;
|
|
39
|
+
case 'list':
|
|
40
|
+
if (result.items.length === 0) lines.push(`No ${result.state === 'all' ? '' : `${result.state} `}follow-ups.`);
|
|
41
|
+
for (const i of result.items) lines.push(`${i.id} ${i.state}${i.takenBy ? ` by ${i.takenBy}` : ''}${i.intent ? ` -> ${i.intent}` : ''} (${sourceText(i.source)}, ${i.added}) ${i.statement}`);
|
|
42
|
+
break;
|
|
43
|
+
case 'take': lines.push(`${result.feature} took ${result.ids.join(', ')}`); break;
|
|
44
|
+
case 'settle': lines.push(`settled ${result.ids.join(', ')} as ${result.as}`); break;
|
|
45
|
+
case 'promote': lines.push(`created ${result.intent} from ${result.ids.join(', ')}`); break;
|
|
46
|
+
case 'report':
|
|
47
|
+
for (const c of result.created) lines.push(`reported ${c.id} (${sourceText(c.source)}): ${c.statement}`, ` body ${c.body} (${c.bodyBytes} bytes), excerpt ${c.excerptBytes} bytes, ${c.masked} value${c.masked === 1 ? '' : 's'} masked`);
|
|
48
|
+
for (const line of result.next) lines.push(`next: ${line}`);
|
|
49
|
+
break;
|
|
50
|
+
default: break;
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function overviewLines(r: any) {
|
|
56
|
+
const lines = [`${r.mode} overview: ${r.followups.open} open follow-up${r.followups.open === 1 ? '' : 's'}, ${r.followups.shown} shown (release mode ${r.releaseMode}${r.integrationRef ? `, integration ref ${r.integrationRef}` : ''})`];
|
|
57
|
+
for (const i of r.followups.items) lines.push(` ${i.id} (${sourceText(i.source)}, ${i.added})${i.promoted ? ` promoted to ${i.intent}` : ''}${i.pending === false ? ' settled this run' : ''} ${i.statement}`);
|
|
58
|
+
if (Array.isArray(r.failures)) {
|
|
59
|
+
lines.push(`failures: ${r.failures.length} new or regressed`);
|
|
60
|
+
for (const f of r.failures) lines.push(` ${f.fp} ${f.status} x${f.count} ${f.command} ${f.kind}${f.message ? `: ${f.message}` : ''} (last seen ${f.lastSeen}, ${f.lastVersion})`);
|
|
61
|
+
}
|
|
62
|
+
for (const g of r.goals) lines.push(`goal ${g.goal}: ${g.items.met}/${g.items.total} items met${g.proposeDone ? ' (propose done)' : ''}`, ...g.nudges.map((n: any) => ` ${n}`), ...(g.conflicts || []).map((c: any) => ` conflict: ${c.reason}${c.text ? ` (dropped text: ${JSON.stringify(c.text)})` : ''}`));
|
|
63
|
+
const f = r.features;
|
|
64
|
+
// The finished and awaiting-release lists only grow, so the text shows their bounded form.
|
|
65
|
+
const bounded = (key: any) => (r.featuresShown && r.featuresShown[key]) || { count: f[key].length, shown: f[key], more: 0 };
|
|
66
|
+
const names = [['finished', bounded('finished')], ['awaiting release', bounded('awaitingRelease')],
|
|
67
|
+
['in progress', { count: f.inProgress.length, shown: f.inProgress, more: 0 }], ['unknown', { count: f.unknown.length, shown: f.unknown, more: 0 }]].filter(([, b]) => b.count);
|
|
68
|
+
for (const [label, b] of names) {
|
|
69
|
+
lines.push(`${label}: ${b.shown.join(', ')}${b.more ? ` (+${b.more} more; --json lists them all, lifecycle --action status --slug <slug> shows one)` : ''}`);
|
|
70
|
+
}
|
|
71
|
+
if (r.gitError) lines.push(`git unavailable (${r.gitError}): feature statuses are unknown`);
|
|
72
|
+
if (r.mode === 'maintain') lines.push(`pending: ${r.pending}`);
|
|
73
|
+
if (r.note) lines.push(`note: ${r.note}`);
|
|
74
|
+
if (r.conflicts.length) lines.push(`${r.conflicts.length} event conflict${r.conflicts.length === 1 ? '' : 's'} (see --json)`);
|
|
75
|
+
if (r.unreadable.length) lines.push(`unreadable event files: ${r.unreadable.map((n: any) => (r.unreadableReasons && r.unreadableReasons[n] ? `${n} (${r.unreadableReasons[n]})` : n)).join(', ')}`);
|
|
76
|
+
for (const line of r.next) lines.push(`next: ${line}`);
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function releaseLines(r: any) {
|
|
81
|
+
const lines = [`${r.recorded ? 'recorded' : 'preview of'} release ${r.tag} (previous ${r.previousTag || 'none'}, bound ${r.bound})`];
|
|
82
|
+
for (const c of r.candidates) if (!r.excluded.includes(c.slug)) lines.push(` ships ${c.slug} (${c.evidence}${c.ref ? ` ${c.ref}` : ''})`);
|
|
83
|
+
if (r.added.length) lines.push(`added: ${r.added.join(', ')}`);
|
|
84
|
+
if (r.excluded.length) lines.push(`excluded: ${r.excluded.join(', ')}`);
|
|
85
|
+
if (r.notDetected.length) lines.push(`not detected: ${r.notDetected.join(', ')}`);
|
|
86
|
+
if (r.followupsDone.length) lines.push(`follow-ups done: ${r.followupsDone.join(', ')}`);
|
|
87
|
+
if (r.note) lines.push(`note: ${r.note}`);
|
|
88
|
+
for (const line of r.next) lines.push(`next: ${line}`);
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function lifecycleLines(result: any) {
|
|
93
|
+
switch (result.action) {
|
|
94
|
+
case 'init': return [`${result.slug}: tracked ${result.tracked}${result.taken.length ? `, took ${result.taken.join(', ')}` : ''}${result.goal ? `, serves ${result.goal}` : ''}`, ...result.next];
|
|
95
|
+
case 'status': return [`${result.slug}: ${result.status}${result.evidence ? ` (${result.evidence.kind}${result.evidence.ref ? ` ${result.evidence.ref}` : ''})` : ''}${result.release ? `, release ${result.release}` : ''}`, ...(result.note ? [`note: ${result.note}`] : [])];
|
|
96
|
+
case 'release': return releaseLines(result);
|
|
97
|
+
case 'merged': return [`${result.slug}: merge confirmed, now ${result.status}`];
|
|
98
|
+
default: return result.mode ? overviewLines(result) : [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function goalLines(r: any) {
|
|
103
|
+
const met = (g: any) => `${g.progress.met}/${g.progress.total} items met`;
|
|
104
|
+
switch (r.action) {
|
|
105
|
+
case 'add': return [`added goal ${r.goal}: ${r.outcome}`, ...r.items.map((i: any) => ` ${i.id} ${i.text}`), ...r.next.map((n: any) => `next: ${n}`)];
|
|
106
|
+
case 'item': return [`added ${r.item.id} to ${r.goal}: ${r.item.text}`, ...r.next.map((n: any) => `next: ${n}`)];
|
|
107
|
+
case 'check': return [`${r.goal} ${r.item}: ${r.met ? 'met' : 'unmet'} (${r.evidence}); ${r.progress.met}/${r.progress.total} items met${r.proposeDone ? ' (propose done)' : ''}`, ...r.next.map((n: any) => `next: ${n}`)];
|
|
108
|
+
case 'link': return [r.linked === 'already' ? `${r.slug} already serves ${r.goal}` : `${r.slug} now serves ${r.goal}${r.replaced ? ` (was ${r.replaced})` : ''}`];
|
|
109
|
+
case 'done': return [`${r.goal} is done${r.unmet.length ? ` with ${r.unmet.join(', ')} unmet: ${r.reason}` : ''}`];
|
|
110
|
+
default: {
|
|
111
|
+
const lines = r.goals.length === 0 ? ['No goals.'] : [];
|
|
112
|
+
for (const g of r.goals) {
|
|
113
|
+
lines.push(`${g.goal} ${g.status} ${met(g)}${g.proposeDone ? ' (propose done)' : ''} ${g.outcome}`);
|
|
114
|
+
for (const i of g.items) lines.push(` ${i.id} ${i.met ? 'met' : 'open'} ${i.text}${i.evidence ? ` (${i.evidence})` : ''}`);
|
|
115
|
+
for (const [label, list] of [['finished', g.features.finished], ['awaiting release', g.features.awaitingRelease], ['in progress', g.features.inProgress], ['unknown', g.features.unknown]]) {
|
|
116
|
+
if (list.length) lines.push(` ${label}: ${list.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
for (const n of g.nudges) lines.push(` ${n}`);
|
|
119
|
+
for (const c of g.conflicts || []) lines.push(` conflict: ${c.reason}${c.text ? ` (dropped text: ${JSON.stringify(c.text)})` : ''}`);
|
|
120
|
+
}
|
|
121
|
+
if (r.reason) lines.push(`note: ${r.reason}`);
|
|
122
|
+
return [...lines, ...r.next.map((n: any) => `next: ${n}`)];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Prints a result and sets the exit status; shared by the verbs. */
|
|
128
|
+
function emit(result: any, json: any, lines: any) {
|
|
129
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
130
|
+
else if (result.ok === false) console.log(printSafe(`${result.finding}: ${result.message}`));
|
|
131
|
+
else for (const line of lines(result)) console.log(printSafe(line));
|
|
132
|
+
return finishRuntime(result.ok === false ? 1 : 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Runs `fn`, turning a caller mistake into the exit-2 usage result and a lock that never came into the `store-locked` refusal. */
|
|
136
|
+
function guarded(verb: any, action: any, json: any, fn: any) {
|
|
137
|
+
try {
|
|
138
|
+
return fn();
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
if (error instanceof followup.FollowupUsageError || error instanceof StoreUnsafeError) return usageError(verb, error.message, json);
|
|
141
|
+
if (error instanceof StoreLockedError) return emit({ ok: false, action, finding: 'store-locked', message: error.message }, json, () => []);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Runs the store upkeep for one invocation and prints its lines to stderr; returns its refusal, or null. */
|
|
147
|
+
function upkeep(verb: any, action: any, root: any) {
|
|
148
|
+
const prepared = prepareStore(root);
|
|
149
|
+
for (const line of prepared.lines) console.error(printSafe(`doflow ${verb}: ${line}`));
|
|
150
|
+
return prepared.ok ? null : { ok: false, action, finding: prepared.finding, message: prepared.message };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function refuseGlobal(verb: any, global: any, json: any) {
|
|
154
|
+
return global ? usageError(verb, `${verb} works on the project store at the repository root and refuses -g`, json) : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* `followup` verb. Actions: add, list, take, settle, promote, report.
|
|
159
|
+
* @param {Object} options
|
|
160
|
+
* @param {string} [options.action] absent when --action was not given
|
|
161
|
+
* @param {string} options.cwd directory the verb runs from (the project, or a subfolder of it)
|
|
162
|
+
* @param {Object} options.flags parsed flag values: statement, stage, source, taskClass, taskId, release,
|
|
163
|
+
* batch, channel, state, ids, as, reason, evidence, title; for report also file, stdin, text, feature
|
|
164
|
+
*/
|
|
165
|
+
function handleFollowupCommand({ action, cwd, global = false, slug = null, json = false, flags = {} }: any = {}) {
|
|
166
|
+
const refused = refuseGlobal('followup', global, json);
|
|
167
|
+
if (refused !== null) return refused;
|
|
168
|
+
return guarded('followup', action, json, () => {
|
|
169
|
+
if (!FOLLOWUP_ACTIONS.includes(action)) throw new followup.FollowupUsageError(`--action is required: one of ${FOLLOWUP_ACTIONS.join(', ')} (got '${action}')`);
|
|
170
|
+
if (action !== 'report') {
|
|
171
|
+
for (const name of ['file', 'stdin', 'text', 'feature']) {
|
|
172
|
+
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action report only`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const name of ['tag', 'confirm', 'exclude']) {
|
|
176
|
+
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to the lifecycle verb's --action release only, not to followup`);
|
|
177
|
+
}
|
|
178
|
+
const root = projectRoot(cwd || process.cwd());
|
|
179
|
+
const refusedStore = upkeep('followup', action, root);
|
|
180
|
+
if (refusedStore) return emit(refusedStore, json, followupLines);
|
|
181
|
+
let result;
|
|
182
|
+
if (action === 'report') {
|
|
183
|
+
if (flags.statement === undefined) throw new followup.FollowupUsageError('--statement is required for --action report');
|
|
184
|
+
result = followup.reportFollowup({
|
|
185
|
+
root, statement: flags.statement, input: { file: flags.file, stdin: Boolean(flags.stdin), text: flags.text },
|
|
186
|
+
release: flags.release, feature: flags.feature, channel: flags.channel,
|
|
187
|
+
});
|
|
188
|
+
} else if (action === 'add') {
|
|
189
|
+
const batch = flags.batch !== undefined;
|
|
190
|
+
if (batch && flags.statement !== undefined) throw new followup.FollowupUsageError('--batch cannot be combined with --statement');
|
|
191
|
+
if (!batch && flags.statement === undefined) throw new followup.FollowupUsageError('--statement is required for --action add (or pass --batch <file.json>)');
|
|
192
|
+
result = followup.addFollowups({
|
|
193
|
+
root, cwd: cwd || process.cwd(), channel: flags.channel,
|
|
194
|
+
items: batch ? followup.readBatchFile(flags.batch) : [{ statement: flags.statement }],
|
|
195
|
+
defaults: { stage: flags.stage, slug, source: flags.source, taskClass: flags.taskClass, taskId: flags.taskId, release: flags.release, batch },
|
|
196
|
+
});
|
|
197
|
+
} else if (action === 'list') {
|
|
198
|
+
result = followup.listFollowups({ root, state: flags.state });
|
|
199
|
+
} else if (action === 'take') {
|
|
200
|
+
result = followup.takeFollowups({ root, ids: flags.ids, slug, channel: flags.channel });
|
|
201
|
+
} else if (action === 'settle') {
|
|
202
|
+
result = followup.settleFollowups({ root, ids: flags.ids, as: flags.as, reason: flags.reason, evidence: flags.evidence, channel: flags.channel });
|
|
203
|
+
} else {
|
|
204
|
+
if (flags.title === undefined) throw new followup.FollowupUsageError('--title is required for --action promote');
|
|
205
|
+
result = followup.promoteFollowups({ root, ids: flags.ids, title: flags.title, channel: flags.channel });
|
|
206
|
+
}
|
|
207
|
+
return emit(result, json, followupLines);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `lifecycle` verb. Actions: overview, init, release, status, merged.
|
|
213
|
+
* @param {Object} options.flags parsed flag values: take, intent, goal, maintain, since, tag, confirm,
|
|
214
|
+
* feature, exclude, reason, channel
|
|
215
|
+
*/
|
|
216
|
+
function handleLifecycleCommand({ action, cwd, global = false, slug = null, json = false, flags = {} }: any = {}) {
|
|
217
|
+
const refused = refuseGlobal('lifecycle', global, json);
|
|
218
|
+
if (refused !== null) return refused;
|
|
219
|
+
return guarded('lifecycle', action === undefined ? 'overview' : action, json, () => {
|
|
220
|
+
// The bare verb is the overview; the router passes no action unless --action was given.
|
|
221
|
+
const act = action === undefined ? 'overview' : action;
|
|
222
|
+
if (!LIFECYCLE_ACTIONS.includes(act)) throw new followup.FollowupUsageError(`--action is required: one of ${LIFECYCLE_ACTIONS.join(', ')} (got '${action}')`);
|
|
223
|
+
if (flags.maintain && act !== 'overview') throw new followup.FollowupUsageError('--maintain applies to --action overview only');
|
|
224
|
+
if (flags.since !== undefined && !flags.maintain) throw new followup.FollowupUsageError('--since applies to --action overview --maintain only');
|
|
225
|
+
if (act !== 'release') {
|
|
226
|
+
for (const name of ['tag', 'confirm', 'feature', 'exclude']) {
|
|
227
|
+
if (flags[name] !== undefined && flags[name] !== false) throw new followup.FollowupUsageError(`--${name} applies to --action release only`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (flags.reason !== undefined && act !== 'merged') throw new followup.FollowupUsageError('--reason applies to --action merged only');
|
|
231
|
+
const root = projectRoot(cwd || process.cwd());
|
|
232
|
+
const refusedStore = upkeep('lifecycle', act, root);
|
|
233
|
+
if (refusedStore) return emit(refusedStore, json, lifecycleLines);
|
|
234
|
+
let result;
|
|
235
|
+
if (act === 'overview') result = buildOverview({ root, maintain: Boolean(flags.maintain), since: flags.since });
|
|
236
|
+
else if (act === 'init') result = initFeature({ root, slug, take: flags.take, intent: flags.intent, goal: flags.goal });
|
|
237
|
+
else if (act === 'release') result = releaseFeatures({ root, tag: flags.tag, confirm: Boolean(flags.confirm), features: flags.feature, exclude: flags.exclude, channel: flags.channel });
|
|
238
|
+
else if (act === 'merged') result = recordMerged({ root, slug, reason: flags.reason, channel: flags.channel });
|
|
239
|
+
else result = featureStatus({ root, slug });
|
|
240
|
+
return emit(result, json, lifecycleLines);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `goal` verb. Actions: add, item, check, link, list, done.
|
|
246
|
+
* @param {Object} options.flags parsed flag values: goal, statement, item (a list), text, evidence, unmet,
|
|
247
|
+
* replace, reason, channel; `--slug` arrives as `slug`
|
|
248
|
+
*/
|
|
249
|
+
function handleGoalCommand({ action, cwd, global = false, slug = null, json = false, flags = {} }: any = {}) {
|
|
250
|
+
const refused = refuseGlobal('goal', global, json);
|
|
251
|
+
if (refused !== null) return refused;
|
|
252
|
+
return guarded('goal', action, json, () => {
|
|
253
|
+
if (!GOAL_ACTIONS.includes(action)) throw new followup.FollowupUsageError(`--action is required: one of ${GOAL_ACTIONS.join(', ')} (got '${action}')`);
|
|
254
|
+
const given = { ...flags, slug: slug === null ? undefined : slug };
|
|
255
|
+
for (const name of ['statement', 'item', 'text', 'evidence', 'unmet', 'replace', 'reason', 'slug']) {
|
|
256
|
+
if (!GOAL_FLAGS[action].includes(name) && given[name] !== undefined && given[name] !== false) {
|
|
257
|
+
throw new followup.FollowupUsageError(`--${name} does not apply to --action ${action}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (action !== 'list' && flags.goal === undefined) throw new followup.FollowupUsageError(`--goal is required for --action ${action}`);
|
|
261
|
+
const root = projectRoot(cwd || process.cwd());
|
|
262
|
+
const refusedStore = upkeep('goal', action, root);
|
|
263
|
+
if (refusedStore) return emit(refusedStore, json, goalLines);
|
|
264
|
+
const base = { root, goal: flags.goal };
|
|
265
|
+
let result;
|
|
266
|
+
if (action === 'add') result = goal.addGoal({ ...base, statement: flags.statement, items: flags.item, channel: flags.channel });
|
|
267
|
+
else if (action === 'item') result = goal.addItem({ ...base, text: flags.text, channel: flags.channel });
|
|
268
|
+
else if (action === 'check') result = goal.checkItem({ ...base, item: flags.item, evidence: flags.evidence, unmet: Boolean(flags.unmet), channel: flags.channel });
|
|
269
|
+
else if (action === 'link') result = goal.linkFeature({ ...base, slug, replace: Boolean(flags.replace), channel: flags.channel });
|
|
270
|
+
else if (action === 'done') result = goal.doneGoal({ ...base, reason: flags.reason, channel: flags.channel });
|
|
271
|
+
else result = goal.listGoals({ root, goal: flags.goal });
|
|
272
|
+
return emit(result, json, goalLines);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = { handleFollowupCommand, handleLifecycleCommand, handleGoalCommand, FOLLOWUP_ACTIONS, LIFECYCLE_ACTIONS, GOAL_ACTIONS };
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The project lifecycle event store (IC-001, IC-002, IC-003). One JSON file per event under
|
|
7
|
+
* `<root>/.doflow/state/lifecycle/events/`, created with an exclusive create, never edited or
|
|
8
|
+
* renamed, and deleted only by the retention pass. The store is local to a checkout, not shared through git. DoFlow never runs `git add`,
|
|
9
|
+
* `git commit` or `git push` on these paths and never writes an ignore rule for them (DEC-012).
|
|
10
|
+
*
|
|
11
|
+
* A write holds the store lock, folds what is there, refuses an event that would be illegal at the
|
|
12
|
+
* end of that fold (`illegal-transition`, nothing written) and stamps `at` as the later of the
|
|
13
|
+
* clock and one millisecond past the newest `at` it has seen, so a later event on one clone never
|
|
14
|
+
* sorts before one it has read.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const nodeFs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const crypto = require('node:crypto');
|
|
20
|
+
const { acquireLock } = require('../task-state.ts');
|
|
21
|
+
const { printSafe } = require('../mask.ts');
|
|
22
|
+
const { foldInto, finalize, applyEvent } = require('./fold.ts');
|
|
23
|
+
|
|
24
|
+
const LIFECYCLE_REL = path.join('.doflow', 'state', 'lifecycle');
|
|
25
|
+
const EVENTS_REL = path.join(LIFECYCLE_REL, 'events');
|
|
26
|
+
/** The retention journal, beside the events folder and never inside it. */
|
|
27
|
+
const JOURNAL_REL = path.join(LIFECYCLE_REL, 'retention.json');
|
|
28
|
+
/** Lock-free reads tried while the journal's generation keeps moving, before one read under the lock. */
|
|
29
|
+
const STABLE_READS = 3;
|
|
30
|
+
/** The pause between those reads: the store lock's own wait. */
|
|
31
|
+
const READ_WAIT_MS = 20;
|
|
32
|
+
const ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz';
|
|
33
|
+
/** `<UTC YYYYMMDDTHHMMSSmmmZ>-<6 lowercase Crockford base32 characters>`. */
|
|
34
|
+
const EVENT_ID = /^[0-9]{8}T[0-9]{9}Z-[0-9a-hjkmnp-tv-z]{6}$/;
|
|
35
|
+
const COLLISION_RETRIES = 5;
|
|
36
|
+
/** The largest legitimate event is a goal with 100 items of 280 characters (about 30 KiB); a file over this is not one. */
|
|
37
|
+
const MAX_EVENT_BYTES = 256 * 1024;
|
|
38
|
+
/** The retention journal lists at most a pass's worth of names (store-upkeep.js bounds a pass well below this). */
|
|
39
|
+
const MAX_JOURNAL_BYTES = 16 * 1024 * 1024;
|
|
40
|
+
const READ_CHUNK = 64 * 1024;
|
|
41
|
+
/** The decision register's channel vocabulary: who the caller says is acting. */
|
|
42
|
+
const CHANNEL_BY: Record<string, string> = { question: 'user', gate: 'user', prompt: 'user', default: 'agent' };
|
|
43
|
+
|
|
44
|
+
function eventsDir(root: string): string { return path.join(root, EVENTS_REL); }
|
|
45
|
+
function lockTarget(root: string): string { return path.join(root, EVENTS_REL); }
|
|
46
|
+
|
|
47
|
+
/** @param {string} [channel] defaults to `default` @returns {'user'|'agent'|null} null for an unknown channel */
|
|
48
|
+
function byFromChannel(channel: string = 'default'): string | null { return CHANNEL_BY[channel] ?? null; }
|
|
49
|
+
|
|
50
|
+
/** `length` random characters of the lowercase Crockford base32 alphabet. */
|
|
51
|
+
function randomChars(length: number): string {
|
|
52
|
+
const bytes = crypto.randomBytes(length);
|
|
53
|
+
let out = '';
|
|
54
|
+
for (const b of bytes) out += ALPHABET[b % 32];
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function idFor(at: string, random: (n: number) => string): string { return `${at.replace(/[-:.]/g, '')}-${random(6)}`; }
|
|
59
|
+
|
|
60
|
+
/** Synchronous sleep without spinning, as the store lock waits. */
|
|
61
|
+
function sleep(ms: number): void {
|
|
62
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** DEC-045: `at` is a strict UTC timestamp that is a real instant, so year 10000 and "+275760-..." never parse. */
|
|
66
|
+
const STRICT_AT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
67
|
+
function isStrictAt(at: any): boolean {
|
|
68
|
+
if (typeof at !== 'string' || !STRICT_AT.test(at)) return false;
|
|
69
|
+
const ms = Date.parse(at);
|
|
70
|
+
return Number.isFinite(ms) && new Date(ms).toISOString() === at;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An IC-002 envelope whose `at` is strict and equals the time prefix of its file id. */
|
|
74
|
+
function isEnvelope(event: any, idFromName: string): boolean {
|
|
75
|
+
return event && typeof event === 'object' && event.v === 1 && event.id === idFromName
|
|
76
|
+
&& typeof event.type === 'string' && typeof event.by === 'string'
|
|
77
|
+
&& isStrictAt(event.at) && idFromName.startsWith(`${event.at.replace(/[-:.]/g, '')}-`)
|
|
78
|
+
&& event.data && typeof event.data === 'object' && !Array.isArray(event.data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The store folders are the project's own: a symlink at either one could send a read or a write outside the repository. */
|
|
82
|
+
class StoreUnsafeError extends Error {}
|
|
83
|
+
|
|
84
|
+
/** The store lock could not be taken in time; the verb refuses with `store-locked` and writes nothing. */
|
|
85
|
+
class StoreLockedError extends Error {}
|
|
86
|
+
|
|
87
|
+
function assertStoreFolders(root: string, fsImpl: any): void {
|
|
88
|
+
for (const rel of [path.join('.doflow'), path.join('.doflow', 'state'), LIFECYCLE_REL, EVENTS_REL]) {
|
|
89
|
+
const folder = path.join(root, rel);
|
|
90
|
+
let link = false;
|
|
91
|
+
try { link = fsImpl.lstatSync(folder).isSymbolicLink(); } catch { /* absent: nothing to follow */ }
|
|
92
|
+
if (link) throw new StoreUnsafeError(`${folder} is a symbolic link; the lifecycle store is never read or written through one`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* One store file's text, or `{reason}` when it must not be read: not a regular file (a symlink, a
|
|
98
|
+
* FIFO, a device, a folder), larger than `maxBytes`, or gone. A symlink is refused by `lstat`
|
|
99
|
+
* and again by `O_NOFOLLOW` where the platform has it; `O_NONBLOCK` keeps a FIFO swapped in after
|
|
100
|
+
* the `lstat` from blocking the open, and the descriptor is checked again before any read.
|
|
101
|
+
*/
|
|
102
|
+
function readEventFile(fsImpl: any, file: string, maxBytes: number = MAX_EVENT_BYTES): { reason?: string; text?: string } {
|
|
103
|
+
const flags = nodeFs.constants;
|
|
104
|
+
let fd: number | undefined;
|
|
105
|
+
try {
|
|
106
|
+
if (!fsImpl.lstatSync(file).isFile()) return { reason: 'not a regular file' };
|
|
107
|
+
fd = fsImpl.openSync(file, flags.O_RDONLY | (flags.O_NOFOLLOW || 0) | (flags.O_NONBLOCK || 0));
|
|
108
|
+
const st = fsImpl.fstatSync(fd);
|
|
109
|
+
if (!st.isFile()) return { reason: 'not a regular file' };
|
|
110
|
+
const tooLarge = { reason: `larger than ${maxBytes / 1024} KiB` };
|
|
111
|
+
if (st.size > maxBytes) return tooLarge;
|
|
112
|
+
// Read in chunks up to one byte past the cap, so a file that grew after the fstat is still caught.
|
|
113
|
+
const chunks: Buffer[] = [];
|
|
114
|
+
let length = 0;
|
|
115
|
+
while (length <= maxBytes) {
|
|
116
|
+
const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK, maxBytes + 1 - length));
|
|
117
|
+
const n = fsImpl.readSync(fd, chunk, 0, chunk.length, null);
|
|
118
|
+
if (n === 0) break;
|
|
119
|
+
chunks.push(chunk.subarray(0, n));
|
|
120
|
+
length += n;
|
|
121
|
+
}
|
|
122
|
+
return length > maxBytes ? tooLarge : { text: Buffer.concat(chunks, length).toString('utf8') };
|
|
123
|
+
} catch (error: any) {
|
|
124
|
+
return { reason: error.code || 'unreadable' };
|
|
125
|
+
} finally {
|
|
126
|
+
if (fd !== undefined) { try { fsImpl.closeSync(fd); } catch { /* read already done */ } }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The event with every string print-safe; a report excerpt is multi-line by design, so it keeps its line breaks (cli.js removes them when printing). */
|
|
131
|
+
function cleanEvent(event: any): any {
|
|
132
|
+
const clean = printSafe(event);
|
|
133
|
+
if (typeof event.data.excerpt === 'string') clean.data.excerpt = printSafe(event.data.excerpt, { keepLineBreaks: true });
|
|
134
|
+
return clean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Whether `name` is `<EVENT_ID>.json`. */
|
|
138
|
+
function isEventName(name: string): boolean {
|
|
139
|
+
const match = /^(.+)\.json$/.exec(name);
|
|
140
|
+
return Boolean(match) && EVENT_ID.test((match as RegExpExecArray)[1]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The retention journal: the files a retention pass is deleting, and a token that changes on every
|
|
145
|
+
* write. Absent, unreadable, unparseable or of another version reads as no generation and nothing
|
|
146
|
+
* pending; a pending name that is not an event file name is ignored.
|
|
147
|
+
* @returns {{generation: string|null, pending: string[]}}
|
|
148
|
+
*/
|
|
149
|
+
function readJournal(root: string, fsImpl: any = nodeFs): { generation: string | null; pending: string[] } {
|
|
150
|
+
const read = readEventFile(fsImpl, path.join(root, JOURNAL_REL), MAX_JOURNAL_BYTES);
|
|
151
|
+
let journal: any;
|
|
152
|
+
try { journal = read.reason ? null : JSON.parse(read.text as string); } catch { journal = null; }
|
|
153
|
+
if (!journal || typeof journal !== 'object' || journal.v !== 1) return { generation: null, pending: [] };
|
|
154
|
+
return {
|
|
155
|
+
generation: typeof journal.generation === 'string' ? journal.generation : null,
|
|
156
|
+
pending: Array.isArray(journal.pending) ? journal.pending.filter((name: any) => typeof name === 'string' && isEventName(name)) : [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One pass over the event files, leaving out the names in `hidden`. */
|
|
161
|
+
function readEventFiles(root: string, fsImpl: any, hidden: Set<string>, shouldStop?: () => boolean): any {
|
|
162
|
+
const dir = eventsDir(root);
|
|
163
|
+
let names: string[];
|
|
164
|
+
try {
|
|
165
|
+
names = fsImpl.readdirSync(dir);
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return { events: [], unreadable: [], reasons: {} };
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
const orderedNames = names.sort();
|
|
171
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
172
|
+
const events: any[] = [];
|
|
173
|
+
const unreadable: string[] = [];
|
|
174
|
+
const reasons: Record<string, string> = {};
|
|
175
|
+
for (const name of orderedNames) {
|
|
176
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
177
|
+
if (!isEventName(name) || hidden.has(name)) continue;
|
|
178
|
+
const read = readEventFile(fsImpl, path.join(dir, name));
|
|
179
|
+
if (read.reason) { unreadable.push(name); reasons[name] = read.reason; continue; }
|
|
180
|
+
try {
|
|
181
|
+
const event = JSON.parse(read.text as string);
|
|
182
|
+
// Someone else's file is shown as it is read, so its text is made print-safe here (stored bytes are not changed).
|
|
183
|
+
if (isEnvelope(event, name.slice(0, -'.json'.length))) events.push(cleanEvent(event)); else unreadable.push(name);
|
|
184
|
+
} catch {
|
|
185
|
+
unreadable.push(name);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
189
|
+
return { events, unreadable, reasons };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Reads every event file. A missing folder is an empty store; a file whose name is not an event id
|
|
194
|
+
* is ignored; a matching entry that is not a regular file of at most MAX_EVENT_BYTES, or is not an
|
|
195
|
+
* IC-002 envelope (including one still being written), is skipped and named in `unreadable`, with
|
|
196
|
+
* the reason in `reasons[name]` when it is not simply a corrupt file. A symlinked store folder
|
|
197
|
+
* throws StoreUnsafeError.
|
|
198
|
+
*
|
|
199
|
+
* Files the retention journal lists as pending are neither events nor unreadable, so an item a
|
|
200
|
+
* retention pass is removing is seen whole or not at all. When the journal's generation moves
|
|
201
|
+
* during a read, the read is repeated, at most STABLE_READS times, and then done once under the
|
|
202
|
+
* store lock, which every journal writer holds. A caller already holding the lock never sees the
|
|
203
|
+
* generation move, so it returns after the first read. A lock that cannot be taken throws
|
|
204
|
+
* StoreLockedError.
|
|
205
|
+
* @returns {{events: Object[], unreadable: string[], reasons: Object<string,string>, incomplete?: true}}
|
|
206
|
+
*/
|
|
207
|
+
function readEvents(root: string, { fsImpl = nodeFs, lockFallback = true, shouldStop }: any = {}): any {
|
|
208
|
+
assertStoreFolders(root, fsImpl);
|
|
209
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
210
|
+
for (let attempt = 1; attempt <= STABLE_READS; attempt += 1) {
|
|
211
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
212
|
+
const before = readJournal(root, fsImpl);
|
|
213
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
214
|
+
const result = readEventFiles(root, fsImpl, new Set(before.pending), shouldStop);
|
|
215
|
+
if (result.incomplete || shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
216
|
+
const after = readJournal(root, fsImpl);
|
|
217
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
218
|
+
if (after.generation === before.generation) return result;
|
|
219
|
+
if (attempt < STABLE_READS && !shouldStop?.()) sleep(READ_WAIT_MS);
|
|
220
|
+
}
|
|
221
|
+
if (shouldStop?.()) return { events: [], unreadable: [], reasons: {}, incomplete: true };
|
|
222
|
+
if (!lockFallback) throw new StoreLockedError('Lifecycle event store changed during a read; a stable snapshot was unavailable.');
|
|
223
|
+
let release: () => void;
|
|
224
|
+
try {
|
|
225
|
+
release = acquireLock(fsImpl, lockTarget(root));
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
if (/^Could not lock/.test(error.message || '')) throw new StoreLockedError(`${error.message} Nothing was written.`);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return readEventFiles(root, fsImpl, new Set(readJournal(root, fsImpl).pending), shouldStop);
|
|
232
|
+
} finally {
|
|
233
|
+
release();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reads and folds the store.
|
|
239
|
+
* @param {string} root the IC-001 root
|
|
240
|
+
* @param {{hasBody?: Function, fsImpl?: Object}} [options]
|
|
241
|
+
* @returns {Object} the fold result (see fold.js) plus `unreadable`
|
|
242
|
+
*/
|
|
243
|
+
function readFold(root: string, { fsImpl = nodeFs, hasBody, now = new Date() }: any = {}): any {
|
|
244
|
+
const { events, unreadable, reasons } = readEvents(root, { fsImpl });
|
|
245
|
+
return { ...finalize(foldInto(events, { now }), { hasBody }), unreadable, unreadableReasons: reasons };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Picks a free, valid id for each draft before any file is created, so a batch is all or nothing. */
|
|
249
|
+
function allocateIds(fsImpl: any, dir: string, stamped: any[], random: (n: number) => string): string[] | null {
|
|
250
|
+
const chosen = new Set<string>();
|
|
251
|
+
const ids: string[] = [];
|
|
252
|
+
for (const { at, id } of stamped) {
|
|
253
|
+
let candidate: string | null = id;
|
|
254
|
+
for (let attempt = 0; attempt <= COLLISION_RETRIES; attempt += 1) {
|
|
255
|
+
if (attempt > 0) candidate = idFor(at, random);
|
|
256
|
+
if (EVENT_ID.test(candidate as string) && !chosen.has(candidate as string) && !fsImpl.existsSync(path.join(dir, `${candidate}.json`))) break;
|
|
257
|
+
candidate = null;
|
|
258
|
+
}
|
|
259
|
+
if (!candidate) return null;
|
|
260
|
+
chosen.add(candidate);
|
|
261
|
+
ids.push(candidate);
|
|
262
|
+
}
|
|
263
|
+
return ids;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Appends events. All are checked against the local fold first; one illegal event refuses the whole
|
|
268
|
+
* call and nothing is written.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} root the IC-001 root
|
|
271
|
+
* @param {Array<{type:string, by:'user'|'agent', data:Object}>} drafts
|
|
272
|
+
* @param {{now?: Date, fsImpl?: Object, random?: (n:number)=>string}} [options]
|
|
273
|
+
* @returns {{ok:true, written: Array<{id:string, file:string, event:Object}>}
|
|
274
|
+
* | {ok:false, finding:string, message:string, conflict?:Object, written?: Array}}
|
|
275
|
+
* `file` is root-relative with `/` separators. The findings are `illegal-transition` (any fold
|
|
276
|
+
* conflict, with its own `code` in `conflict.code`), `id-collision`, `invalid-id` (the clock or
|
|
277
|
+
* the random source cannot produce an id of the IC-001 shape) and `store-locked`.
|
|
278
|
+
*/
|
|
279
|
+
function appendEvents(root: string, drafts: any[], { now = new Date(), fsImpl = nodeFs, random = randomChars }: any = {}): any {
|
|
280
|
+
const dir = eventsDir(root);
|
|
281
|
+
// A write that would be refused leaves nothing behind, not even the store folder (IC-001: created
|
|
282
|
+
// on the first write), so the check runs once before the folder and the lock and again under the lock.
|
|
283
|
+
const early = planEvents(root, drafts, { now, fsImpl, random });
|
|
284
|
+
if (early.refused) return early.refused;
|
|
285
|
+
fsImpl.mkdirSync(dir, { recursive: true });
|
|
286
|
+
let release: () => void;
|
|
287
|
+
try {
|
|
288
|
+
release = acquireLock(fsImpl, lockTarget(root));
|
|
289
|
+
} catch (error: any) {
|
|
290
|
+
if (/^Could not lock/.test(error.message || '')) return { ok: false, finding: 'store-locked', message: `${error.message} Nothing was written.` };
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const { stamped, refused } = planEvents(root, drafts, { now, fsImpl, random });
|
|
295
|
+
if (refused) return refused;
|
|
296
|
+
const ids = allocateIds(fsImpl, dir, stamped, random);
|
|
297
|
+
if (!ids) return { ok: false, finding: 'id-collision', written: [], message: `could not find a free event id after ${COLLISION_RETRIES} tries. Nothing was written.` };
|
|
298
|
+
const written: any[] = [];
|
|
299
|
+
stamped.forEach(({ draft, at }: any, index: number) => {
|
|
300
|
+
const event = { v: 1, id: ids[index], type: draft.type, at, by: draft.by, data: draft.data };
|
|
301
|
+
const file = path.join(dir, `${event.id}.json`);
|
|
302
|
+
// The ids are free under the lock; the exclusive create only guards against a writer outside it.
|
|
303
|
+
fsImpl.writeFileSync(file, `${JSON.stringify(event, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
304
|
+
written.push({ id: event.id, file: path.relative(root, file).split(path.sep).join('/'), event });
|
|
305
|
+
});
|
|
306
|
+
return { ok: true, written };
|
|
307
|
+
} finally {
|
|
308
|
+
release();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Folds the store, stamps each draft and applies it; `refused` is the whole call's refusal when any is illegal. */
|
|
313
|
+
function planEvents(root: string, drafts: any[], { now, fsImpl, random }: any): any {
|
|
314
|
+
const state = foldInto(readEvents(root, { fsImpl }).events, { now });
|
|
315
|
+
// An event dated far ahead is not in the fold (DEC-045), so it cannot push this stamp forward.
|
|
316
|
+
let floor = state.newestAt === null ? -Infinity : state.newestAt + 1;
|
|
317
|
+
const stamped: any[] = [];
|
|
318
|
+
for (const draft of drafts) {
|
|
319
|
+
const atMs = Math.max(now.getTime(), floor);
|
|
320
|
+
floor = atMs + 1;
|
|
321
|
+
let at: string | null;
|
|
322
|
+
try { at = new Date(atMs).toISOString(); } catch { at = null; }
|
|
323
|
+
// The dry run draws the id the write will try first, so a conflict names the event it refuses.
|
|
324
|
+
const id = at && isStrictAt(at) ? idFor(at, random) : null;
|
|
325
|
+
if (!id || !EVENT_ID.test(id)) {
|
|
326
|
+
return { refused: { ok: false, finding: 'invalid-id', message: 'cannot build an event id of the form <UTC time>-<6 base32 characters> from the clock and the random source. Nothing was written.' } };
|
|
327
|
+
}
|
|
328
|
+
const raised = applyEvent(state, { v: 1, id, type: draft.type, at, by: draft.by, data: draft.data }, { checkFuture: false });
|
|
329
|
+
if (raised.length) {
|
|
330
|
+
return { refused: { ok: false, finding: 'illegal-transition', conflict: raised[0], message: `${raised[0].reason}. Nothing was written.` } };
|
|
331
|
+
}
|
|
332
|
+
stamped.push({ draft, at, id });
|
|
333
|
+
}
|
|
334
|
+
return { stamped };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = {
|
|
338
|
+
appendEvents, readEvents, readFold, byFromChannel, randomChars, StoreUnsafeError, StoreLockedError, MAX_EVENT_BYTES,
|
|
339
|
+
EVENT_ID, EVENTS_REL, LIFECYCLE_REL, JOURNAL_REL, ALPHABET, COLLISION_RETRIES, lockTarget, readJournal, isEventName,
|
|
340
|
+
};
|