@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,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// harness-paths.js — the one validator and resolver for the native path facts DoFlow declares in
|
|
4
|
+
// core/registry/harnesses.json under each harness's "paths" section (Stage 3 of the structure
|
|
5
|
+
// refactor). Adapters consume these declarations through resolveHarnessPaths() instead of
|
|
6
|
+
// hardcoding directory joins; whatever cannot be expressed here without branching (env-var
|
|
7
|
+
// overrides, conditional user directories, content-embedded placeholders) stays in its adapter
|
|
8
|
+
// behind an inline justification comment naming why it cannot be declared.
|
|
9
|
+
//
|
|
10
|
+
// The schema is deliberately minimal — no template engine, no expression language, no string
|
|
11
|
+
// interpolation beyond plain segment joins:
|
|
12
|
+
//
|
|
13
|
+
// "paths": {
|
|
14
|
+
// "<surface>": <rule> — same rule for both scopes
|
|
15
|
+
// | { "project": <rule>, "user": <rule> } — per-scope rules; an omitted scope
|
|
16
|
+
// resolves to null (the surface
|
|
17
|
+
// genuinely does not exist there)
|
|
18
|
+
// }
|
|
19
|
+
//
|
|
20
|
+
// <rule> ::= { "base": "root" | "home", "segments": ["<segment>", ...] }
|
|
21
|
+
//
|
|
22
|
+
// "root" is the scope's install root: the project directory at project scope, the home directory
|
|
23
|
+
// at user scope (adapters historically name that same value scopeRoot at user scope). "home" is
|
|
24
|
+
// always the home directory. Segments are plain single path segments — no separators, no "..".
|
|
25
|
+
// XDG-style directories (~/.config/opencode) are just home-rooted segment joins: no adapter in
|
|
26
|
+
// this repository consults $XDG_* today, so the schema carries no xdg base rather than declaring
|
|
27
|
+
// vocabulary nothing reads.
|
|
28
|
+
//
|
|
29
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
30
|
+
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
31
|
+
// caller's `require('../helper/harness-paths.ts')` keeps working while the tree is still `.js`. The
|
|
32
|
+
// `./repo-root` require stays bare while repo-root is still `.js`.
|
|
33
|
+
|
|
34
|
+
const fs = require('node:fs');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
const { REPO_ROOT } = require('./repo-root.ts');
|
|
37
|
+
|
|
38
|
+
const PATH_BASES = new Set(['root', 'home']);
|
|
39
|
+
const SURFACE_NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
40
|
+
const SEGMENT = /^(?!\.\.$)[^\\/]+$/;
|
|
41
|
+
const SCOPES = ['project', 'user'];
|
|
42
|
+
|
|
43
|
+
interface PathRule {
|
|
44
|
+
base?: string;
|
|
45
|
+
segments?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ResolveInputs {
|
|
49
|
+
scope?: string;
|
|
50
|
+
scopeRoot?: string;
|
|
51
|
+
homeDir?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function issue(errors: string[], location: string, message: string): void { errors.push(`${location}: ${message}`); }
|
|
55
|
+
|
|
56
|
+
/** Validate one {base, segments} rule. Returns errors via the pushed array. */
|
|
57
|
+
function validateRule(rule: any, location: string, errors: string[]): void {
|
|
58
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) {
|
|
59
|
+
issue(errors, location, 'must be an object'); return;
|
|
60
|
+
}
|
|
61
|
+
for (const key of Object.keys(rule)) {
|
|
62
|
+
if (!['base', 'segments'].includes(key)) issue(errors, location, `unsupported key '${key}'`);
|
|
63
|
+
}
|
|
64
|
+
if (!PATH_BASES.has(rule.base)) issue(errors, location, `base must be one of ${[...PATH_BASES].join(', ')}`);
|
|
65
|
+
const segments = rule.segments;
|
|
66
|
+
if (!Array.isArray(segments) || segments.some((segment: any) => typeof segment !== 'string' || !SEGMENT.test(segment))) {
|
|
67
|
+
issue(errors, location, "segments must be an array of plain single path segments (no '/', no '\\', no '..')");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Validate one harness's whole paths section. Unknown keys and malformed rules are rejected
|
|
72
|
+
* loudly: a typo'd declaration must fail the registry load, not silently fall back to whatever
|
|
73
|
+
* the adapter used to hardcode. Returns an array of error messages (empty when valid). */
|
|
74
|
+
function validatePathsSection(section: any, location: string): string[] {
|
|
75
|
+
const errors: string[] = [];
|
|
76
|
+
if (!section || typeof section !== 'object' || Array.isArray(section)) {
|
|
77
|
+
return [`${location}: must be an object`];
|
|
78
|
+
}
|
|
79
|
+
for (const [name, value] of Object.entries<any>(section)) {
|
|
80
|
+
const at = `${location}.${name}`;
|
|
81
|
+
if (!SURFACE_NAME.test(name)) { issue(errors, at, `surface name '${name}' must be camelCase`); continue; }
|
|
82
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { issue(errors, at, 'must be an object'); continue; }
|
|
83
|
+
const hasBase = value.base !== undefined || value.segments !== undefined;
|
|
84
|
+
const scopedKeys = SCOPES.filter((scope) => value[scope] !== undefined);
|
|
85
|
+
const knownKeys = [...SCOPES, 'base', 'segments'];
|
|
86
|
+
for (const key of Object.keys(value)) {
|
|
87
|
+
if (!knownKeys.includes(key)) issue(errors, at, `unsupported key '${key}'`);
|
|
88
|
+
}
|
|
89
|
+
if (hasBase && scopedKeys.length) {
|
|
90
|
+
issue(errors, at, 'must be either a single {base, segments} rule or per-scope {project, user} rules, not both');
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (hasBase) { validateRule(value, at, errors); continue; }
|
|
94
|
+
if (!scopedKeys.length) { issue(errors, at, `requires a rule or at least one of ${SCOPES.join('/')}`); continue; }
|
|
95
|
+
for (const scope of scopedKeys) validateRule(value[scope], `${at}.${scope}`, errors);
|
|
96
|
+
}
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Resolve one rule to an absolute path. */
|
|
101
|
+
function resolveRule(rule: PathRule, { root, home }: { root: string; home: string }): string {
|
|
102
|
+
const base = rule.base === 'home' ? home : root;
|
|
103
|
+
return path.join(base, ...(rule.segments || []));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a harness's declared paths section against runtime scope inputs.
|
|
108
|
+
* `scope` accepts the adapters' historical spellings ('global' == 'user').
|
|
109
|
+
* Returns { <surface>: absolutePath | null } — null when the scope has no rule for a surface —
|
|
110
|
+
* plus `root`, the scope's install root itself (an input, not a declaration).
|
|
111
|
+
*/
|
|
112
|
+
function resolveHarnessPaths(section: any, { scope, scopeRoot, homeDir }: ResolveInputs = {}): Record<string, string | null> {
|
|
113
|
+
const userScope = scope === 'user' || scope === 'global';
|
|
114
|
+
const home = path.resolve(homeDir || scopeRoot);
|
|
115
|
+
const root = userScope ? home : path.resolve(scopeRoot);
|
|
116
|
+
const resolved: Record<string, string | null> = { root };
|
|
117
|
+
for (const [name, value] of Object.entries<any>(section || {})) {
|
|
118
|
+
const rule = (value.base !== undefined || value.segments !== undefined)
|
|
119
|
+
? value
|
|
120
|
+
: (value[userScope ? 'user' : 'project'] ?? null);
|
|
121
|
+
resolved[name] = rule ? resolveRule(rule, { root, home }) : null;
|
|
122
|
+
}
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let declaredCache: { repoRoot: string; paths: Record<string, any> } | undefined;
|
|
127
|
+
/** Every harness's declared paths section, keyed by harness id, read straight from
|
|
128
|
+
* core/registry/harnesses.json (memoized per process — the file ships inside the package).
|
|
129
|
+
* Adapters default their factories to this, so a factory called with no arguments and the CLI
|
|
130
|
+
* wiring in src/cli/shared.js#buildAdapterRegistry consume byte-identical declarations. */
|
|
131
|
+
function declaredHarnessPaths(repoRoot: string = REPO_ROOT): Record<string, any> {
|
|
132
|
+
if (declaredCache && declaredCache.repoRoot === path.resolve(repoRoot)) return declaredCache.paths;
|
|
133
|
+
const file = path.join(path.resolve(repoRoot), 'core', 'registry', 'harnesses.json');
|
|
134
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
135
|
+
const paths = Object.fromEntries((parsed.harnesses || []).map((harness: any) => [harness.id, harness.paths ?? {}]));
|
|
136
|
+
declaredCache = { repoRoot: path.resolve(repoRoot), paths };
|
|
137
|
+
return paths;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { PATH_BASES, validatePathsSection, resolveHarnessPaths, declaredHarnessPaths };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// marker-merge.js — marker-delimited merge so a target project's own CLAUDE.md
|
|
3
|
+
// content survives repeated doflow install/update runs; only the span between the
|
|
4
|
+
// markers is ever regenerated.
|
|
5
|
+
//
|
|
6
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
7
|
+
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
8
|
+
// caller's `require('../helper/marker-merge.ts')` keeps working while the tree is still `.js`.
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
const MARKER_START =
|
|
13
|
+
'<!-- doflow:start — content below is managed by doflow install/update; edits here are overwritten on the next run -->';
|
|
14
|
+
const MARKER_END = '<!-- doflow:end -->';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Merge `srcAbs`'s content into `dstAbs` inside a doflow-owned marker span,
|
|
18
|
+
* preserving any of `dstAbs`'s own content outside that span byte-for-byte.
|
|
19
|
+
*/
|
|
20
|
+
function mergeMarkedSection(srcAbs: string, dstAbs: string, { dryRun = false }: { dryRun?: boolean } = {}): { changed: boolean } {
|
|
21
|
+
const srcContent = fs.readFileSync(srcAbs, 'utf8').replace(/\s+$/, '');
|
|
22
|
+
const section = `${MARKER_START}\n${srcContent}\n${MARKER_END}\n`;
|
|
23
|
+
|
|
24
|
+
// Read once, reused both for parsing and for the final no-op comparison below.
|
|
25
|
+
const existing = fs.existsSync(dstAbs) ? fs.readFileSync(dstAbs, 'utf8') : null;
|
|
26
|
+
|
|
27
|
+
let newContent: string;
|
|
28
|
+
if (existing === null) {
|
|
29
|
+
newContent = section;
|
|
30
|
+
} else {
|
|
31
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
32
|
+
if (startIdx === -1) {
|
|
33
|
+
// Foreign file with no doflow span yet — append without touching existing's own bytes.
|
|
34
|
+
// A file ending in a newline (or empty) always gets exactly one '\n', so removeMarkedSection
|
|
35
|
+
// can strip exactly one and hand the user's bytes back unchanged; one with no final newline
|
|
36
|
+
// gets a blank line, and comes back from removal with a final newline added.
|
|
37
|
+
const separator = existing === '' || existing.endsWith('\n') ? '\n' : '\n\n';
|
|
38
|
+
newContent = existing + separator + section;
|
|
39
|
+
} else {
|
|
40
|
+
const endIdx = existing.indexOf(MARKER_END, startIdx + MARKER_START.length);
|
|
41
|
+
if (endIdx === -1) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Malformed doflow markers in ${dstAbs}: found MARKER_START with no matching MARKER_END — fix or remove the markers manually before retrying.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
// Search for a duplicate START only *after* the span closes, not from inside it — the
|
|
47
|
+
// previous version of this function searched from startIdx + MARKER_START.length, which
|
|
48
|
+
// is *inside* the span, and false-positived whenever the span's own content happened to
|
|
49
|
+
// contain the marker text (e.g. srcContent documenting this exact mechanism). Searching
|
|
50
|
+
// from after the real end only flags a genuine second, structurally separate span.
|
|
51
|
+
//
|
|
52
|
+
// Residual limitation: endIdx above is still the *first* MARKER_END found after startIdx,
|
|
53
|
+
// so srcContent containing the literal MARKER_END text (not just MARKER_START) could still
|
|
54
|
+
// truncate the span early. Fixing that by taking the *last* MARKER_END in the file instead
|
|
55
|
+
// was tried and reverted — it silently defeated this exact duplicate-span detection (two
|
|
56
|
+
// complete, genuinely separate spans collapse into one under lastIndexOf). Between "rare,
|
|
57
|
+
// avoidable corruption if source content literally quotes MARKER_END" and "silently losing
|
|
58
|
+
// detection of real duplicate spans," the latter is worse, so this trade-off is intentional
|
|
59
|
+
// pending a more precise fix.
|
|
60
|
+
const dupIdx = existing.indexOf(MARKER_START, endIdx + MARKER_END.length);
|
|
61
|
+
if (dupIdx !== -1) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Malformed doflow markers in ${dstAbs}: found a duplicate doflow:start marker — fix or remove the markers manually before retrying.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
// section always supplies its own trailing "\n" after MARKER_END, so the
|
|
67
|
+
// original file's matching trailing "\n" (if present) must be consumed here —
|
|
68
|
+
// otherwise a replace with no real change grows the file by one newline
|
|
69
|
+
// every time it runs (breaks idempotency).
|
|
70
|
+
let afterEnd = endIdx + MARKER_END.length;
|
|
71
|
+
if (existing[afterEnd] === '\n') afterEnd += 1;
|
|
72
|
+
newContent = existing.slice(0, startIdx) + section + existing.slice(afterEnd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (existing === newContent) return { changed: false };
|
|
77
|
+
if (dryRun) return { changed: true };
|
|
78
|
+
|
|
79
|
+
fs.mkdirSync(path.dirname(dstAbs), { recursive: true });
|
|
80
|
+
fs.writeFileSync(dstAbs, newContent);
|
|
81
|
+
return { changed: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Strip only the doflow-managed span from `file`, leaving any surrounding content (before/after
|
|
86
|
+
* the markers) byte-for-byte untouched. Companion to `mergeMarkedSection` — shared by the Claude
|
|
87
|
+
* and Codex adapters' `remove()`, which had each carried an identical private copy of this.
|
|
88
|
+
* A file that held nothing but the span is DoFlow's own and is deleted; any other file is written
|
|
89
|
+
* back, even when only whitespace remains.
|
|
90
|
+
* @returns whether a managed span was found and stripped (false if the file doesn't
|
|
91
|
+
* exist or has no doflow markers — both are no-ops, not errors)
|
|
92
|
+
*/
|
|
93
|
+
function removeMarkedSection(file: string): boolean {
|
|
94
|
+
if (!fs.existsSync(file)) return false;
|
|
95
|
+
const existing = fs.readFileSync(file, 'utf8');
|
|
96
|
+
const start = existing.indexOf(MARKER_START);
|
|
97
|
+
if (start === -1) return false;
|
|
98
|
+
const end = existing.indexOf(MARKER_END, start + MARKER_START.length);
|
|
99
|
+
if (end === -1 || existing.indexOf(MARKER_START, end + MARKER_END.length) !== -1) {
|
|
100
|
+
throw new Error(`Refusing to remove malformed DoFlow markers in ${file}`);
|
|
101
|
+
}
|
|
102
|
+
let after = end + MARKER_END.length;
|
|
103
|
+
if (existing[after] === '\n') after += 1;
|
|
104
|
+
const before = existing.slice(0, start);
|
|
105
|
+
const rest = existing.slice(after);
|
|
106
|
+
if (before === '' && rest === '') {
|
|
107
|
+
fs.unlinkSync(file);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
// Do not alter user content; only remove the one '\n' mergeMarkedSection put before an
|
|
111
|
+
// appended span.
|
|
112
|
+
fs.writeFileSync(file, (rest === '' && before.endsWith('\n') ? before.slice(0, -1) : before) + rest);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { mergeMarkedSection, removeMarkedSection, MARKER_START, MARKER_END };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// prompt.js — port of sync.sh's confirm(): y/N prompt, auto-confirmed under --force.
|
|
3
|
+
//
|
|
4
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
5
|
+
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
6
|
+
// caller's `require('../helper/prompt.ts')` keeps working while the rest of the tree is still `.js`.
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* fs.readSync(0, ...) is documented as blocking, but that only holds if fd 0's O_NONBLOCK flag is
|
|
11
|
+
* clear. On a real TTY that flag can end up set (observed after raw-mode use elsewhere in the
|
|
12
|
+
* process, e.g. mcp.js's checkbox prompt) — a read attempted before the user has typed anything
|
|
13
|
+
* then throws EAGAIN instead of waiting for a keypress. That's "not ready yet", not "no stdin
|
|
14
|
+
* available": retry after a brief synchronous sleep rather than failing the prompt closed. Only
|
|
15
|
+
* done when stdin is actually a TTY (a human might still type) and bounded by a deadline (a fd
|
|
16
|
+
* that will genuinely never produce data must still fail closed, not hang forever).
|
|
17
|
+
*/
|
|
18
|
+
function readSyncBlocking(fd: number, buf: Buffer): number {
|
|
19
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
20
|
+
for (;;) {
|
|
21
|
+
try {
|
|
22
|
+
return fs.readSync(fd, buf, 0, buf.length, null);
|
|
23
|
+
} catch (e: any) {
|
|
24
|
+
if (!process.stdin.isTTY || e.code !== 'EAGAIN' || Date.now() > deadline) throw e;
|
|
25
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** true if confirmed (via --force or an explicit y/Y reply) */
|
|
31
|
+
function confirm(message: string, force: boolean): boolean {
|
|
32
|
+
if (force) {
|
|
33
|
+
console.error(`[INFO] ${message} [auto-confirmed via --force]`);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
process.stdout.write(`${message} [y/N] `);
|
|
37
|
+
const buf = Buffer.alloc(256);
|
|
38
|
+
let bytesRead = 0;
|
|
39
|
+
try {
|
|
40
|
+
bytesRead = readSyncBlocking(0, buf);
|
|
41
|
+
} catch {
|
|
42
|
+
return false; // no stdin available (e.g. piped/non-interactive) — matches bash `read` failing closed
|
|
43
|
+
}
|
|
44
|
+
const reply = buf.toString('utf8', 0, bytesRead).trim();
|
|
45
|
+
return /^[Yy]$/.test(reply);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Free-text stdin read (e.g. "which backup id?"), matching sync.sh's `read -r reply`. */
|
|
49
|
+
function promptLine(message: string): string {
|
|
50
|
+
process.stdout.write(message);
|
|
51
|
+
const buf = Buffer.alloc(1024);
|
|
52
|
+
let bytesRead = 0;
|
|
53
|
+
try {
|
|
54
|
+
bytesRead = readSyncBlocking(0, buf);
|
|
55
|
+
} catch {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
return buf.toString('utf8', 0, bytesRead).trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { confirm, promptLine, readSyncBlocking };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The package root, resolved once from this file's own location.
|
|
7
|
+
*
|
|
8
|
+
* Eighteen modules used to compute this themselves as `REPO_ROOT`, which
|
|
9
|
+
* silently encodes how deep the computing file sits. Grouping the scaffold, trace and verification
|
|
10
|
+
* modules into directories moved three of them one level down and turned that uniform duplication
|
|
11
|
+
* into two different forms of the same expression — so a later move has to pick the right one, with
|
|
12
|
+
* nothing checking the choice. Every wrong pick resolves to a real directory (`<repo>/src`), so it
|
|
13
|
+
* fails by reading the wrong tree rather than by throwing.
|
|
14
|
+
*
|
|
15
|
+
* Homed in `src/helper/` because the root is not a runtime, install or harness concern: it is the
|
|
16
|
+
* one fact all three layers share. `src/adapters/`, `src/install/` and `src/lifecycle/` already
|
|
17
|
+
* depend on this directory.
|
|
18
|
+
*
|
|
19
|
+
* A constant rather than a function: it cannot vary within a process, and a function invites a
|
|
20
|
+
* caller to pass an argument that would make it vary.
|
|
21
|
+
*
|
|
22
|
+
* TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
23
|
+
* native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
24
|
+
* caller's `require('../helper/repo-root.ts')` keeps working while the rest of the tree is still
|
|
25
|
+
* `.js`. As the highest-fan-out helper, this is the last T1 leaf converted.
|
|
26
|
+
*/
|
|
27
|
+
const REPO_ROOT: string = path.resolve(__dirname, '..', '..');
|
|
28
|
+
|
|
29
|
+
module.exports = { REPO_ROOT };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// settings.json and keybindings.json are SHARED files: the user hand-maintains them, DoFlow adds
|
|
4
|
+
// its hooks, and third-party tools (other installers, command proxies) add their own entries.
|
|
5
|
+
// That makes
|
|
6
|
+
// whole-file SHA-256 ownership — correct for copy-tree assets DoFlow owns outright — the wrong
|
|
7
|
+
// model here. It can only ever hold on a machine DoFlow provisioned from empty, so on any real
|
|
8
|
+
// machine a first install saw "target != source" and refused, aborting the entire lifecycle
|
|
9
|
+
// before a single file landed.
|
|
10
|
+
//
|
|
11
|
+
// These helpers give the settings renderer the same posture marker-merge already gives
|
|
12
|
+
// CLAUDE.md: DoFlow guarantees ITS keys are present and correct, and treats everything else in
|
|
13
|
+
// the file as none of its business — preserved on write, ignored on verify, left behind on
|
|
14
|
+
// remove.
|
|
15
|
+
//
|
|
16
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
17
|
+
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
18
|
+
// caller's `require('../helper/settings-merge.ts')` keeps working while the tree is still `.js`.
|
|
19
|
+
|
|
20
|
+
interface HookCommand { command?: unknown }
|
|
21
|
+
interface HookEntry { matcher?: unknown; hooks?: HookCommand[] }
|
|
22
|
+
type Settings = Record<string, any>;
|
|
23
|
+
|
|
24
|
+
/** A hook entry's identity is its (matcher, commands) pair — NOT the commands alone. One script is
|
|
25
|
+
* legitimately registered under several matchers for the same event (post-edit-lint.sh runs on
|
|
26
|
+
* both `Edit` and `Write`), so comparing commands only makes those entries indistinguishable and
|
|
27
|
+
* silently collapses them to one during a merge. Position is not usable: it is not stable across
|
|
28
|
+
* a re-render. */
|
|
29
|
+
function hookCommands(entry: HookEntry | null | undefined): string[] {
|
|
30
|
+
return (entry?.hooks || []).map((hook) => hook?.command).filter((command): command is string => typeof command === 'string');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameHookEntry(a: HookEntry | null | undefined, b: HookEntry | null | undefined): boolean {
|
|
34
|
+
if ((a?.matcher ?? null) !== (b?.matcher ?? null)) return false;
|
|
35
|
+
const left = hookCommands(a);
|
|
36
|
+
const right = hookCommands(b);
|
|
37
|
+
return left.length === right.length && left.every((command, index) => command === right[index]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isPlainObject(value: any): value is Record<string, any> {
|
|
41
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Union of managed hook entries into whatever is already registered for an event. Foreign
|
|
45
|
+
* entries keep their relative order and stay first, so a third-party guard that was already
|
|
46
|
+
* intercepting a tool keeps intercepting it. */
|
|
47
|
+
function mergeHookEvent(existing: HookEntry[] | undefined, managed: HookEntry[] | undefined): HookEntry[] {
|
|
48
|
+
const result = Array.isArray(existing) ? existing.slice() : [];
|
|
49
|
+
for (const entry of managed || []) {
|
|
50
|
+
if (!result.some((present) => sameHookEntry(present, entry))) result.push(entry);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mergeHooks(existing: any, managed: any): Record<string, HookEntry[]> {
|
|
56
|
+
const result = isPlainObject(existing) ? { ...existing } : {};
|
|
57
|
+
for (const [event, entries] of Object.entries<any>(managed || {})) {
|
|
58
|
+
result[event] = mergeHookEvent(result[event], entries);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Merge DoFlow's managed settings into an existing file.
|
|
64
|
+
*
|
|
65
|
+
* Precedence is deliberate and asymmetric:
|
|
66
|
+
* - `hooks` — managed entries are ADDED (union). DoFlow's guardrails must actually be present,
|
|
67
|
+
* and foreign entries must survive.
|
|
68
|
+
* - everything else — the EXISTING value wins. DoFlow ships opinionated defaults (`model`,
|
|
69
|
+
* `permissions`, `outputStyle`), and silently overriding a user's explicit choice during an
|
|
70
|
+
* install is exactly the surprise this module exists to prevent. Managed keys the file does
|
|
71
|
+
* not have yet are still added, so a fresh install is unchanged.
|
|
72
|
+
*/
|
|
73
|
+
function mergeSettings(existing: any, managed: any): Settings {
|
|
74
|
+
if (!isPlainObject(existing)) return managed;
|
|
75
|
+
const result: Settings = { ...existing };
|
|
76
|
+
for (const [key, value] of Object.entries<any>(managed || {})) {
|
|
77
|
+
if (key === 'hooks') { result.hooks = mergeHooks(existing.hooks, value); continue; }
|
|
78
|
+
if (!Object.hasOwn(result, key)) { result[key] = value; continue; }
|
|
79
|
+
if (isPlainObject(result[key]) && isPlainObject(value)) result[key] = mergeSettings(result[key], value);
|
|
80
|
+
// else: existing scalar/array wins — user intent is preserved.
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Verification predicate replacing byte-equality: DoFlow's own keys must be present, and every
|
|
86
|
+
* managed hook entry must still be registered. A foreign key, a user-tuned `model`, or an extra
|
|
87
|
+
* hook entry are all legitimate and must NOT read as drift. */
|
|
88
|
+
function settingsContains(current: any, managed: any): boolean {
|
|
89
|
+
if (!isPlainObject(current)) return false;
|
|
90
|
+
for (const [key, value] of Object.entries<any>(managed || {})) {
|
|
91
|
+
if (key === 'hooks') {
|
|
92
|
+
for (const [event, entries] of Object.entries<any>(value || {})) {
|
|
93
|
+
const present = current.hooks?.[event];
|
|
94
|
+
if (!Array.isArray(present)) return false;
|
|
95
|
+
for (const entry of entries || []) {
|
|
96
|
+
if (!present.some((candidate: HookEntry) => sameHookEntry(candidate, entry))) return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!Object.hasOwn(current, key)) return false;
|
|
102
|
+
if (isPlainObject(value) && isPlainObject(current[key]) && !settingsContains(current[key], value)) return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Remove DoFlow's hook entries and leave everything else standing. `doflow remove` previously
|
|
108
|
+
* rmSync'd the whole file, which on a shared settings.json would delete the user's entire
|
|
109
|
+
* own configuration along with DoFlow's contribution. Returns null only when nothing
|
|
110
|
+
* meaningful remains, letting the caller delete a file DoFlow genuinely created. */
|
|
111
|
+
function stripManagedSettings(current: any, managed: any): Settings | null {
|
|
112
|
+
if (!isPlainObject(current)) return null;
|
|
113
|
+
const result: Settings = { ...current };
|
|
114
|
+
const managedHooks = managed?.hooks;
|
|
115
|
+
if (isPlainObject(result.hooks) && isPlainObject(managedHooks)) {
|
|
116
|
+
const hooks: Record<string, HookEntry[]> = {};
|
|
117
|
+
for (const [event, entries] of Object.entries<any>(result.hooks)) {
|
|
118
|
+
const remaining = (entries || []).filter(
|
|
119
|
+
(entry: HookEntry) => !(managedHooks[event] || []).some((owned: HookEntry) => sameHookEntry(owned, entry)),
|
|
120
|
+
);
|
|
121
|
+
if (remaining.length) hooks[event] = remaining;
|
|
122
|
+
}
|
|
123
|
+
if (Object.keys(hooks).length) result.hooks = hooks; else delete result.hooks;
|
|
124
|
+
}
|
|
125
|
+
// Beyond hooks, drop any key still holding exactly the value DoFlow wrote — we put it there and
|
|
126
|
+
// nothing has touched it since. A key the user changed no longer matches and is left alone.
|
|
127
|
+
// Without this, a file DoFlow created outright (keybindings.json, which has no hooks at all)
|
|
128
|
+
// would survive `remove` fully intact and read as a resource that refused to go away.
|
|
129
|
+
for (const [key, value] of Object.entries<any>(managed || {})) {
|
|
130
|
+
if (key === 'hooks') continue;
|
|
131
|
+
if (Object.hasOwn(result, key) && JSON.stringify(result[key]) === JSON.stringify(value)) delete result[key];
|
|
132
|
+
}
|
|
133
|
+
const meaningful = Object.keys(result).filter((key) => !key.startsWith('$'));
|
|
134
|
+
return meaningful.length ? result : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True when at least one managed hook entry is still registered. Only hook entries count:
|
|
138
|
+
* non-hook managed keys are ordinary settings that survive a remove and are indistinguishable
|
|
139
|
+
* from the user's own, so they cannot signal DoFlow's presence. Lets verification tell "removed"
|
|
140
|
+
* (none of ours left) apart from "damaged" (some of ours left, some missing). */
|
|
141
|
+
function settingsContainsAny(current: any, managed: any): boolean {
|
|
142
|
+
if (!isPlainObject(current)) return false;
|
|
143
|
+
for (const [event, entries] of Object.entries<any>(managed?.hooks || {})) {
|
|
144
|
+
const present = current.hooks?.[event];
|
|
145
|
+
if (!Array.isArray(present)) continue;
|
|
146
|
+
if ((entries || []).some((entry: HookEntry) => present.some((candidate: HookEntry) => sameHookEntry(candidate, entry)))) return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { mergeSettings, mergeHooks, settingsContains, settingsContainsAny, stripManagedSettings, sameHookEntry };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// settings-scope.js — GLOBAL_HOOK_PREFIX/PROJECT_HOOK_PREFIX are the shared constants the Claude
|
|
3
|
+
// adapter's settings-asset renderer (src/adapters/claude/index.ts#settingsContent) uses to rewrite
|
|
4
|
+
// a project-scoped settings.json's hook `command` paths from the global-only `~/.claude/hooks/`
|
|
5
|
+
// form to Claude Code's documented ${CLAUDE_PROJECT_DIR}/.claude/hooks/ placeholder — see
|
|
6
|
+
// https://code.claude.com/docs/en/hooks. chmodHooksExecutable is a defensive final pass: an
|
|
7
|
+
// npm-packaged tarball does not reliably preserve the executable bit on arbitrary files (unlike a
|
|
8
|
+
// git checkout, which usually does), so this unconditionally re-adds +x after hook scripts are
|
|
9
|
+
// deployed, regardless of what bit the source file happened to carry on this machine.
|
|
10
|
+
//
|
|
11
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): authored as `.ts`, run directly by Node
|
|
12
|
+
// native type-stripping — no build step, no runtime dependency. Body stays CommonJS so a `.js`
|
|
13
|
+
// caller's `require('../helper/settings-scope.ts')` keeps working while the tree is still `.js`.
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
const GLOBAL_HOOK_PREFIX = '~/.claude/hooks/';
|
|
18
|
+
const PROJECT_HOOK_PREFIX = '${CLAUDE_PROJECT_DIR}/.claude/hooks/';
|
|
19
|
+
|
|
20
|
+
/** Adds ugo+x (relative — preserves existing bits), not an absolute mode — a hook file copied in
|
|
21
|
+
* at 664/775 must stay that way, only gaining +x. */
|
|
22
|
+
function chmodHooksExecutable(claudeDir: string): void {
|
|
23
|
+
const hooksDir = path.join(claudeDir, 'hooks');
|
|
24
|
+
if (!fs.existsSync(hooksDir)) return;
|
|
25
|
+
for (const f of fs.readdirSync(hooksDir)) {
|
|
26
|
+
if (!f.endsWith('.sh')) continue;
|
|
27
|
+
const p = path.join(hooksDir, f);
|
|
28
|
+
const currentMode = fs.statSync(p).mode & 0o777;
|
|
29
|
+
fs.chmodSync(p, currentMode | 0o111);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { GLOBAL_HOOK_PREFIX, PROJECT_HOOK_PREFIX, chmodHooksExecutable };
|