@khoavu882/doflow 2.0.0-beta.4 → 2.0.0
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/README.md +12 -11
- package/bin/doflow.js +21 -11
- package/core/.antigravity-plugin/plugin.json +1 -3
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/claude/settings/settings.json +18 -0
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +57 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -1
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
- package/core/harnesses/kiro/hooks/session-start.sh +12 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
- package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
- package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +72 -80
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +60 -33
- package/core/registry/contribution-points.json +32 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1542 -277
- package/core/registry/lifecycle.json +2837 -37
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +276 -0
- package/core/registry/storage.json +701 -30
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -1
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
- package/core/shared/scripts/doflow/bin/doflow-run +154 -35
- package/core/shared/skills/do/SKILL.md +12 -10
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +41 -144
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +24 -23
- package/core/shared/skills/do-diagnose/SKILL.md +14 -4
- package/core/shared/skills/do-document/SKILL.md +11 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +18 -6
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +8 -9
- package/core/shared/skills/do-flow/references/task_classes.md +2 -2
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +7 -9
- package/core/shared/skills/do-plan/SKILL.md +29 -19
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +226 -113
- package/src/adapters/claude/index.js +39 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +40 -5
- package/src/adapters/codex/index.js +100 -22
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +294 -48
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +38 -13
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +120 -0
- package/src/adapters/pi/index.js +48 -16
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +3 -2
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +21 -2
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +3 -2
- package/src/cli/index.js +338 -33
- package/src/cli/runtime-commands.js +243 -52
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +74 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +434 -86
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +686 -32
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +492 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +514 -0
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +27 -12
- package/src/runtime/cli-result.js +11 -4
- package/src/runtime/cli.js +221 -277
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +100 -0
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +1106 -245
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/evidence-ledger.js +41 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +77 -10
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +565 -17
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +277 -22
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +16 -6
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +6 -1
- package/src/runtime/lifecycle/overview.js +24 -1
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +3 -3
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +477 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +127 -16
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +402 -73
- package/src/runtime/retrieval-record.js +293 -0
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +270 -0
- package/src/runtime/roadmap/loop.js +1636 -0
- package/src/runtime/roadmap/merge.js +381 -0
- package/src/runtime/roadmap/rows.js +1113 -0
- package/src/runtime/roadmap/seed.js +951 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +63 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +290 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +47 -11
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +202 -56
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +440 -65
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -9
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +288 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +578 -63
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -5
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -210
- package/src/adapters/gemini/index.js +0 -390
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// SemVer 2.0.0 parsing and precedence, and the package host-range grammar (IC-016).
|
|
4
|
+
// No dependency: DoFlow ships `2.0.0-beta.N`, so prerelease precedence is part of the contract.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const HOST_RANGE_GRAMMAR = 'one or more comparators separated by single spaces, all of which must hold: '
|
|
11
|
+
+ '>=V, <V, =V or a bare V (exact), where V is a full semver 2.0.0 version; '
|
|
12
|
+
+ 'an upper bound (<V, =V or a bare V) is required; no ^, ~, ||, x or *';
|
|
13
|
+
|
|
14
|
+
function parseSemVer(value ) {
|
|
15
|
+
const match = value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
const prerelease = match[4] ? match[4].split('.') : [];
|
|
18
|
+
if (prerelease.some((identifier) => /^\d+$/.test(identifier)
|
|
19
|
+
&& identifier.length > 1 && identifier.startsWith('0'))) return null;
|
|
20
|
+
return { major: match[1], minor: match[2], patch: match[3], prerelease };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareNumeric(left , right ) {
|
|
24
|
+
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
25
|
+
if (left === right) return 0;
|
|
26
|
+
return left < right ? -1 : 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function compareSemVer(leftValue , rightValue ) {
|
|
30
|
+
const left = parseSemVer(leftValue);
|
|
31
|
+
const right = parseSemVer(rightValue);
|
|
32
|
+
if (!left || !right) return null;
|
|
33
|
+
|
|
34
|
+
for (const key of ['major', 'minor', 'patch'] ) {
|
|
35
|
+
const order = compareNumeric(left[key], right[key]);
|
|
36
|
+
if (order !== 0) return order;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0;
|
|
40
|
+
if (left.prerelease.length === 0) return 1;
|
|
41
|
+
if (right.prerelease.length === 0) return -1;
|
|
42
|
+
|
|
43
|
+
const length = Math.min(left.prerelease.length, right.prerelease.length);
|
|
44
|
+
for (let index = 0; index < length; index++) {
|
|
45
|
+
const leftId = left.prerelease[index];
|
|
46
|
+
const rightId = right.prerelease[index];
|
|
47
|
+
const leftNumeric = /^\d+$/.test(leftId);
|
|
48
|
+
const rightNumeric = /^\d+$/.test(rightId);
|
|
49
|
+
if (leftNumeric && rightNumeric) {
|
|
50
|
+
const order = compareNumeric(leftId, rightId);
|
|
51
|
+
if (order !== 0) return order;
|
|
52
|
+
} else if (leftNumeric !== rightNumeric) {
|
|
53
|
+
return leftNumeric ? -1 : 1;
|
|
54
|
+
} else if (leftId !== rightId) {
|
|
55
|
+
return leftId < rightId ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (left.prerelease.length === right.prerelease.length) return 0;
|
|
59
|
+
return left.prerelease.length < right.prerelease.length ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function refused(message ) {
|
|
63
|
+
return { ok: false, error: { code: 'bad-type', message, allowed: HOST_RANGE_GRAMMAR } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Never throws: a refused range comes back as { ok: false, error } for the caller to turn into a finding.
|
|
67
|
+
function parseHostRange(text ) {
|
|
68
|
+
if (typeof text !== 'string') return refused('the host range must be a string');
|
|
69
|
+
const comparators = [];
|
|
70
|
+
for (const token of text.split(' ')) {
|
|
71
|
+
const match = token.match(/^(>=|<|=)?(.+)$/);
|
|
72
|
+
if (!match || parseSemVer(match[2]) === null) {
|
|
73
|
+
return refused(`"${token}" is not a comparator of the host range grammar`);
|
|
74
|
+
}
|
|
75
|
+
comparators.push({ op: (match[1] || '=') , version: match[2] });
|
|
76
|
+
}
|
|
77
|
+
if (!comparators.some((comparator) => comparator.op !== '>=')) {
|
|
78
|
+
return refused('the host range has no upper bound');
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, comparators };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `range` is the range text or a parseHostRange result; a refused range or an invalid version satisfies nothing.
|
|
84
|
+
function satisfiesHostRange(version , range ) {
|
|
85
|
+
const parsed = typeof range === 'string' ? parseHostRange(range) : range;
|
|
86
|
+
if (!parsed.ok) return false;
|
|
87
|
+
return parsed.comparators.every(({ op, version: bound }) => {
|
|
88
|
+
const order = compareSemVer(version, bound);
|
|
89
|
+
if (order === null) return false;
|
|
90
|
+
return op === '>=' ? order >= 0 : op === '<' ? order < 0 : order === 0;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { parseSemVer, compareSemVer, parseHostRange, satisfiesHostRange, HOST_RANGE_GRAMMAR };
|
package/src/install/backup.js
CHANGED
|
@@ -24,7 +24,7 @@ const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
|
24
24
|
const {
|
|
25
25
|
legacyBackupReadRoot, isLegacyBackupReadRoot, scopeRootFromCanonicalBackupRoot,
|
|
26
26
|
} = require('./paths.js');
|
|
27
|
-
const { validateLedger } = require('../state/index.js');
|
|
27
|
+
const { validateLedger, ownershipKey } = require('../state/index.js');
|
|
28
28
|
|
|
29
29
|
/** Where a listed/restorable backup was found. 'current' = the canonical `.doflow/backups` root the
|
|
30
30
|
* installer writes to; 'legacy' = the pre-7de6d5f `.claude/backups` root, readable only. Callers
|
|
@@ -69,6 +69,8 @@ function backupId(op , date ) {
|
|
|
69
69
|
const MANIFEST_FILE = '.manifest.json';
|
|
70
70
|
const OWNER_FILE = '.owner.json';
|
|
71
71
|
const TEMP_PREFIX = '.tmp-';
|
|
72
|
+
/** A backup retention is removing: renamed here first so a half-removed backup is never listed or counted. */
|
|
73
|
+
const REMOVING_PREFIX = '.tmp-del-';
|
|
72
74
|
const STALE_TEMP_MS = 24 * 60 * 60 * 1000;
|
|
73
75
|
const MAX_ID_ATTEMPTS = 1000;
|
|
74
76
|
const RENAME_COLLISION_CODES = new Set(['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES']);
|
|
@@ -129,7 +131,9 @@ function createFileBackup({
|
|
|
129
131
|
fsImpl.mkdirSync(backupRoot, { recursive: true });
|
|
130
132
|
fsImpl.mkdirSync(tmpDir);
|
|
131
133
|
tmpCreated = true;
|
|
132
|
-
|
|
134
|
+
// When this run started, not `date`: that is the time the backup is named for, and a caller may pass any
|
|
135
|
+
// date. isStaleTemp measures a temp's age from this field.
|
|
136
|
+
const owner = { pid: process.pid, hostname: os.hostname(), startedAt: new Date().toISOString() };
|
|
133
137
|
fsImpl.writeFileSync(path.join(tmpDir, OWNER_FILE), `${JSON.stringify(owner)}\n`, { flag: 'wx' });
|
|
134
138
|
} catch (err) { throw writeFailure(err); }
|
|
135
139
|
|
|
@@ -234,6 +238,35 @@ function defaultIsAlive(pid ) {
|
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
|
|
241
|
+
/**
|
|
242
|
+
* The id of this process's pid namespace where the platform exposes one (Linux: the target of
|
|
243
|
+
* `/proc/self/ns/pid`, such as `pid:[4026531836]`), else the empty string. A pid means something only
|
|
244
|
+
* inside the namespace that issued it; two containers can share a hostname and not a pid space.
|
|
245
|
+
*/
|
|
246
|
+
function pidNamespaceId() {
|
|
247
|
+
try { return fs.readlinkSync('/proc/self/ns/pid'); } catch { return ''; }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Eight hex digits that identify the host and pid namespace in a rename-aside name. A pid means
|
|
252
|
+
* something only in the namespace it was issued in, so an aside is judged by pid only by a process
|
|
253
|
+
* in the same namespace on the same host. Where no namespace id exists the tag is the hostname alone.
|
|
254
|
+
*/
|
|
255
|
+
function hostTag(hostname = os.hostname(), pidNamespace = pidNamespaceId()) {
|
|
256
|
+
return crypto.createHash('sha1').update(`${hostname}\0${pidNamespace}`).digest('hex').slice(0, 8);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The owner a rename-aside folder (`.tmp-del-<id>-<host tag>-<pid>-<8 hex>`) names, or null when the
|
|
261
|
+
* name does not carry one. Ids contain `-`, so the fields are read from the end. Names without a host
|
|
262
|
+
* tag (an earlier layout that never shipped: it existed only on unreleased commits, so no migration)
|
|
263
|
+
* are unparseable and are kept.
|
|
264
|
+
*/
|
|
265
|
+
function removingOwner(name ) {
|
|
266
|
+
const m = /-([0-9a-f]{8})-([0-9]+)-[0-9a-f]{8}$/.exec(name);
|
|
267
|
+
return m ? { host: m[1], pid: Number(m[2]) } : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
237
270
|
function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } ) {
|
|
238
271
|
let owner = null;
|
|
239
272
|
try { owner = JSON.parse(fsImpl.readFileSync(path.join(dir, OWNER_FILE), 'utf8')); } catch { /* no owner */ }
|
|
@@ -247,15 +280,25 @@ function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } )
|
|
|
247
280
|
/**
|
|
248
281
|
* Remove `.tmp-*` directories left in `backupRoot` by runs that died: the owner process is gone on
|
|
249
282
|
* this host, or the temp is older than 24 hours. Returns how many were removed.
|
|
283
|
+
*
|
|
284
|
+
* A `.tmp-del-*` rename-aside is a backup a retention pass is deleting, so age says nothing about it
|
|
285
|
+
* (it keeps the backup's own, possibly old, mtime and may be rolled back if the delete fails). It is
|
|
286
|
+
* removed only by the host and pid namespace named in it, and only when the pid in its name is not
|
|
287
|
+
* alive; any other host's or namespace's aside, and one whose name carries no owner, is left alone.
|
|
250
288
|
*/
|
|
251
|
-
function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname() } = {}) {
|
|
289
|
+
function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname(), pidNamespace = pidNamespaceId() } = {}) {
|
|
252
290
|
assertMutableBackupRoot(backupRoot, 'clean up backups');
|
|
253
291
|
if (!fsImpl.existsSync(backupRoot)) return 0;
|
|
254
292
|
let removed = 0;
|
|
255
293
|
for (const e of fsImpl.readdirSync(backupRoot, { withFileTypes: true })) {
|
|
256
294
|
if (!e.isDirectory() || !e.name.startsWith(TEMP_PREFIX)) continue;
|
|
257
295
|
const dir = path.join(backupRoot, e.name);
|
|
258
|
-
if (
|
|
296
|
+
if (e.name.startsWith(REMOVING_PREFIX)) {
|
|
297
|
+
// Removed only by the host that made it, and only once its pid is gone there. Another host's
|
|
298
|
+
// (or an unparseable) aside is kept: its pass may be live where this one cannot see it.
|
|
299
|
+
const owner = removingOwner(e.name);
|
|
300
|
+
if (owner === null || owner.host !== hostTag(hostname, pidNamespace) || isAlive(owner.pid)) continue;
|
|
301
|
+
} else if (!isStaleTemp(dir, { now, fsImpl, isAlive, hostname })) continue;
|
|
259
302
|
try {
|
|
260
303
|
fsImpl.rmSync(dir, { recursive: true, force: true });
|
|
261
304
|
removed += 1;
|
|
@@ -347,7 +390,7 @@ function formatBytes(n ) {
|
|
|
347
390
|
|
|
348
391
|
/** Security guard: a backup id is a directory *name*, never a path — reject traversal/absolute ids. */
|
|
349
392
|
function assertSafeBackupId(bid ) {
|
|
350
|
-
if (!bid || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
|
|
393
|
+
if (!bid || bid === '.' || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
|
|
351
394
|
throw new Error(`Invalid backup id: '${bid}'`);
|
|
352
395
|
}
|
|
353
396
|
}
|
|
@@ -441,6 +484,9 @@ function refusalOf(entry , target , { scope, scopeRoot, bkDir, fsImpl
|
|
|
441
484
|
} catch {
|
|
442
485
|
return 'content missing from backup';
|
|
443
486
|
}
|
|
487
|
+
// A restore chmods the file it writes to this mode, so a manifest that lacks one (or holds anything
|
|
488
|
+
// but a permission mask) is refused here, with a name, rather than failing inside the write.
|
|
489
|
+
if (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777) return 'invalid mode';
|
|
444
490
|
return null;
|
|
445
491
|
}
|
|
446
492
|
|
|
@@ -473,6 +519,8 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
473
519
|
untargeted: 0,
|
|
474
520
|
snapshot: [],
|
|
475
521
|
v1: null,
|
|
522
|
+
// --target narrows the ledger with the files: the retired-harness rule of mergeRestoredLedger is off.
|
|
523
|
+
narrowed: Boolean(explicitTargets),
|
|
476
524
|
};
|
|
477
525
|
const wanted = new Set(targets);
|
|
478
526
|
|
|
@@ -535,13 +583,25 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
535
583
|
* the backup does not hold and rows of MCP entries (a file holding only MCP entries is never backed
|
|
536
584
|
* up) stay as they are; restoring the whole old ledger would make DoFlow forget files and entries it
|
|
537
585
|
* still has on disk. With no current ledger the backup's is taken whole.
|
|
586
|
+
*
|
|
587
|
+
* A harness in `retired` (retired in the registry now, row 127) is the exception: every row of it
|
|
588
|
+
* and its `targets` entry come back as the backup holds them. A sweep that retired it released some
|
|
589
|
+
* rows with no file effect (a file a live harness still claims), so no file of theirs is in the backup,
|
|
590
|
+
* yet the run dropped them. Only the sweep ever changes a retired row, so the backup's copy is never
|
|
591
|
+
* staler than the current one; a current row of it the backup lacks stays. A restored row whose file
|
|
592
|
+
* a live harness now claims is released by the next sweep (claim rule 2), file untouched.
|
|
538
593
|
*/
|
|
539
|
-
function mergeRestoredLedger(backup , current , restored ) {
|
|
594
|
+
function mergeRestoredLedger(backup , current , restored , retired = new Set()) {
|
|
540
595
|
if (!current) return backup;
|
|
541
596
|
const at = (target ) => typeof target === 'string' && restored.has(path.resolve(target));
|
|
597
|
+
const whole = (row ) => retired.has(row?.harness);
|
|
598
|
+
const back = backup.resources.filter((r ) => at(r.target) || whole(r));
|
|
599
|
+
const backKeys = new Set(back.filter(whole).map(ownershipKey));
|
|
600
|
+
const targets = Object.entries(backup.targets ?? {}).filter(([id]) => retired.has(id));
|
|
542
601
|
return {
|
|
543
602
|
...current,
|
|
544
|
-
|
|
603
|
+
...(targets.length ? { targets: { ...current.targets, ...Object.fromEntries(targets) } } : {}),
|
|
604
|
+
resources: [...current.resources.filter((r ) => !at(r.target) && !(whole(r) && backKeys.has(ownershipKey(r)))), ...back],
|
|
545
605
|
tombstones: [
|
|
546
606
|
...(current.tombstones ?? []).filter((t ) => !at(t.fromTarget)),
|
|
547
607
|
...(backup.tombstones ?? []).filter((t ) => at(t.fromTarget)),
|
|
@@ -549,21 +609,32 @@ function mergeRestoredLedger(backup , current , restored )
|
|
|
549
609
|
};
|
|
550
610
|
}
|
|
551
611
|
|
|
612
|
+
/** Ids the registry retires (row 127 IC-001): `registry` when a caller passes one, else the shipped
|
|
613
|
+
* registry. One that cannot be read retires nothing, which leaves the merge per restored file. */
|
|
614
|
+
function retiredIds(registry ) {
|
|
615
|
+
if (registry === undefined) {
|
|
616
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
617
|
+
try { registry = require('../registry/index.js').loadRegistry({ repoRoot: REPO_ROOT }); } catch { registry = null; }
|
|
618
|
+
}
|
|
619
|
+
return new Set((Array.isArray(registry?.retired) ? registry.retired : []).map((entry ) => entry?.id).filter((id ) => typeof id === 'string'));
|
|
620
|
+
}
|
|
621
|
+
|
|
552
622
|
/** The bytes a ledger restore item writes: the backup's ledger merged onto the current one. */
|
|
553
|
-
function restoredLedgerBytes(item , restored , fsImpl ) {
|
|
623
|
+
function restoredLedgerBytes(item , restored , retired , fsImpl ) {
|
|
554
624
|
const backup = validateLedger(JSON.parse(fsImpl.readFileSync(item.stored, 'utf8')));
|
|
555
625
|
const current = fsImpl.existsSync(item.path) ? validateLedger(JSON.parse(fsImpl.readFileSync(item.path, 'utf8'))) : null;
|
|
556
|
-
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored)), null, 2)}\n`);
|
|
626
|
+
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored, retired)), null, 2)}\n`);
|
|
557
627
|
}
|
|
558
628
|
|
|
559
629
|
/**
|
|
560
630
|
* Carry out a plan from planRestore. Format 2 writes each restore item through a temp file renamed
|
|
561
631
|
* over the target (or straight through a symlinked target, which stays a link); a failed item is
|
|
562
632
|
* reported and the rest continue. The ownership ledger goes last and restores only the rows of the
|
|
563
|
-
* files written back before it
|
|
633
|
+
* files written back before it, plus every row of a retired harness unless --target narrowed the
|
|
634
|
+
* restore (mergeRestoredLedger); `registry` is for tests, omitted the shipped one is read. It never deletes a file and never writes
|
|
564
635
|
* outside `plan.restore`. A 1.19 plan goes through restoreBackup unchanged.
|
|
565
636
|
*/
|
|
566
|
-
function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs } ) {
|
|
637
|
+
function executeRestore(plan , { backupRoot, dryRun = false, registry, fsImpl = fs } ) {
|
|
567
638
|
if (plan.format !== 2) {
|
|
568
639
|
const toolDirs = Object.fromEntries(plan.v1.tools.map((t ) => [t.tool, t.dstDir]));
|
|
569
640
|
restoreBackup({ bid: plan.bid, backupRoot, dirs: toolDirs, dryRun });
|
|
@@ -583,7 +654,7 @@ function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs }
|
|
|
583
654
|
try {
|
|
584
655
|
const dir = path.dirname(item.path);
|
|
585
656
|
fsImpl.mkdirSync(dir, { recursive: true });
|
|
586
|
-
const bytes = item.ledger ? restoredLedgerBytes(item, files, fsImpl) : fsImpl.readFileSync(item.stored);
|
|
657
|
+
const bytes = item.ledger ? restoredLedgerBytes(item, files, plan.narrowed ? new Set() : retiredIds(registry), fsImpl) : fsImpl.readFileSync(item.stored);
|
|
587
658
|
let isLink = false;
|
|
588
659
|
try {
|
|
589
660
|
isLink = fsImpl.lstatSync(item.path).isSymbolicLink();
|
|
@@ -685,7 +756,12 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
|
|
|
685
756
|
const protectedCandidates = candidates.filter((c) => protectedIds.has(c.id));
|
|
686
757
|
const ordered = [...protectedCandidates, ...candidates.filter((c) => !protectedIds.has(c.id))];
|
|
687
758
|
const keepCount = keep === 0 ? ordered.length : Math.max(keep - reserve, protectedCandidates.length);
|
|
688
|
-
|
|
759
|
+
// The protected backup is the run's own, so one newer than it was made by another run that finished
|
|
760
|
+
// after it; this pass only reaps history older than its own backup.
|
|
761
|
+
const newestProtected = protectedCandidates[0];
|
|
762
|
+
const removal = ordered.slice(keepCount)
|
|
763
|
+
.filter((c) => !newestProtected || compareSortKeys(c, newestProtected) >= 0)
|
|
764
|
+
.map((c) => c.id);
|
|
689
765
|
|
|
690
766
|
if (dryRun || keep === 0) {
|
|
691
767
|
if (dryRun) result.wouldRemove = removal;
|
|
@@ -693,16 +769,30 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
|
|
|
693
769
|
return result;
|
|
694
770
|
}
|
|
695
771
|
|
|
772
|
+
// Also deletes what an earlier pass renamed aside and died before deleting (a `.tmp-del-*` whose pid
|
|
773
|
+
// is dead): already out of every listing, so it goes at once. One whose pid is alive belongs to a
|
|
774
|
+
// pass that may still roll it back, so it is never touched here, whatever its age.
|
|
696
775
|
result.sweptTemps = sweepStaleTemps(backupRoot, { now, fsImpl });
|
|
697
776
|
for (const id of removal) {
|
|
698
777
|
const dir = path.join(backupRoot, id);
|
|
699
778
|
// Another run's retention pass may have removed it already.
|
|
700
779
|
if (!fsImpl.existsSync(dir)) continue;
|
|
780
|
+
// One rename takes the backup out of the listing whole; the deletion then happens out of sight.
|
|
781
|
+
const aside = path.join(backupRoot, `${REMOVING_PREFIX}${id}-${hostTag()}-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
701
782
|
try {
|
|
702
|
-
fsImpl.
|
|
783
|
+
fsImpl.renameSync(dir, aside);
|
|
784
|
+
} catch (err ) {
|
|
785
|
+
if (err.code !== 'ENOENT') result.failed.push({ id, error: err.message });
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
fsImpl.rmSync(aside, { recursive: true, force: true });
|
|
703
790
|
result.removed.push(id);
|
|
704
791
|
} catch (err ) {
|
|
792
|
+
// Not removed: put it back under its id so it is still counted and listed, as when it was
|
|
793
|
+
// removed in place. If that fails too the aside folder is not listed and the next pass deletes it.
|
|
705
794
|
result.failed.push({ id, error: err.message });
|
|
795
|
+
try { fsImpl.renameSync(aside, dir); } catch { /* the next pass cleans the aside folder */ }
|
|
706
796
|
}
|
|
707
797
|
}
|
|
708
798
|
result.kept = ordered.filter((c) => fsImpl.existsSync(path.join(backupRoot, c.id))).length;
|
|
@@ -718,7 +808,7 @@ function retentionNote(prune , keep ) {
|
|
|
718
808
|
|
|
719
809
|
module.exports = {
|
|
720
810
|
backupId, restoreBackup, listBackups, assertSafeBackupId, scopeRelative, retentionNote,
|
|
721
|
-
backupReadRoots, BACKUP_ORIGIN_CURRENT, BACKUP_ORIGIN_LEGACY,
|
|
811
|
+
backupReadRoots, hostTag, BACKUP_ORIGIN_CURRENT, BACKUP_ORIGIN_LEGACY,
|
|
722
812
|
BackupError, DEFAULT_BACKUP_RETENTION, createFileBackup, sweepStaleTemps, classifyBackupDir,
|
|
723
813
|
sortKey, backupSize, formatBytes, planRestore, executeRestore, mergeRestoredLedger, applyRetention,
|
|
724
814
|
};
|
package/src/install/context.js
CHANGED
|
@@ -46,7 +46,7 @@ function printContext(ctx ) {
|
|
|
46
46
|
'[CONTEXT] Resolved install context',
|
|
47
47
|
` scope : ${ctx.scope} (root: ${ctx.scopeRoot})`,
|
|
48
48
|
` targets : ${ctx.targets.join(', ')}`,
|
|
49
|
-
...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t]}`),
|
|
49
|
+
...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t] ?? '(resolved by adapter)'}`),
|
|
50
50
|
` repo root : ${ctx.repoRoot}`,
|
|
51
51
|
` source commit : ${ctx.sourceCommit}`,
|
|
52
52
|
` node : ${ctx.nodeVersion}`,
|
package/src/install/targets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// targets.js — target validation + install-dir resolution.
|
|
3
|
-
// PARITY: sync.sh is global-only — $HOME/.{claude,codex
|
|
3
|
+
// PARITY: sync.sh is global-only — $HOME/.{claude,codex}, hardcoded, NOT CLAUDE_CONFIG_DIR.
|
|
4
4
|
// doflow extends this with a second, DoFlow-only scope: project-local install (this repo's own
|
|
5
5
|
// .claude/ is a real precedent — Claude Code, Codex, and Copilot all support project-scoped config
|
|
6
6
|
// alongside global, see agent-docs/research/context-setup-claude-codex-copilot.md).
|
|
@@ -15,25 +15,43 @@
|
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
|
|
18
|
-
const VALID = ['claude', 'codex', '
|
|
18
|
+
const VALID = ['claude', 'codex', 'copilot', 'kiro', 'opencode', 'pi', 'antigravity'];
|
|
19
19
|
|
|
20
20
|
/** The target used when `--target` is omitted. Deliberately one harness, not all of VALID: an
|
|
21
21
|
* install with no flag should touch the one tool the user almost certainly has, not write
|
|
22
22
|
* configuration into every harness DoFlow knows about — several of which may not be installed at
|
|
23
23
|
* all. That mattered less when VALID was three entries and matters more as it grows. Ask for the
|
|
24
|
-
* rest explicitly with `--target claude,codex,
|
|
24
|
+
* rest explicitly with `--target claude,codex,antigravity`. */
|
|
25
25
|
const DEFAULT_TARGETS = ['claude'];
|
|
26
26
|
|
|
27
27
|
const TARGET_ALIASES = {
|
|
28
28
|
agy: 'antigravity',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/** The registry's retired record for `id`, or null. The registry is read only for an id outside
|
|
32
|
+
* VALID, and a registry that cannot be read leaves the id unknown rather than failing differently. */
|
|
33
|
+
function retiredTarget(id , registry ) {
|
|
34
|
+
const { loadRegistry, retiredFor } = require('../registry/index.js');
|
|
35
|
+
if (!registry) {
|
|
36
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
37
|
+
try { registry = loadRegistry({ repoRoot: REPO_ROOT }); } catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
return retiredFor(registry, id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Default to DEFAULT_TARGETS; validate any explicitly requested. A retired id (row 127 IC-002) is
|
|
43
|
+
* refused like an unknown one, naming its replacement. `registry` is for tests; omitted, the
|
|
44
|
+
* shipped registry is read. */
|
|
45
|
+
function resolveTargets(requested , { registry } = {}) {
|
|
33
46
|
const rawTargets = requested && requested.length ? requested : [...DEFAULT_TARGETS];
|
|
34
47
|
const targets = rawTargets.map((t) => TARGET_ALIASES[t] || t);
|
|
35
48
|
for (const t of targets) {
|
|
36
49
|
if (!VALID.includes(t)) {
|
|
50
|
+
const retired = retiredTarget(t, registry);
|
|
51
|
+
if (retired) {
|
|
52
|
+
const alias = Object.keys(TARGET_ALIASES).find((key) => TARGET_ALIASES[key] === retired.replacement);
|
|
53
|
+
throw new Error(`Target '${t}' was retired on ${retired.retiredOn}; use '${retired.replacement}'${alias ? ` (alias ${alias})` : ''}. Its DoFlow files are released by the next 'doflow install' or 'doflow update' in that scope.`);
|
|
54
|
+
}
|
|
37
55
|
throw new Error(`Unknown target: '${t}' (valid: ${VALID.join(', ')})`);
|
|
38
56
|
}
|
|
39
57
|
}
|
|
@@ -47,16 +65,14 @@ function resolveTargets(requested ) {
|
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* Map tool -> install root.
|
|
50
|
-
* global=true -> $HOME/.{claude,codex,
|
|
51
|
-
* (sync.sh parity for claude/codex
|
|
52
|
-
*
|
|
53
|
-
* global=false (dflt) -> <resolved projectRoot>/.{claude,codex,kiro,opencode,pi}, but .
|
|
54
|
-
*
|
|
55
|
-
*
|
|
68
|
+
* global=true -> $HOME/.{claude,codex,copilot,kiro,pi}, $HOME/.config/opencode
|
|
69
|
+
* (sync.sh parity for claude/codex; the other harnesses follow each one's
|
|
70
|
+
* own documented global root — see below)
|
|
71
|
+
* global=false (dflt) -> <resolved projectRoot>/.{claude,codex,kiro,opencode,pi}, but .github for
|
|
72
|
+
* copilot — deliberately asymmetric with its own global-scope directory, for reasons native to
|
|
73
|
+
* the harness (see per-key notes below). antigravity has no entry: its adapter resolves its
|
|
74
|
+
* own paths from core/registry/harnesses.json.
|
|
56
75
|
*
|
|
57
|
-
* gemini -> project-scoped Gemini config follows the Antigravity customization convention, not
|
|
58
|
-
* `.gemini/`, so it is asymmetric with its own global directory and with the other
|
|
59
|
-
* targets.
|
|
60
76
|
* copilot -> Copilot has no single native root the way Claude's `.claude` is one: instructions,
|
|
61
77
|
* skills, agents, and MCP each live under a different path (see design.md §4's
|
|
62
78
|
* native-surfaces table). `.github` is the project root DoFlow treats as Copilot's
|
|
@@ -67,7 +83,7 @@ function resolveTargets(requested ) {
|
|
|
67
83
|
* the clean, symmetric case.
|
|
68
84
|
* opencode -> Global config is `~/.config/opencode`, NOT `~/.opencode/` — a path DoFlow's own
|
|
69
85
|
* docs asserted incorrectly for several releases (see src/adapters/opencode/index.js).
|
|
70
|
-
* Project scope is `.opencode`, asymmetric with the global path the same way
|
|
86
|
+
* Project scope is `.opencode`, asymmetric with the global path the same way copilot's
|
|
71
87
|
* is.
|
|
72
88
|
* pi -> Global config is `~/.pi/agent` — note the `agent/` segment; `~/.pi/settings.json`
|
|
73
89
|
* is not a location Pi reads. Project scope is `.pi` (see
|
|
@@ -79,7 +95,6 @@ function toolDirs(opts = {}) {
|
|
|
79
95
|
return {
|
|
80
96
|
claude: path.join(root, '.claude'),
|
|
81
97
|
codex: path.join(root, '.codex'),
|
|
82
|
-
gemini: global ? path.join(root, '.gemini') : path.join(root, '.agents'),
|
|
83
98
|
copilot: global ? path.join(root, '.copilot') : path.join(root, '.github'),
|
|
84
99
|
kiro: path.join(root, '.kiro'),
|
|
85
100
|
opencode: global ? path.join(root, '.config', 'opencode') : path.join(root, '.opencode'),
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
|
|
5
|
+
const POLICY_ORDER = Object.freeze(['pre-implementation-gate', 'pre-bash-guard', 'session-context', 'mcp-tool-guard', 'stop-check']);
|
|
6
|
+
const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
|
|
7
|
+
const BEHAVIOUR_ORDER = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
|
|
8
|
+
const NEVER_BLOCKING_EVENTS = new Set([
|
|
9
|
+
'pi\u0000tool_execution_start', 'antigravity\u0000PostToolUse', 'antigravity\u0000PreInvocation',
|
|
10
|
+
'antigravity\u0000PostInvocation', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
|
|
11
|
+
]);
|
|
12
|
+
function stable(value ) {
|
|
13
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
14
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
15
|
+
return JSON.stringify(value);
|
|
16
|
+
}
|
|
17
|
+
function rowsDigest(rows ) { return `sha256:${crypto.createHash('sha256').update(stable(rows)).digest('hex')}`; }
|
|
18
|
+
class UnknownEnforcementTarget extends Error {
|
|
19
|
+
code ;
|
|
20
|
+
constructor(code , detail ) { super(detail); this.name = 'UnknownEnforcementTarget'; this.code = code; }
|
|
21
|
+
}
|
|
22
|
+
function enforcementFor(coverage , query ) {
|
|
23
|
+
const rows = coverage?.rows || [];
|
|
24
|
+
const profiles = coverage?.profiles || {};
|
|
25
|
+
if (query.policy === '*') throw new UnknownEnforcementTarget('unknown-policy', "Unknown policy '*'");
|
|
26
|
+
if (!rows.some((row ) => row.harness === query.harness)) throw new UnknownEnforcementTarget('unknown-harness', `Unknown harness '${query.harness}'`);
|
|
27
|
+
if (!rows.some((row ) => row.policy === query.policy)) throw new UnknownEnforcementTarget('unknown-policy', `Unknown policy '${query.policy}'`);
|
|
28
|
+
if (query.profile !== undefined && !Object.hasOwn(profiles, query.profile)) throw new UnknownEnforcementTarget('unknown-profile', `Unknown profile '${query.profile}'`);
|
|
29
|
+
const pairRows = rows.filter((row ) => row.harness === query.harness && (row.policy === query.policy || row.policy === '*'));
|
|
30
|
+
const timing = query.policy === 'stop-check' ? 'stop' : 'pre-tool';
|
|
31
|
+
const applicable = pairRows.filter((row ) => row.timing === timing && (query.profile === undefined || row.profile === query.profile || row.profile === 'any'));
|
|
32
|
+
const enforced = applicable.length > 0 && applicable.every((row ) => row.wired && row.class === 'blocking' && row.verification === 'verified');
|
|
33
|
+
const rank = (row ) => CLASS_ORDER.indexOf(row.class);
|
|
34
|
+
const weakest = [...applicable].sort((a , b ) => rank(a) - rank(b))[0];
|
|
35
|
+
const intersection = (field ) => applicable.length ? [...new Set (applicable[0][field] || [])].filter((item) => applicable.every((row ) => (row[field] || []).includes(item))) : [];
|
|
36
|
+
const union = (field ) => [...new Set(applicable.flatMap((row ) => row[field] || []))];
|
|
37
|
+
const parity = enforced && pairRows.length > 0 && pairRows.every((row ) => row.class === 'blocking');
|
|
38
|
+
return {
|
|
39
|
+
harness: query.harness, policy: query.policy, profile: query.profile ?? null, enforced, parity,
|
|
40
|
+
class: weakest?.class || 'unknown', verification: weakest?.verification || 'unverified',
|
|
41
|
+
prerequisites: union('prerequisites'), covers: intersection('covers'), doesNotCover: union('doesNotCover'), caveats: union('caveats'),
|
|
42
|
+
rows: applicable, matrix: { schemaVersion: coverage.schemaVersion, revision: coverage.revision, rowsDigest: coverage.rowsDigest },
|
|
43
|
+
reason: weakest ? `${weakest.class} at (${weakest.policy}, ${weakest.harness}, ${weakest.event}, ${weakest.profile})` : `unknown at (${query.policy}, ${query.harness}, *, ${query.profile ?? '*'})`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function renderEnforcementMatrix(coverage ) {
|
|
47
|
+
const order = (items , value ) => {
|
|
48
|
+
const index = items.indexOf(value);
|
|
49
|
+
return index < 0 ? items.length : index;
|
|
50
|
+
};
|
|
51
|
+
const rows = [...coverage.rows].sort((a , b ) => order(POLICY_ORDER, a.policy) - order(POLICY_ORDER, b.policy)
|
|
52
|
+
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
53
|
+
|| a.event.localeCompare(b.event) || a.profile.localeCompare(b.profile));
|
|
54
|
+
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}; ${coverage.rowsDigest}`, '| Policy | Harness | Native event | Profile | Class | Verification | Prerequisites | Does not cover |', '|---|---|---|---|---|---|---|---|'];
|
|
55
|
+
for (const row of rows) lines.push(`| ${row.policy} | ${row.harness} | ${row.event} | ${row.profile} | ${row.class} | ${row.verification} | ${(row.prerequisites || []).join(', ')} | ${(row.doesNotCover || []).join(', ')} |`);
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
// IC-019: one row per behaviour x harness, ordered by IC-018's behaviour list, then HARNESS_ORDER.
|
|
59
|
+
function renderBehaviourMatrix(coverage ) {
|
|
60
|
+
const order = (items , value ) => {
|
|
61
|
+
const index = items.indexOf(value);
|
|
62
|
+
return index < 0 ? items.length : index;
|
|
63
|
+
};
|
|
64
|
+
const entries = [...(coverage.behaviours || [])].sort((a , b ) => order(BEHAVIOUR_ORDER, a.behaviour) - order(BEHAVIOUR_ORDER, b.behaviour)
|
|
65
|
+
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
66
|
+
|| a.behaviour.localeCompare(b.behaviour) || a.harness.localeCompare(b.harness));
|
|
67
|
+
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}`, '| Behaviour | Harness | Status | Limitation or tests |', '|---|---|---|---|'];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const detail = entry.status === 'enforced' ? (entry.tests || []).join(', ') : (entry.limitation || '');
|
|
70
|
+
lines.push(`| ${entry.behaviour} | ${entry.harness} | ${entry.status} | ${detail} |`);
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
module.exports = { enforcementFor, renderEnforcementMatrix, renderBehaviourMatrix, BEHAVIOUR_ORDER, HARNESS_ORDER, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// IC-037 (DEC-034): the shared hooks tree and the hook front doors of every harness that reads it
|
|
6
|
+
// move together. An install or update that writes changed bytes into `hooks.shared` also plans the
|
|
7
|
+
// front doors of the other harnesses installed in the same scope, so a harness the run does not
|
|
8
|
+
// name is never left with doors written for an older tree. The doors ride the same plan, backup
|
|
9
|
+
// and ledger write as the run's own targets, and a door edited outside DoFlow refuses the whole plan.
|
|
10
|
+
const { selectAssets } = require('../registry/index.js');
|
|
11
|
+
const { resolveAdapter, projectAdapterInput } = require('../adapters/index.js');
|
|
12
|
+
const { renderPolicies } = require('./policies.js');
|
|
13
|
+
|
|
14
|
+
const HOOKS_TREE_ASSET = 'hooks.shared';
|
|
15
|
+
const OPERATIONS = ['install', 'update'];
|
|
16
|
+
|
|
17
|
+
/** Whether any target of the plan creates or updates a file of the shared hooks tree. */
|
|
18
|
+
function hooksTreeChanged(targets ) {
|
|
19
|
+
return targets.some((target ) => !target.skipped
|
|
20
|
+
&& target.changes.some((change ) => change.assetId === HOOKS_TREE_ASSET && (change.operation === 'create' || change.operation === 'update')));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The plan entries for the hook front doors of every harness installed in this scope that the run
|
|
24
|
+
* does not name. Only the doors that differ from the current source appear, each with one notice;
|
|
25
|
+
* a door the adapter refuses to overwrite carries the conflict and the command that resolves it.
|
|
26
|
+
* `harnessScope` is the registry's name for the scope. `targetFromResult` is the lifecycle's own
|
|
27
|
+
* conversion of an adapter result into a plan entry. */
|
|
28
|
+
function planHookCompanions({ registry, adapters, scope, harnessScope, scopeRoot, ledger, context, targets, targetFromResult } ) {
|
|
29
|
+
// DEC-034 is about install and update: every other operation plans exactly the targets it was asked for.
|
|
30
|
+
if (!OPERATIONS.includes(context.operation) || !hooksTreeChanged(targets)) return [];
|
|
31
|
+
const named = new Set(targets.map((target ) => target.harness));
|
|
32
|
+
const installed = new Set((ledger?.resources ?? []).map((resource ) => resource.harness));
|
|
33
|
+
const run = targets.filter((target ) => !target.skipped).map((target ) => target.harness).join(',');
|
|
34
|
+
const companions = [];
|
|
35
|
+
for (const harness of registry.harnesses) {
|
|
36
|
+
if (named.has(harness.id) || !installed.has(harness.id) || !harness.scopes.includes(harnessScope)) continue;
|
|
37
|
+
if (!harness.capabilities?.hooks || harness.capabilities.hooks.status === 'unavailable') continue;
|
|
38
|
+
const adapter = resolveAdapter(adapters, harness);
|
|
39
|
+
if (typeof adapter.planHookDoors !== 'function') continue;
|
|
40
|
+
const assets = selectAssets(registry, { harness: harness.id });
|
|
41
|
+
const policies = renderPolicies(registry, { harness: harness.id });
|
|
42
|
+
const adapterInput = projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies, context });
|
|
43
|
+
const input = { ...adapterInput, registry, ledger };
|
|
44
|
+
const discovery = adapter.discover(input);
|
|
45
|
+
const target = targetFromResult({ harness, assets, mcp: [], mcpSelected: null, policies, adapterInput, discovery,
|
|
46
|
+
result: adapter.planHookDoors({ ...input, discovery }) });
|
|
47
|
+
if (!target.changes.length && !target.conflicts.length) continue;
|
|
48
|
+
companions.push({
|
|
49
|
+
...target, hookDoorsOnly: true,
|
|
50
|
+
// The assets that own the doors this run plans, not the harness's whole set.
|
|
51
|
+
assets: target.assets.filter((asset ) => target.changes.some((change ) => change.assetId === asset.id)),
|
|
52
|
+
conflicts: target.conflicts.length
|
|
53
|
+
? [...target.conflicts, `${harness.id} hook front doors must move with the shared hooks tree; run: doflow update -t ${run},${harness.id} (add -f to overwrite edited doors). Working command: doflow update -t ${run},${harness.id} -f`]
|
|
54
|
+
: target.conflicts,
|
|
55
|
+
notices: target.changes.length ? [`refreshed ${harness.id} hook front doors to match the shared hooks tree`, ...target.notices] : target.notices,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return companions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { planHookCompanions };
|