@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,620 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Shared copy-tree engine — the generic "own a tree of files" implementation every asset
|
|
6
|
+
// declaring `renderer: "copy-tree"` in core/registry/assets.json needs. Codex's adapter already
|
|
7
|
+
// proved this per-file fingerprint/conflict model for agents and hooks (ownedRemovalPlan,
|
|
8
|
+
// nativeManagedResources); this module generalizes it so Claude, Gemini, and Codex's own
|
|
9
|
+
// tree-shaped assets (skills, rules, agent-specs, templates, scripts, modes, references) share
|
|
10
|
+
// one implementation instead of three adapter-specific copies.
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
|
|
15
|
+
function sha256(buf: any): string {
|
|
16
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function walkRelFiles(dir: string, fsImpl: any): string[] {
|
|
20
|
+
const out: string[] = [];
|
|
21
|
+
for (const entry of fsImpl.readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
const abs = path.join(dir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) out.push(...walkRelFiles(abs, fsImpl).map((rel) => path.join(entry.name, rel)));
|
|
24
|
+
else if (entry.isFile()) out.push(entry.name);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Destination layouts. A copy-tree normally mirrors source paths exactly, but a harness may
|
|
30
|
+
* require a different shape for the same content: Antigravity discovers a custom agent at
|
|
31
|
+
* `.agents/agents/<name>/agent.md`, a directory per agent, so DoFlow's flat `<name>.md` specs were
|
|
32
|
+
* written where that harness never looks. Declared per projection in the registry rather than
|
|
33
|
+
* branched on inside an adapter, so the shape stays visible next to the asset it applies to.
|
|
34
|
+
* @type {Record<string, (sourceRel: string) => string>}
|
|
35
|
+
*/
|
|
36
|
+
const LAYOUTS: Record<string, (sourceRel: string) => string> = {
|
|
37
|
+
/** `spec-analyst.md` -> `spec-analyst/agent.md` */
|
|
38
|
+
'dir-per-file:agent.md': (sourceRel: string) => {
|
|
39
|
+
const ext = path.extname(sourceRel);
|
|
40
|
+
return path.join(sourceRel.slice(0, sourceRel.length - ext.length), `agent${ext || '.md'}`);
|
|
41
|
+
},
|
|
42
|
+
/** `RULE_01_SAFETY.md` -> `RULE_01_SAFETY.instructions.md` — GitHub's path-specific
|
|
43
|
+
* instructions are discovered by their `.instructions.md` suffix. */
|
|
44
|
+
'instructions-md': (sourceRel: string) => {
|
|
45
|
+
const ext = path.extname(sourceRel);
|
|
46
|
+
return `${sourceRel.slice(0, sourceRel.length - ext.length)}.instructions.md`;
|
|
47
|
+
},
|
|
48
|
+
/** `MODE_Orchestration.md` -> `doflow-orchestration.md` — Claude's /config picker lists
|
|
49
|
+
* output styles by file name; the doflow- prefix keeps DoFlow's styles grouped and
|
|
50
|
+
* non-colliding with anything a user authors by hand. */
|
|
51
|
+
'doflow-output-style': (sourceRel: string) => {
|
|
52
|
+
const ext = path.extname(sourceRel);
|
|
53
|
+
const base = path.basename(sourceRel, ext).replace(/^MODE_/i, '').replace(/_/g, '-').toLowerCase();
|
|
54
|
+
return `doflow-${base}${ext || '.md'}`;
|
|
55
|
+
},
|
|
56
|
+
/** `core-implementer.md` -> `core-implementer.json` — kiro-cli discovers custom agents as
|
|
57
|
+
* `.kiro/agents/<name>.json` (2.27.1 `kiro-cli agent list` did not list `.md` agents). */
|
|
58
|
+
'kiro-agent-json': (sourceRel: string) => {
|
|
59
|
+
const ext = path.extname(sourceRel);
|
|
60
|
+
return `${sourceRel.slice(0, sourceRel.length - ext.length)}.json`;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Resolve a declared layout name to its mapper. Unknown names fail loudly rather than silently
|
|
65
|
+
* mirroring, because a typo would otherwise install to the wrong shape and look like it worked. */
|
|
66
|
+
function resolveLayout(name: any): (sourceRel: string) => string {
|
|
67
|
+
if (!name) return (rel: string) => rel;
|
|
68
|
+
const layout = LAYOUTS[name];
|
|
69
|
+
if (!layout) throw new Error(`Unknown copy-tree layout '${name}' (known: ${Object.keys(LAYOUTS).join(', ')})`);
|
|
70
|
+
return layout;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Destination content transforms. Where a harness reads a supported but different file FORMAT
|
|
74
|
+
* than the shared source authors (Copilot's path-specific instructions require an `applyTo`
|
|
75
|
+
* frontmatter header; OpenCode's markdown agents want their own frontmatter vocabulary), the
|
|
76
|
+
* projection declares a named transform instead of an adapter branching on bytes. A transform is
|
|
77
|
+
* a pure function of (sourceRelPath, sourceBytes) -> Buffer: deterministic, so the fingerprint
|
|
78
|
+
* planTree records is the fingerprint verifyTree re-derives and removeTree re-checks, and so
|
|
79
|
+
* applyTree can re-derive the written bytes from the untouched source at any later retry.
|
|
80
|
+
* @type {Record<string, (sourceRel: string, content: Buffer) => Buffer>}
|
|
81
|
+
*/
|
|
82
|
+
const TRANSFORMS: Record<string, (sourceRel: string, content: any) => any> = {
|
|
83
|
+
/** Shared mode doc -> Claude output style with name/description/keep-coding-instructions */
|
|
84
|
+
'claude-output-styles': renderClaudeOutputStyle,
|
|
85
|
+
/** `<rule>.md` -> `<rule>.instructions.md` body under a Copilot applyTo header */
|
|
86
|
+
'copilot-rule-instructions': (sourceRel: string, content: any) => {
|
|
87
|
+
void sourceRel;
|
|
88
|
+
return Buffer.from(`---\napplyTo: '**'\n---\n\n${stripFrontmatter(content.toString('utf8'))}`);
|
|
89
|
+
},
|
|
90
|
+
/** Shared agent spec frontmatter -> only the keys Gemini CLI's subagent schema accepts */
|
|
91
|
+
'gemini-agents': (sourceRel: string, content: any) => {
|
|
92
|
+
void sourceRel;
|
|
93
|
+
return Buffer.from(filterGeminiAgentFrontmatter(content.toString('utf8')));
|
|
94
|
+
},
|
|
95
|
+
/** Shared agent spec frontmatter -> OpenCode's markdown-agent vocabulary */
|
|
96
|
+
'opencode-agents': (sourceRel: string, content: any) => {
|
|
97
|
+
void sourceRel;
|
|
98
|
+
return Buffer.from(renderOpencodeAgent(content.toString('utf8')));
|
|
99
|
+
},
|
|
100
|
+
/** Shared agent spec -> kiro-cli custom agent JSON (name, description, prompt, tools, hooks) */
|
|
101
|
+
'kiro-agents': (sourceRel: string, content: any) => {
|
|
102
|
+
void sourceRel;
|
|
103
|
+
return Buffer.from(renderKiroAgent(content.toString('utf8')));
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function resolveTransform(name: any): ((sourceRel: string, content: any) => any) | null {
|
|
108
|
+
if (!name) return null;
|
|
109
|
+
const transform = TRANSFORMS[name];
|
|
110
|
+
if (!transform) throw new Error(`Unknown copy-tree transform '${name}' (known: ${Object.keys(TRANSFORMS).join(', ')})`);
|
|
111
|
+
return transform;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stripFrontmatter(text: string): string {
|
|
115
|
+
if (!text.startsWith('---')) return text;
|
|
116
|
+
const end = text.indexOf('\n---', 3);
|
|
117
|
+
if (end === -1) return text;
|
|
118
|
+
return text.slice(end + 4).replace(/^\n+/, '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Wrap a shared mode document as a Claude output style: system-prompt modifiers keep the
|
|
122
|
+
* built-in engineering instructions (these styles shape HOW DoFlow works, not WHETHER Claude
|
|
123
|
+
* codes). The description is lifted from the document's first `**Purpose**` line so the /config
|
|
124
|
+
* picker explains each style without opening it. */
|
|
125
|
+
function renderClaudeOutputStyle(sourceRel: string, content: any): string {
|
|
126
|
+
const text = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
|
|
127
|
+
const base = path.basename(sourceRel).replace(/\.[^.]+$/, '').replace(/^MODE_/i, '').replace(/_/g, ' ');
|
|
128
|
+
const purpose = text.match(/\*\*Purpose\*\*[:*]*\s*(.+)?/);
|
|
129
|
+
const description = (purpose && purpose[1] ? purpose[1] : `DoFlow ${base} mode`).trim().replace(/\s+/g, ' ');
|
|
130
|
+
return ['---', `name: DoFlow: ${base}`, `description: ${JSON.stringify(description)}`, 'keep-coding-instructions: true', '---', '', text.replace(/\n*$/, ''), ''].join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The frontmatter keys Gemini CLI documents for a custom subagent (https://geminicli.com/docs/core/subagents, accessed 2026-10-06).
|
|
134
|
+
* Its agent schema is strict, so a spec key outside this list (DoFlow's `effort`) would be rejected. */
|
|
135
|
+
const GEMINI_AGENT_KEYS = new Set(['name', 'description', 'kind', 'tools', 'mcpServers', 'model', 'temperature', 'max_turns', 'timeout_mins']);
|
|
136
|
+
|
|
137
|
+
/** Keep only Gemini's documented top-level frontmatter keys, with their indented continuation
|
|
138
|
+
* lines, in source order. The body after the closing `---` passes through byte for byte, and a
|
|
139
|
+
* file without frontmatter is returned unchanged. */
|
|
140
|
+
function filterGeminiAgentFrontmatter(text: string): string {
|
|
141
|
+
const match = text.match(/^---\r?\n([\s\S]*?\r?\n)---(?=\r?\n|$)/);
|
|
142
|
+
if (!match) return text;
|
|
143
|
+
let keep = false;
|
|
144
|
+
const kept = match[1].split(/(?<=\n)/).filter((line: string) => {
|
|
145
|
+
const key = line.match(/^([A-Za-z][A-Za-z0-9_-]*):/);
|
|
146
|
+
if (key) keep = GEMINI_AGENT_KEYS.has(key[1]);
|
|
147
|
+
return keep;
|
|
148
|
+
});
|
|
149
|
+
return `${text.slice(0, text.indexOf('\n') + 1)}${kept.join('')}${text.slice(match[0].length - 3)}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The command a Kiro hook runs for one front-door script: the project copy when the git root
|
|
153
|
+
* holds one, else the user-scope copy under $HOME. One builder, so the static `doflow` agent
|
|
154
|
+
* (core/harnesses/kiro/agents/doflow.json) and every generated specialist share the exact string. */
|
|
155
|
+
function kiroHookCommand(script: string): string {
|
|
156
|
+
return `bash "$(root=$(git rev-parse --show-toplevel 2>/dev/null) && [ -f "$root/.kiro/hooks/${script}" ] && printf '%s/.kiro/hooks/${script}' "$root" || printf '%s/.kiro/hooks/${script}' "$HOME")"`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** One kiro-cli agent hook entry. */
|
|
160
|
+
function kiroHook({ name, trigger, matcher, script, timeout }: { name: string, trigger: string, matcher?: string, script: string, timeout: number }): any {
|
|
161
|
+
return {
|
|
162
|
+
name, trigger, ...(matcher ? { matcher } : {}),
|
|
163
|
+
action: { type: 'command', command: kiroHookCommand(script) }, timeout,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The three preToolUse hooks every DoFlow-rendered Kiro agent carries. The MCP guard uses the
|
|
168
|
+
* "*" matcher: Kiro documents "@<server>" (every tool of one server) and "*" but no all-MCP
|
|
169
|
+
* matcher, and mcp-tool-guard.sh exits 0 immediately for any non-MCP tool name, so "*" costs one
|
|
170
|
+
* fast no-op for built-in tools and still covers every MCP server. */
|
|
171
|
+
function kiroPreToolUseHooks(): any[] {
|
|
172
|
+
return [
|
|
173
|
+
kiroHook({ name: 'doflow-pre-implementation-gate', trigger: 'preToolUse', matcher: 'write', script: 'pre-implement-gate.sh', timeout: 10 }),
|
|
174
|
+
kiroHook({ name: 'doflow-pre-bash-guard', trigger: 'preToolUse', matcher: 'shell', script: 'pre-bash-guard.sh', timeout: 10 }),
|
|
175
|
+
kiroHook({ name: 'doflow-mcp-tool-guard', trigger: 'preToolUse', matcher: '*', script: 'mcp-tool-guard.sh', timeout: 10 }),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Map one core/shared agent-spec file onto a kiro-cli custom agent. Spec keys other than
|
|
180
|
+
* name/description (model: inherit, effort) are dropped: Kiro inherits the session model. */
|
|
181
|
+
function renderKiroAgent(text: string): string {
|
|
182
|
+
const fm: Record<string, string> = {};
|
|
183
|
+
let body = text;
|
|
184
|
+
if (text.startsWith('---')) {
|
|
185
|
+
const end = text.indexOf('\n---', 3);
|
|
186
|
+
if (end !== -1) {
|
|
187
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
188
|
+
const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
189
|
+
if (m) fm[m[1]] = m[2].trim().replace(/^"|"$/g, '');
|
|
190
|
+
}
|
|
191
|
+
body = text.slice(end + 4).replace(/^\n+/, '');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const agent = {
|
|
195
|
+
name: fm.name || '', description: fm.description || '',
|
|
196
|
+
prompt: `${body.replace(/\s+$/, '')}\n`,
|
|
197
|
+
tools: ['*'],
|
|
198
|
+
hooks: kiroPreToolUseHooks(),
|
|
199
|
+
};
|
|
200
|
+
return `${JSON.stringify(agent, null, 2)}\n`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const OPENCODE_READONLY_AGENTS = new Set(['spec-analyst', 'system-architect', 'quality-guardian', 'research-writer']);
|
|
204
|
+
|
|
205
|
+
/** Map one core/shared agent-spec file onto OpenCode's documented markdown-agent frontmatter
|
|
206
|
+
* (description / mode / permission; model omitted because OpenCode has no 'inherit'). Unknown
|
|
207
|
+
* spec keys (tools, effort) are dropped rather than passed through, so OpenCode never sees
|
|
208
|
+
* vocabulary it does not define. Body below the frontmatter passes through untouched. */
|
|
209
|
+
function renderOpencodeAgent(text: string): string {
|
|
210
|
+
const fm: Record<string, string> = {};
|
|
211
|
+
let body = text;
|
|
212
|
+
if (text.startsWith('---')) {
|
|
213
|
+
const end = text.indexOf('\n---', 3);
|
|
214
|
+
if (end !== -1) {
|
|
215
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
216
|
+
const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
217
|
+
if (m) fm[m[1]] = m[2].trim().replace(/^"|"$/g, '');
|
|
218
|
+
}
|
|
219
|
+
body = text.slice(end + 4).replace(/^\n+/, '');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const lines = ['---'];
|
|
223
|
+
if (fm.description) lines.push(`description: ${JSON.stringify(fm.description)}`);
|
|
224
|
+
lines.push('mode: subagent');
|
|
225
|
+
if (OPENCODE_READONLY_AGENTS.has(fm.name)) {
|
|
226
|
+
lines.push('permission:', ' edit: deny', ' bash: deny');
|
|
227
|
+
}
|
|
228
|
+
lines.push('---', '', body.replace(/\n*$/, ''), '');
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** List every source file with its would-be destination and content fingerprint. Does not touch
|
|
233
|
+
* the destination tree beyond checking existence. `relPath` is destination-relative and doubles as
|
|
234
|
+
* the ledger identity, so a layout change relocates the recorded resource too — planTree's
|
|
235
|
+
* relocation handling then removes the old path rather than orphaning it. */
|
|
236
|
+
function discoverTree({ sourceDir, destDir, fsImpl = fs, layout, transform }: any): { files: any[] } {
|
|
237
|
+
if (!fsImpl.existsSync(sourceDir)) throw new Error(`copy-tree source is missing: ${sourceDir}`);
|
|
238
|
+
const mapRel = typeof layout === 'function' ? layout : resolveLayout(layout);
|
|
239
|
+
const mapContent = typeof transform === 'function' ? transform : resolveTransform(transform);
|
|
240
|
+
const files = walkRelFiles(sourceDir, fsImpl).map((sourceRel) => {
|
|
241
|
+
const relPath = mapRel(sourceRel);
|
|
242
|
+
const sourceAbs = path.join(sourceDir, sourceRel);
|
|
243
|
+
const destAbs = path.join(destDir, relPath);
|
|
244
|
+
const raw = fsImpl.readFileSync(sourceAbs);
|
|
245
|
+
return { relPath, sourceAbs, destAbs, exists: fsImpl.existsSync(destAbs), fingerprint: sha256(mapContent ? mapContent(sourceRel, raw) : raw) };
|
|
246
|
+
});
|
|
247
|
+
return { files };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A file's identity on disk (device + inode), or null when the path does not resolve to one. */
|
|
251
|
+
function fileId(file: string, fsImpl: any): string | null {
|
|
252
|
+
try {
|
|
253
|
+
const stat = fsImpl.statSync(file, { bigint: true });
|
|
254
|
+
// An fs stand-in that reports no inode identifies nothing, rather than every file as one.
|
|
255
|
+
if (stat?.dev === undefined || stat?.ino === undefined) return null;
|
|
256
|
+
return `${stat.dev}:${stat.ino}`;
|
|
257
|
+
} catch { return null; }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A lookup answering "is `target` the same file on disk as one of `paths`, and which one?".
|
|
262
|
+
* A case-insensitive filesystem (APFS, NTFS by default) resolves `Foo.js` and `foo.js` to one
|
|
263
|
+
* file, so a string compare calls them two and a removal of the old spelling deletes the file just
|
|
264
|
+
* written under the new one. Device + inode is the only answer that holds on every filesystem, and
|
|
265
|
+
* a hard link answers the same way. Built on first use, one stat per path; a path that does not
|
|
266
|
+
* exist matches nothing, and `target` never matches itself.
|
|
267
|
+
*/
|
|
268
|
+
function sameFileIndex(paths: string[], fsImpl: any = fs): (target: string) => string | null {
|
|
269
|
+
let byId: Map<string, string> | undefined;
|
|
270
|
+
return (target: string) => {
|
|
271
|
+
if (byId === undefined) {
|
|
272
|
+
byId = new Map();
|
|
273
|
+
for (const candidate of paths) {
|
|
274
|
+
const id = fileId(candidate, fsImpl);
|
|
275
|
+
if (id !== null && !byId.has(id)) byId.set(id, candidate);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const id = fileId(target, fsImpl);
|
|
279
|
+
const match = id === null ? undefined : byId.get(id);
|
|
280
|
+
return match !== undefined && match !== target ? match : null;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Diff the source tree against `previousResources` (the ledger's prior per-file ownership
|
|
286
|
+
* records: `{relPath, fingerprint}`) to produce create/update/remove changes, refusing to touch
|
|
287
|
+
* any destination file whose on-disk content doesn't match its last-recorded fingerprint (or, for
|
|
288
|
+
* a never-before-owned file, doesn't match the source it would be overwritten with) — the same
|
|
289
|
+
* conflict-safety Codex's adapter already applies to agents and hooks.
|
|
290
|
+
* `operation: 'remove'` skips the source tree entirely and only proposes removals for every
|
|
291
|
+
* previously-owned file, mirroring Codex's `ownedRemovalPlan`.
|
|
292
|
+
* `siblingReplaced` lists the harnesses whose recorded bytes this plan replaces: the file matches
|
|
293
|
+
* neither the source nor this harness's own record, only a sibling's (forced or not).
|
|
294
|
+
*/
|
|
295
|
+
function planTree({ sourceDir, destDir, previousResources = [], operation = 'apply', fsImpl = fs, layout, transform, force = false, keepModified = false, siblingFingerprints = new Map() }: any): any {
|
|
296
|
+
const prevByPath = new Map<string, any>(previousResources.map((resource: any) => [resource.relPath, resource]));
|
|
297
|
+
const changes: any[] = [];
|
|
298
|
+
const conflicts: string[] = [];
|
|
299
|
+
const kept: any[] = [];
|
|
300
|
+
const siblingReplaced = new Set<string>();
|
|
301
|
+
|
|
302
|
+
// Fingerprints the CURRENT source would write, keyed by destination. Resolved lazily and only
|
|
303
|
+
// when a recorded fingerprint has already failed to match, so the common removal path still
|
|
304
|
+
// never reads the source tree. A missing source directory is not an error here — an asset can be
|
|
305
|
+
// removed after its source moved — it simply leaves the recorded fingerprint as the only signal.
|
|
306
|
+
let sourceByDest: Map<string, string> | undefined;
|
|
307
|
+
const sourceFingerprint = (destAbs: string): string | undefined => {
|
|
308
|
+
if (sourceByDest === undefined) {
|
|
309
|
+
sourceByDest = sourceDir && fsImpl.existsSync(sourceDir)
|
|
310
|
+
? new Map<string, string>(discoverTree({ sourceDir, destDir, fsImpl, layout, transform }).files.map((file): [string, string] => [file.destAbs, file.fingerprint]))
|
|
311
|
+
: new Map();
|
|
312
|
+
}
|
|
313
|
+
return sourceByDest.get(destAbs);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Every destination the current source produces, set once the apply path has discovered them.
|
|
317
|
+
let producedSameFile: ((target: string) => string | null) | undefined;
|
|
318
|
+
|
|
319
|
+
const proposeRemoval = (prev: any, { relocated = false }: { relocated?: boolean } = {}) => {
|
|
320
|
+
// prev's OWN recorded location, not the current destDir — an asset whose nativeDir changed
|
|
321
|
+
// since prev was recorded must be removed from where it actually is, not from where it would
|
|
322
|
+
// land today. Absent (pre-this-fix data, or a caller-constructed previousResources entry with
|
|
323
|
+
// no target) falls back to the old destDir-relative computation, unchanged.
|
|
324
|
+
const destAbs = prev.target ?? path.join(destDir, prev.relPath);
|
|
325
|
+
if (!fsImpl.existsSync(destAbs)) return;
|
|
326
|
+
const current = sha256(fsImpl.readFileSync(destAbs));
|
|
327
|
+
// A recorded path that is the same file as one the source produces (a case-only rename on a
|
|
328
|
+
// case-insensitive filesystem) is that produced file under its old spelling: deleting it
|
|
329
|
+
// deletes what this run writes. Only the old row is released; the file stays, and the
|
|
330
|
+
// produced path's row is recorded when the run verifies.
|
|
331
|
+
if (relocated && producedSameFile?.(destAbs)) {
|
|
332
|
+
changes.push({ relPath: prev.relPath, target: destAbs, operation: 'remove', fingerprint: current, kept: true });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Untampered on removal means the same two signals the apply path below already accepts: the
|
|
336
|
+
// bytes this harness last recorded, or the bytes the current source would write. The second
|
|
337
|
+
// one matters because a destination tree can be claimed by several harnesses (scripts.doflow
|
|
338
|
+
// is one `<project>/.doflow/scripts` for claude, codex and gemini), so a sibling's update
|
|
339
|
+
// legitimately rewrites files this harness's rows still describe — the same "a sibling
|
|
340
|
+
// changed bytes my row still describes" case the apply path had to be taught, arriving here
|
|
341
|
+
// as an un-releasable claim instead of a refused install. A hand edit matches neither and is
|
|
342
|
+
// still refused. The OBSERVED fingerprint travels with the change so removeTree's own
|
|
343
|
+
// pre-delete re-check agrees with the decision taken here rather than throwing mid-apply.
|
|
344
|
+
// A third signal, the bytes a sibling harness recorded at this target: in a shared tree the
|
|
345
|
+
// sibling may have written the file last, and calling that a hand edit would keep it under a
|
|
346
|
+
// false label while the lifecycle's runtime prune deletes it on the same proof.
|
|
347
|
+
const modified = current !== prev.fingerprint && current !== sourceFingerprint(destAbs) && !siblingFingerprints.get(destAbs)?.has(current);
|
|
348
|
+
// A row left behind by an apply (the asset moved, or its source file went away) is released,
|
|
349
|
+
// and a hand edit there is the user's, not DoFlow's: with `keepModified` the file stays where
|
|
350
|
+
// it is, force or not, and only the ledger row goes. The change carries `kept` so removeTree
|
|
351
|
+
// skips it and the caller can say so.
|
|
352
|
+
if (relocated && keepModified && modified) {
|
|
353
|
+
kept.push({ relPath: prev.relPath, target: destAbs });
|
|
354
|
+
changes.push({ relPath: prev.relPath, target: destAbs, operation: 'remove', fingerprint: current, kept: true });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!force && modified) {
|
|
358
|
+
conflicts.push(`${prev.relPath} was modified outside DoFlow`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
changes.push({ relPath: prev.relPath, target: destAbs, operation: 'remove', fingerprint: current });
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
if (operation === 'remove') {
|
|
365
|
+
for (const prev of previousResources) proposeRemoval(prev);
|
|
366
|
+
return { changes, conflicts, kept, siblingReplaced: [] };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const { files } = discoverTree({ sourceDir, destDir, fsImpl, layout, transform });
|
|
370
|
+
producedSameFile = sameFileIndex(files.map((file) => file.destAbs), fsImpl);
|
|
371
|
+
const satisfiedAtSameLocation = new Set();
|
|
372
|
+
for (const file of files) {
|
|
373
|
+
const prev = prevByPath.get(file.relPath);
|
|
374
|
+
// prev.target === undefined means "location unknown" (pre-this-fix ledger data, or a
|
|
375
|
+
// caller-constructed previousResources entry) — treat as same-location, matching pre-fix
|
|
376
|
+
// behavior exactly, rather than misreading "no data" as "relocated" and spuriously removing
|
|
377
|
+
// it. Only an explicit, differing target means the asset's nativeDir actually changed.
|
|
378
|
+
const sameLocation = prev !== undefined && (prev.target === undefined || prev.target === file.destAbs);
|
|
379
|
+
if (sameLocation) satisfiedAtSameLocation.add(file.relPath);
|
|
380
|
+
if (file.exists) {
|
|
381
|
+
// A destination file is untampered if it matches the source we are about to write, OR the
|
|
382
|
+
// fingerprint this harness last recorded AT THIS LOCATION. Source-match is checked FIRST and
|
|
383
|
+
// unconditionally: it is the stronger signal, and a present-but-stale `prev` must not shadow
|
|
384
|
+
// it.
|
|
385
|
+
//
|
|
386
|
+
// This matters because guidance.context-layer projects one destination for all three
|
|
387
|
+
// harnesses while ownership is recorded per harness, so a sibling's install legitimately
|
|
388
|
+
// changes bytes that this harness's row still describes. Comparing only against `prev` made
|
|
389
|
+
// that indistinguishable from a hand edit and refused the whole install. Ordering the
|
|
390
|
+
// disjunction this way adds no new notion of safety — it stops a weaker signal from
|
|
391
|
+
// preempting a check that would have passed. Gating the fallback on `sameLocation` (rather
|
|
392
|
+
// than `prev !== undefined` alone, as before) is strictly more precise: a relocated asset's
|
|
393
|
+
// old row describes different bytes at a different path, so it must not be consulted here.
|
|
394
|
+
const current = sha256(fsImpl.readFileSync(file.destAbs));
|
|
395
|
+
const ownRecord = current === file.fingerprint || (sameLocation && current === prev.fingerprint);
|
|
396
|
+
const sibling = ownRecord ? undefined : siblingFingerprints.get(file.destAbs)?.get(current);
|
|
397
|
+
const knownGood = force || ownRecord || sibling !== undefined;
|
|
398
|
+
if (!knownGood) { conflicts.push(`${file.relPath} was modified outside DoFlow`); continue; }
|
|
399
|
+
if (sibling !== undefined) siblingReplaced.add(sibling);
|
|
400
|
+
// A true no-op requires the DESTINATION bytes to equal the incoming source. Comparing only
|
|
401
|
+
// ledger-vs-source fingerprints here (the previous form) let a forced run over a hand-edited
|
|
402
|
+
// destination — force ⇒ knownGood — fall through and silently ignore exactly the drift it
|
|
403
|
+
// was asked to heal.
|
|
404
|
+
if (sameLocation && current === file.fingerprint && prev.fingerprint === file.fingerprint) continue; // unchanged, no-op
|
|
405
|
+
}
|
|
406
|
+
changes.push({ relPath: file.relPath, target: file.destAbs, source: file.sourceAbs,
|
|
407
|
+
operation: sameLocation ? 'update' : 'create', fingerprint: file.fingerprint });
|
|
408
|
+
}
|
|
409
|
+
for (const prev of previousResources) {
|
|
410
|
+
if (!satisfiedAtSameLocation.has(prev.relPath)) proposeRemoval(prev, { relocated: true });
|
|
411
|
+
}
|
|
412
|
+
return { changes, conflicts, kept, siblingReplaced: [...siblingReplaced].sort() };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Write every create/update change. Preserves the source file's mode (so a hook script's +x
|
|
416
|
+
* bit survives the copy without a separate chmod pass) and mtime, matching this codebase's
|
|
417
|
+
* general convention for a lifecycle-owned write (see src/adapters/claude/index.ts's
|
|
418
|
+
* applySettingsAsset for the same pattern applied to a transformed-content file). */
|
|
419
|
+
function applyTree({ changes = [], fsImpl = fs, transform }: any): { applied: number } {
|
|
420
|
+
const mapContent = typeof transform === 'function' ? transform : resolveTransform(transform);
|
|
421
|
+
let applied = 0;
|
|
422
|
+
for (const change of changes) {
|
|
423
|
+
if (change.operation === 'remove') continue;
|
|
424
|
+
fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
|
|
425
|
+
const sourceStat = fsImpl.statSync(change.source);
|
|
426
|
+
if (mapContent) {
|
|
427
|
+
// Re-derive the bytes from the source at write time instead of copying it: the transform,
|
|
428
|
+
// not the source file, is what the recorded fingerprint describes. Keeps recovery retries
|
|
429
|
+
// correct without ever persisting rendered content in state.
|
|
430
|
+
fsImpl.writeFileSync(change.target, mapContent(path.basename(change.source), fsImpl.readFileSync(change.source)));
|
|
431
|
+
fsImpl.chmodSync(change.target, sourceStat.mode & 0o777);
|
|
432
|
+
} else {
|
|
433
|
+
fsImpl.copyFileSync(change.source, change.target);
|
|
434
|
+
fsImpl.chmodSync(change.target, sourceStat.mode & 0o777);
|
|
435
|
+
}
|
|
436
|
+
fsImpl.utimesSync(change.target, Math.floor(sourceStat.atimeMs / 1000), Math.floor(sourceStat.mtimeMs / 1000));
|
|
437
|
+
applied += 1;
|
|
438
|
+
}
|
|
439
|
+
return { applied };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Why a removal the runtime prune planned may no longer run, or null when it still may: the
|
|
443
|
+
* prune's plan-time proof (src/lifecycle/index.ts pruneRetiredRuntimeFiles) re-run right before the
|
|
444
|
+
* unlink, so a directory swapped for a symlink, or a file linked to a shipped one, after the plan was
|
|
445
|
+
* made is not deleted through. `guard` is `{ root, source }`: the tree and the source it projects.
|
|
446
|
+
* One walk of the source and one stat per shipped path per tree per call, cached in `cache`. */
|
|
447
|
+
function pruneGuardRefusal(change: any, cache: Map<string, any>, fsImpl: any): string | null {
|
|
448
|
+
const { root, source } = change.pruneGuard;
|
|
449
|
+
const key = `${root}\0${source}`;
|
|
450
|
+
if (!cache.has(key)) {
|
|
451
|
+
const produced = fsImpl.existsSync(source) ? discoverTree({ sourceDir: source, destDir: root, fsImpl }).files.map((file: any) => file.destAbs) : [];
|
|
452
|
+
cache.set(key, { realRoot: fsImpl.realpathSync(root), sameFile: sameFileIndex(produced, fsImpl) });
|
|
453
|
+
}
|
|
454
|
+
const { realRoot, sameFile } = cache.get(key);
|
|
455
|
+
if (!fsImpl.realpathSync(change.target).startsWith(`${realRoot}${path.sep}`)) return `it resolves outside ${root}`;
|
|
456
|
+
const shipped = sameFile(change.target);
|
|
457
|
+
return shipped ? `it is the same file as ${shipped}, which this version ships` : null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Delete every remove change, re-checking each file's fingerprint immediately before deletion —
|
|
461
|
+
* refuses (throws) rather than silently deleting a file modified since planTree ran. A removal the
|
|
462
|
+
* runtime prune planned (`pruneGuard`) also re-checks its containment and same-file proof there. */
|
|
463
|
+
function removeTree({ changes = [], fsImpl = fs }: any): { removed: number } {
|
|
464
|
+
let removed = 0;
|
|
465
|
+
const guards = new Map<string, any>();
|
|
466
|
+
for (const change of changes) {
|
|
467
|
+
if (change.operation !== 'remove' || change.kept) continue;
|
|
468
|
+
if (!fsImpl.existsSync(change.target)) continue;
|
|
469
|
+
const refusal = change.pruneGuard ? pruneGuardRefusal(change, guards, fsImpl) : null;
|
|
470
|
+
if (refusal) throw new Error(`Refusing to remove copy-tree resource ${change.relPath ?? change.target}: ${refusal}`);
|
|
471
|
+
const current = sha256(fsImpl.readFileSync(change.target));
|
|
472
|
+
if (current !== change.fingerprint) throw new Error(`Refusing to remove modified copy-tree resource: ${change.relPath}`);
|
|
473
|
+
fsImpl.rmSync(change.target, { force: true });
|
|
474
|
+
pruneEmptyAncestors(path.dirname(change.target), { fsImpl });
|
|
475
|
+
removed += 1;
|
|
476
|
+
}
|
|
477
|
+
return { removed };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const ANCHOR_DIRS = new Set(['.agents', '.claude', '.codex', '.copilot', '.doflow', '.gemini', '.github', '.kiro', '.opencode', '.pi']);
|
|
481
|
+
|
|
482
|
+
/** Delete now-empty ancestor directories after a file removal, so a relocated or fully-uninstalled
|
|
483
|
+
* tree doesn't leave a skeleton of empty folders behind. Walks upward only while rmdir succeeds
|
|
484
|
+
* (i.e. the directory is empty, so it holds nothing but what the removal just emptied); stops
|
|
485
|
+
* unconditionally at a directory whose basename is in ANCHOR_DIRS — the dot-folder harness roots,
|
|
486
|
+
* which may hold user content elsewhere. The match is by basename, so a root with another name
|
|
487
|
+
* (`~/.gemini/config`, `~/.pi/agent`, `~/.config/opencode`) is pruned once it is empty; no file is
|
|
488
|
+
* ever lost, since rmdir refuses a folder that holds one. */
|
|
489
|
+
function pruneEmptyAncestors(startDir: string, { fsImpl = fs }: { fsImpl?: any } = {}): void {
|
|
490
|
+
let current = startDir;
|
|
491
|
+
for (;;) {
|
|
492
|
+
const basename = path.basename(current);
|
|
493
|
+
if (!basename || basename === '.' || basename === path.sep || ANCHOR_DIRS.has(basename)) return;
|
|
494
|
+
try { fsImpl.rmdirSync(current); } catch { return; }
|
|
495
|
+
current = path.dirname(current);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Re-derive ownership resources from what's actually on disk right now, for status/verify.
|
|
500
|
+
* `plannedWrites` lists the destinations the run being verified wrote. An absent source file is
|
|
501
|
+
* not reported in general, because a user may delete one, but an absent file this run wrote means
|
|
502
|
+
* the run lost it (a case-only rename once deleted the file it had just written) and is a conflict. */
|
|
503
|
+
function verifyTree({ sourceDir, destDir, fsImpl = fs, layout, transform, plannedWrites = [] }: any): { ok: boolean, resources: any[], conflicts: string[] } {
|
|
504
|
+
const { files } = discoverTree({ sourceDir, destDir, fsImpl, layout, transform });
|
|
505
|
+
const written = new Set(plannedWrites);
|
|
506
|
+
const resources: any[] = [];
|
|
507
|
+
const conflicts: string[] = [];
|
|
508
|
+
for (const file of files) {
|
|
509
|
+
if (!file.exists) {
|
|
510
|
+
if (written.has(file.destAbs)) conflicts.push(`${file.relPath} was written by this run but is missing`);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const current = sha256(fsImpl.readFileSync(file.destAbs));
|
|
514
|
+
if (current !== file.fingerprint) { conflicts.push(`${file.relPath} does not match source`); continue; }
|
|
515
|
+
resources.push({ relPath: file.relPath, target: file.destAbs, fingerprint: file.fingerprint });
|
|
516
|
+
}
|
|
517
|
+
return { ok: conflicts.length === 0, resources, conflicts };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ---- per-adapter copy-tree glue, shared so codex/claude/gemini don't each carry their own copy ----
|
|
521
|
+
|
|
522
|
+
/** Filter a harness's asset list down to the ones this engine handles. */
|
|
523
|
+
function copyTreeAssets(assets: any): any[] {
|
|
524
|
+
return (assets || []).filter((asset: any) => asset?.renderer === 'copy-tree');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Recursively sort object keys before serializing, so two logically-equal objects with
|
|
528
|
+
* differently-ordered keys (e.g. after a settings file is merged and re-merged) fingerprint
|
|
529
|
+
* identically instead of spuriously registering as changed. */
|
|
530
|
+
function stableSort(value: any): any {
|
|
531
|
+
if (Array.isArray(value)) return value.map(stableSort);
|
|
532
|
+
if (!value || typeof value !== 'object') return value;
|
|
533
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableSort(value[key])]));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Content fingerprint every adapter uses to track what it owns: sha256 of a string as-is, or of
|
|
537
|
+
* a stably key-sorted JSON serialization of anything else. */
|
|
538
|
+
function fingerprint(value: any): string {
|
|
539
|
+
return sha256(typeof value === 'string' ? value : JSON.stringify(stableSort(value)));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Read and parse a native JSON file an adapter merges into. Distinguishes "absent" (safe to
|
|
543
|
+
* create) from "present but unparseable" (a conflict to report, never silently overwritten) —
|
|
544
|
+
* every adapter that merges into a native JSON settings/config file needs exactly this. */
|
|
545
|
+
function readJson(file: string, { fsImpl = fs }: { fsImpl?: any } = {}): { exists: boolean, value: any, error: string | null } {
|
|
546
|
+
if (!fsImpl.existsSync(file)) return { exists: false, value: {}, error: null };
|
|
547
|
+
try { return { exists: true, value: JSON.parse(fsImpl.readFileSync(file, 'utf8')), error: null }; }
|
|
548
|
+
catch (error: any) { return { exists: true, value: null, error: `Invalid JSON in ${file}: ${error.message}` }; }
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Resolve an asset's source directory against the repo root, refusing a path that escapes the
|
|
552
|
+
* repository or does not exist. `harnessName` only shapes the thrown error message. */
|
|
553
|
+
function sourceDirFor(asset: any, context: any = {}, fsImpl: any = fs, harnessName: string = 'Adapter'): string {
|
|
554
|
+
if (!asset || typeof asset.source !== 'string') throw new Error(`${harnessName} asset requires a source path`);
|
|
555
|
+
const repoRoot = context.repoRoot ? path.resolve(context.repoRoot) : process.cwd();
|
|
556
|
+
const source = path.resolve(repoRoot, asset.source);
|
|
557
|
+
if (!source.startsWith(`${repoRoot}${path.sep}`) || !fsImpl.existsSync(source)) {
|
|
558
|
+
throw new Error(`${harnessName} asset source is unavailable: ${asset.source}`);
|
|
559
|
+
}
|
|
560
|
+
return source;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Resolve an asset's native destination directory under the harness's already-resolved config dir. */
|
|
564
|
+
function copyTreeDestDir(configDir: string, asset: any): string {
|
|
565
|
+
return path.join(configDir, asset.nativeDir || '');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Destination of a `../.doflow` shared-tree asset at the scope root (`<project>/.doflow`, or
|
|
569
|
+
* `$HOME/.doflow` globally), for harnesses whose tree root sits more than one level under the scope
|
|
570
|
+
* root. Returns null for any other nativeDir, so the caller falls back to `copyTreeDestDir`. */
|
|
571
|
+
function sharedTreeDestDir(rootDir: string, nativeDir: any): string | null {
|
|
572
|
+
if (nativeDir !== '../.doflow' && !String(nativeDir || '').startsWith('../.doflow/')) return null;
|
|
573
|
+
const sharedRoot = path.join(rootDir, '.doflow');
|
|
574
|
+
const dest = path.join(rootDir, '.doflow', nativeDir.slice('../.doflow'.length));
|
|
575
|
+
if (dest !== sharedRoot && !dest.startsWith(`${sharedRoot}${path.sep}`)) {
|
|
576
|
+
throw new Error(`shared-tree nativeDir escapes .doflow: ${nativeDir}`);
|
|
577
|
+
}
|
|
578
|
+
return dest;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** The assets that project a tree several harnesses claim at one scope root: the runtime tree, and
|
|
582
|
+
* the guidance tree. A harness whose
|
|
583
|
+
* install predates its own rows for a shared tree (Pi gained guidance rows in v1.15.0) can only
|
|
584
|
+
* recognise the bytes a sibling recorded. */
|
|
585
|
+
const SHARED_TREE_ASSETS = new Set([
|
|
586
|
+
'scripts.doflow', 'runtime.cli', 'runtime.lib', 'runtime.registry',
|
|
587
|
+
'guidance.context-layer', 'kiro.guidance-tree',
|
|
588
|
+
]);
|
|
589
|
+
|
|
590
|
+
/** For each shared-tree target, the fingerprints other harnesses recorded and which harness
|
|
591
|
+
* recorded each, so an update accepts a runtime or guidance tree a sibling wrote (several harnesses
|
|
592
|
+
* claim one `.doflow` file) while bytes no such row recorded are still refused as a hand edit. */
|
|
593
|
+
function ledgerSiblingFingerprints(resources: any, harness: string): Map<string, Map<string, string>> {
|
|
594
|
+
const byTarget = new Map<string, Map<string, string>>();
|
|
595
|
+
for (const resource of resources || []) {
|
|
596
|
+
if (resource.kind !== 'copy-tree-file' || resource.harness === harness || !SHARED_TREE_ASSETS.has(resource.assetId)) continue;
|
|
597
|
+
if (typeof resource.target !== 'string' || typeof resource.fingerprint !== 'string') continue;
|
|
598
|
+
if (!byTarget.has(resource.target)) byTarget.set(resource.target, new Map());
|
|
599
|
+
const byFingerprint = byTarget.get(resource.target)!;
|
|
600
|
+
if (!byFingerprint.has(resource.fingerprint)) byFingerprint.set(resource.fingerprint, resource.harness);
|
|
601
|
+
}
|
|
602
|
+
return byTarget;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** The one-line plan notice for a plan whose copy-tree results replaced sibling-written files, or
|
|
606
|
+
* an empty list when none did. */
|
|
607
|
+
function siblingReplacedNotices(results: any[]): string[] {
|
|
608
|
+
const harnesses = [...new Set(results.flatMap((result) => result.siblingReplaced))].sort();
|
|
609
|
+
if (!harnesses.length) return [];
|
|
610
|
+
return [`replaced shared files written by ${harnesses.join(', ')}; reinstall ${harnesses.length > 1 ? 'those harnesses' : 'that harness'} to restore them`];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Narrow a harness's flat neutral-resource list to one asset's previously-owned copy-tree files. */
|
|
614
|
+
function ledgerFileResources(resources: any, harness: string, assetId: string): any[] {
|
|
615
|
+
return (resources || [])
|
|
616
|
+
.filter((resource: any) => resource.harness === harness && resource.assetId === assetId && resource.kind === 'copy-tree-file')
|
|
617
|
+
.map((resource: any) => ({ relPath: resource.identity, fingerprint: resource.fingerprint, target: resource.target }));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
module.exports = { kiroHook, kiroHookCommand, kiroPreToolUseHooks, walkRelFiles, discoverTree, sameFileIndex, planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, resolveLayout, LAYOUTS, resolveTransform, TRANSFORMS, fingerprint, pruneEmptyAncestors, readJson, sourceDirFor };
|