@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
package/src/lifecycle/index.js
CHANGED
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const crypto = require('node:crypto');
|
|
11
|
-
const { harnessFor, selectAssets, selectMcpServers, mcpCapable } = require('../registry/index.js');
|
|
11
|
+
const { harnessFor, selectAssets, selectMcpServers, mcpCapable, isRetired } = require('../registry/index.js');
|
|
12
12
|
const { defaultLedger, ownershipKey, writeLedger, writeRecoveryRecord, upgradeLedger, LEDGER_VERSION } = require('../state/index.js');
|
|
13
13
|
const { resolveAdapter, projectAdapterInput } = require('../adapters/index.js');
|
|
14
14
|
const { pruneEmptyAncestors, discoverTree, sameFileIndex, sharedTreeDestDir, walkRelFiles } = require('../adapters/copy-tree.js');
|
|
15
15
|
const { renderPolicies } = require('./policies.js');
|
|
16
16
|
const { renderMcpIndex } = require('./mcp-index.js');
|
|
17
17
|
const { hasBashCapableShell } = require('./bash-availability.js');
|
|
18
|
-
const {
|
|
18
|
+
const { planHookCompanions } = require('./hook-step.js');
|
|
19
|
+
const { tieredRegistry, hookHoldersFromLedger, hooksTieredOff, withoutHookSources, planHookTierRemovals } = require('./hook-tier.js');
|
|
20
|
+
const { readLock, lockPackages } = require('../state/lockfile.js');
|
|
21
|
+
const { resolveRegistrySource } = require('../adapters/resolve-source.js');
|
|
22
|
+
const { planRetiredHarnesses, planHandovers, handoversFor, applyRetiredHarnesses, retiredReleaseKeys, claimKey } = require('./retired-harnesses.js');
|
|
19
23
|
|
|
20
24
|
const OPERATIONS = new Set(['create', 'merge', 'update', 'remove']);
|
|
21
25
|
/** A harness's MCP selection meaning "the servers it owns now", read through its adapter's
|
|
@@ -198,12 +202,14 @@ function releasedOwnershipKeys(harnessPlans , scope ) {
|
|
|
198
202
|
* applied. Membership is decided per ROW, not per harness: a harness that is releasing its claim
|
|
199
203
|
* on this file is not a claimant even if it is releasing nothing else, and a harness in the same
|
|
200
204
|
* batch that keeps its row (an install that relocates one asset while a sibling's projection is
|
|
201
|
-
* unchanged) still is.
|
|
202
|
-
|
|
205
|
+
* unchanged) still is. A retired harness's row is never a claimant (row 127 IC-003): nothing will
|
|
206
|
+
* ever maintain the file for it, so it must not keep a live harness's removal from reclaiming it. */
|
|
207
|
+
function survivingClaimantsByTarget(ledger , releasedKeys , registry ) {
|
|
203
208
|
const byTarget = new Map();
|
|
204
209
|
for (const resource of ledger?.resources || []) {
|
|
205
210
|
if (typeof resource?.target !== 'string' || typeof resource?.harness !== 'string') continue;
|
|
206
211
|
if (releasedKeys.has(ownershipKey(resource))) continue;
|
|
212
|
+
if (isRetired(registry, resource.harness)) continue;
|
|
207
213
|
const key = claimKey(resource);
|
|
208
214
|
if (!byTarget.has(key)) byTarget.set(key, new Set());
|
|
209
215
|
byTarget.get(key).add(resource.harness);
|
|
@@ -211,16 +217,10 @@ function survivingClaimantsByTarget(ledger , releasedKeys ) {
|
|
|
211
217
|
return byTarget;
|
|
212
218
|
}
|
|
213
219
|
|
|
214
|
-
/** What one claim covers: a whole file, except an MCP entry, which is claimed on its own because
|
|
215
|
-
* several harnesses keep their servers side by side in one MCP file. */
|
|
216
|
-
function claimKey(record ) {
|
|
217
|
-
return record.kind === 'mcp-server' ? `${record.target}\u0000${record.identity}` : record.target;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
220
|
/** NFR-007: a removal reclaims only what no other harness still claims.
|
|
221
221
|
*
|
|
222
222
|
* Several assets project to ONE destination for several harnesses — `scripts.doflow` is a single
|
|
223
|
-
* `<project>/.doflow/scripts` tree for claude
|
|
223
|
+
* `<project>/.doflow/scripts` tree for claude and codex, and antigravity and copilot both
|
|
224
224
|
* resolve to `<root>/.agents` at project scope — while ownership is recorded per harness. A
|
|
225
225
|
* removal that deleted every file its own rows named therefore took the shared runtime out from
|
|
226
226
|
* under the harnesses that are still installed, leaving them with rows pointing at files that no
|
|
@@ -232,8 +232,19 @@ function claimKey(record ) {
|
|
|
232
232
|
* stays, and the last claimant's removal is the one that reclaims it. The neutral ledger is the
|
|
233
233
|
* only thing consulted — it is already the record of who owns what, and a second ownership
|
|
234
234
|
* registry would be one more thing to keep in sync. */
|
|
235
|
-
function markRetainedRemovals(harnessPlans , ledger , scope ) {
|
|
236
|
-
const claimants = survivingClaimantsByTarget(ledger, releasedOwnershipKeys(harnessPlans, scope));
|
|
235
|
+
function markRetainedRemovals(harnessPlans , ledger , scope , registry ) {
|
|
236
|
+
const claimants = survivingClaimantsByTarget(ledger, releasedOwnershipKeys(harnessPlans, scope), registry);
|
|
237
|
+
// A harness this same plan writes a destination for claims it after the run (maint-hooks R-2: the
|
|
238
|
+
// hook tier moves from claude to kiro in one update, so the shared hooks tree must stay standing).
|
|
239
|
+
for (const target of harnessPlans) {
|
|
240
|
+
if (target.skipped) continue;
|
|
241
|
+
for (const change of target.changes || []) {
|
|
242
|
+
if (change.operation === 'remove' || typeof change.target !== 'string' || typeof change.harness !== 'string') continue;
|
|
243
|
+
const key = claimKey(change);
|
|
244
|
+
if (!claimants.has(key)) claimants.set(key, new Set());
|
|
245
|
+
claimants.get(key).add(change.harness);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
237
248
|
if (!claimants.size) return harnessPlans;
|
|
238
249
|
return harnessPlans.map((target ) => {
|
|
239
250
|
if (target.skipped) return target;
|
|
@@ -254,10 +265,11 @@ function markRetainedRemovals(harnessPlans , ledger , scope ) {
|
|
|
254
265
|
}
|
|
255
266
|
|
|
256
267
|
/** Whether this plan leaves no ownership row standing anywhere — the only case in which a shared,
|
|
257
|
-
* install-wide artifact that no single harness owns is genuinely unclaimed.
|
|
258
|
-
|
|
268
|
+
* install-wide artifact that no single harness owns is genuinely unclaimed. A retired harness's row
|
|
269
|
+
* claims nothing (survivingClaimantsByTarget), so it does not count as standing. */
|
|
270
|
+
function removalIsTotal(plan , registry ) {
|
|
259
271
|
const released = releasedOwnershipKeys(plan.targets || [], plan.scope);
|
|
260
|
-
return (plan.ledger?.resources || []).every((resource ) => released.has(ownershipKey(resource)));
|
|
272
|
+
return (plan.ledger?.resources || []).every((resource ) => released.has(ownershipKey(resource)) || isRetired(registry, resource?.harness));
|
|
261
273
|
}
|
|
262
274
|
|
|
263
275
|
/** What a removal kept and why, ready to print. A command that says "removed" while silently
|
|
@@ -307,13 +319,17 @@ function harnessMcpSelection(registry , harness , raw ) {
|
|
|
307
319
|
return [];
|
|
308
320
|
}
|
|
309
321
|
|
|
310
|
-
/** The
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* ships is dead for every harness at once.
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
|
|
322
|
+
/** The shared trees the orphan prune judges, each a group of the assets that project it. Each tree
|
|
323
|
+
* is one source directory projected to one `<scopeRoot>/.doflow/...` tree for every harness, with
|
|
324
|
+
* no layout and no transform (test/guards/runtime-tree.test.js pins that), so a file the current
|
|
325
|
+
* source no longer ships is dead for every harness at once. The guidance tree is two assets: Kiro
|
|
326
|
+
* projects it through kiro.guidance-tree, every other harness through guidance.context-layer. A
|
|
327
|
+
* harness's own native files name only DOFLOW_CORE.md in it, which every source ships. The scripts
|
|
328
|
+
* tree is not here: a harness this run does not update calls those scripts by name from its own
|
|
329
|
+
* native files, so NFR-007 retention is right for it. */
|
|
330
|
+
const PRUNED_TREES = [['runtime.cli'], ['runtime.lib'], ['runtime.registry'], ['guidance.context-layer', 'kiro.guidance-tree']];
|
|
331
|
+
/** Generated in the guidance tree per install, never a ledger row (mcpIndexPath): not an orphan. */
|
|
332
|
+
const PRUNE_EXEMPT = new Set(['MCP_INDEX.md']);
|
|
317
333
|
|
|
318
334
|
function sha256File(file , fsImpl ) {
|
|
319
335
|
return crypto.createHash('sha256').update(fsImpl.readFileSync(file)).digest('hex');
|
|
@@ -363,12 +379,22 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
363
379
|
const recorded = new Set(rows.map((row ) => row.target));
|
|
364
380
|
const shown = (file ) => path.relative(scopeRoot, file);
|
|
365
381
|
|
|
366
|
-
|
|
367
|
-
const
|
|
382
|
+
const sharedDest = (target , asset ) => {
|
|
383
|
+
const nativeDir = typeof asset.nativeDir === 'string' ? asset.nativeDir : asset.nativeDir?.[target.harness];
|
|
384
|
+
return sharedTreeDestDir(scopeRoot, nativeDir);
|
|
385
|
+
};
|
|
386
|
+
for (const group of PRUNED_TREES) {
|
|
387
|
+
// The first planned harness projecting this group into a shared tree (Kiro's guidance.context-layer
|
|
388
|
+
// projects to its own steering directory, so its kiro.guidance-tree is the one that counts).
|
|
389
|
+
let executor = null;
|
|
390
|
+
let asset = null;
|
|
391
|
+
for (const target of planned) {
|
|
392
|
+
asset = target.assets.find((item ) => group.includes(item.id) && sharedDest(target, item));
|
|
393
|
+
if (asset) { executor = target; break; }
|
|
394
|
+
}
|
|
368
395
|
if (!executor) continue;
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
const destDir = sharedTreeDestDir(scopeRoot, nativeDir);
|
|
396
|
+
const assetId = asset.id;
|
|
397
|
+
const destDir = sharedDest(executor, asset);
|
|
372
398
|
const sourceDir = path.resolve(repoRoot, asset.source);
|
|
373
399
|
if (!destDir || !fsImpl.existsSync(sourceDir) || !fsImpl.existsSync(destDir)) continue;
|
|
374
400
|
const produced = discoverTree({ sourceDir, destDir, fsImpl }).files.map((file ) => file.destAbs);
|
|
@@ -385,7 +411,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
385
411
|
const removalsAt = new Map ();
|
|
386
412
|
for (const target of planned) {
|
|
387
413
|
for (const change of target.changes) {
|
|
388
|
-
if (change.operation !== 'remove' || change.assetId
|
|
414
|
+
if (change.operation !== 'remove' || !group.includes(change.assetId) || !isUnder(change.target, destDir) || producedSet.has(change.target)) continue;
|
|
389
415
|
if (!removalsAt.has(change.target)) removalsAt.set(change.target, []);
|
|
390
416
|
removalsAt.get(change.target) .push(change);
|
|
391
417
|
}
|
|
@@ -396,11 +422,19 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
396
422
|
if (!change.retained) replaced.get(change.harness) .set(change, Object.freeze({ ...change, retained: true, retainedFor: [] }));
|
|
397
423
|
}
|
|
398
424
|
};
|
|
425
|
+
// The tree itself, or any directory above it, may be a link out of the scope (a project's
|
|
426
|
+
// .doflow/guidance pointing at ~/.doflow/guidance). realpath resolves every component, so the
|
|
427
|
+
// whole tree is then left alone: nothing in it is pruned, and no planned removal runs.
|
|
428
|
+
if (!isUnder(realRoot, fsImpl.realpathSync(scopeRoot))) {
|
|
429
|
+
for (const file of removalsAt.keys()) keepFile(file);
|
|
430
|
+
notices.get(executor.harness) .push(`kept ${shown(destDir)}: it resolves outside the scope root, so no file in it is pruned`);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
399
433
|
|
|
400
434
|
for (const file of new Set([...rowsAt.keys(), ...removalsAt.keys()])) {
|
|
401
435
|
const fileRows = rowsAt.get(file) ?? [];
|
|
402
436
|
// Another asset's live row on this path is a claim this pass does not judge.
|
|
403
|
-
if (fileRows.some((row ) => row.assetId
|
|
437
|
+
if (fileRows.some((row ) => !group.includes(row.assetId))) continue;
|
|
404
438
|
const removals = removalsAt.get(file) ?? [];
|
|
405
439
|
const identity = path.relative(destDir, file);
|
|
406
440
|
// The rows no planned removal of this file releases on its own.
|
|
@@ -423,14 +457,25 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
423
457
|
}
|
|
424
458
|
if (sameFile(file)) { keepFile(file); continue; } // the produced file under an old spelling
|
|
425
459
|
const current = sha256File(file, fsImpl);
|
|
426
|
-
|
|
460
|
+
// Each row is its harness's latest record of this path, and every row in this scope's ledger
|
|
461
|
+
// counts, not only the run's harnesses. Only bytes equal to ALL of them prove the file is
|
|
462
|
+
// DoFlow's last write: a match with one claimant's older record (a user restoring v1 after
|
|
463
|
+
// another harness wrote v2) authorizes nothing.
|
|
464
|
+
const stale = fileRows.filter((row ) => row.fingerprint !== current);
|
|
465
|
+
if (stale.length === fileRows.length) {
|
|
427
466
|
// A planned removal here was already kept by planTree, and its adapter named it.
|
|
428
467
|
if (!removals.length) notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, changed since DoFlow wrote it`);
|
|
429
468
|
continue;
|
|
430
469
|
}
|
|
470
|
+
if (stale.length) {
|
|
471
|
+
keepFile(file);
|
|
472
|
+
const who = [...new Set(stale.map((row ) => row.harness))].sort().join(', ');
|
|
473
|
+
notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, and its bytes differ from what ${who} last recorded`);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
431
476
|
notices.get(executor.harness) .push(`prune ${shown(file)}: no longer shipped, unchanged since DoFlow wrote it`);
|
|
432
477
|
// removeTree re-runs containment and the same-file check from this right before the unlink.
|
|
433
|
-
const pruneGuard = { root: destDir, source: sourceDir };
|
|
478
|
+
const pruneGuard = { root: destDir, source: sourceDir, scope: scopeRoot };
|
|
434
479
|
if (!removals.length) {
|
|
435
480
|
added.get(executor.harness) .push(synthetic({ fingerprint: current, supersedes: superseded, pruneGuard }));
|
|
436
481
|
continue;
|
|
@@ -445,7 +490,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
445
490
|
// Files no row names and the source does not produce: never DoFlow's to delete, but named.
|
|
446
491
|
for (const rel of walkRelFiles(destDir, fsImpl)) {
|
|
447
492
|
const file = path.join(destDir, rel);
|
|
448
|
-
if (producedSet.has(file) || recorded.has(file) || sameFile(file)) continue;
|
|
493
|
+
if (producedSet.has(file) || recorded.has(file) || sameFile(file) || PRUNE_EXEMPT.has(rel)) continue;
|
|
449
494
|
notices.get(executor.harness) .push(`kept ${shown(file)}: not shipped by this version and not recorded as DoFlow's`);
|
|
450
495
|
}
|
|
451
496
|
}
|
|
@@ -453,11 +498,242 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
453
498
|
: withChanges(target, replaced.get(target.harness) , added.get(target.harness) , notices.get(target.harness) )));
|
|
454
499
|
}
|
|
455
500
|
|
|
501
|
+
/** One harness's plan entry, from what its adapter returned: the changes checked against the
|
|
502
|
+
* harness's assets, and the conflicts, prerequisites and notices in the lifecycle's common shape. */
|
|
503
|
+
function targetFromResult({ harness, assets, mcp, mcpSelected, policies, adapterInput, discovery, result } ) {
|
|
504
|
+
if (!result || typeof result !== 'object') throw new Error(`Adapter '${harness.id}' returned an invalid plan`);
|
|
505
|
+
const changes = (result.changes || []).map((change ) => normalizeChange(change, { harness, assets }));
|
|
506
|
+
const conflicts = adapterConflicts(result, harness);
|
|
507
|
+
const prerequisites = [...(result.prerequisites || []), ...changes.map((change ) => change.prerequisite).filter(Boolean)];
|
|
508
|
+
const requiredNativeResources = result.requiredNativeResources ?? changes;
|
|
509
|
+
if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
|
|
510
|
+
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── package skills and agents (122 IC-017) ──────────────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
/** A package contribution's asset id is `pkg.<qualified id>`; no registry asset id starts with `pkg.`. */
|
|
516
|
+
const PACKAGE_ASSET_PREFIX = 'pkg.';
|
|
517
|
+
const isPackageAsset = (id ) => typeof id === 'string' && id.startsWith(PACKAGE_ASSET_PREFIX);
|
|
518
|
+
|
|
519
|
+
/** IC-017's ownership identity: the harness, the qualified id and the path inside the projected tree. It names no
|
|
520
|
+
* scope and no destination, as G20 requires of every identity. */
|
|
521
|
+
function packageIdentity(harness , assetId , relPath ) {
|
|
522
|
+
return `doflow:${harness}:package:${assetId.slice(PACKAGE_ASSET_PREFIX.length)}:${relPath}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Whether an adapter's conflict line or status belongs to one of `ids` (copy-tree prefixes each with `<asset id>: `). */
|
|
526
|
+
const namesAsset = (reason , ids ) => [...ids].some((id) => String(reason).includes(`${id}: `));
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* The active third-party packages with skill or agent contributions, read from `context.packageView` when given (a
|
|
530
|
+
* test seam, and the verbs' own view) or else from the package view of the global lock under `scopeRoot`. Nothing is
|
|
531
|
+
* read when the lock records no third-party package. The view is taken with no `packages.disabled` applied: harness
|
|
532
|
+
* files are machine-wide, so only the lock bit, the trust grant and the store's integrity decide them.
|
|
533
|
+
*/
|
|
534
|
+
function activePackageEntries(registry , scopeRoot , context ) {
|
|
535
|
+
let view = context.packageView;
|
|
536
|
+
if (!view) {
|
|
537
|
+
let rows ;
|
|
538
|
+
try { rows = lockPackages(readLock({ scope: 'global', homeDir: scopeRoot })); } catch { return []; }
|
|
539
|
+
if (!rows.some((row ) => row?.source?.type !== 'bundled')) return [];
|
|
540
|
+
view = require('../runtime/packages/view.js').packageView({ repoRoot: registry.repoRoot, homeDir: scopeRoot, settingsView: {} });
|
|
541
|
+
}
|
|
542
|
+
const { instructionEntries } = require('../runtime/packages/store.js');
|
|
543
|
+
const out = [];
|
|
544
|
+
for (const pkg of view.packages ?? []) {
|
|
545
|
+
if (pkg.firstParty || pkg.state !== 'active' || typeof pkg.storeDir !== 'string') continue;
|
|
546
|
+
let manifest ;
|
|
547
|
+
try { manifest = JSON.parse(fs.readFileSync(path.join(pkg.storeDir, 'doflow-package.json'), 'utf8')); } catch { continue; }
|
|
548
|
+
const entries = instructionEntries(manifest);
|
|
549
|
+
if (entries.length) out.push({ id: pkg.id, storeDir: pkg.storeDir, entries });
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** The registry-shaped asset for one contribution on one harness, or `{ warning }` when it cannot be projected there.
|
|
555
|
+
* The source resolves inside the store entry with real paths on both sides (`resolveRegistrySource` with a source
|
|
556
|
+
* root); a skill lands in its own directory under the borrowed asset's destination, an agent beside DoFlow's. */
|
|
557
|
+
function packageAsset(registry , harnessId , pkg , entry , realRoot ) {
|
|
558
|
+
const { projectionReach } = require('../runtime/packages/store.js');
|
|
559
|
+
const reach = projectionReach(registry, harnessId, entry.point);
|
|
560
|
+
if (!reach.ok) return { warning: `${harnessId} cannot take ${entry.point} ${entry.qualifiedId}: ${reach.reason}` };
|
|
561
|
+
let real ;
|
|
562
|
+
try { real = resolveRegistrySource(registry, entry.path, `package ${entry.qualifiedId}`, pkg.storeDir); } catch (error ) {
|
|
563
|
+
return { warning: `${entry.point} ${entry.qualifiedId} is not projected: ${error.message}` };
|
|
564
|
+
}
|
|
565
|
+
let sourceDir = real;
|
|
566
|
+
let nativeDir = reach.nativeDir ? `${reach.nativeDir}/${entry.name}` : entry.name;
|
|
567
|
+
if (entry.point === 'agent') {
|
|
568
|
+
// The adapters project a whole source directory, so an agent file is projected only from a directory of its own.
|
|
569
|
+
sourceDir = path.dirname(real);
|
|
570
|
+
nativeDir = reach.nativeDir;
|
|
571
|
+
const others = fs.readdirSync(sourceDir).filter((name ) => name !== path.basename(real));
|
|
572
|
+
if (others.length) return { warning: `agent ${entry.qualifiedId} is not projected: its directory ${path.relative(realRoot, sourceDir) || '.'} holds other entries (${others.slice(0, 3).join(', ')}); give the agent file a directory of its own` };
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
asset: { id: `${PACKAGE_ASSET_PREFIX}${entry.qualifiedId}`, kind: entry.point, source: path.relative(realRoot, sourceDir), ownership: 'managed-tree',
|
|
576
|
+
projection: { [harnessId]: reach.projection }, nativeDir: { [harnessId]: nativeDir } },
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** What one harness's adapter plans for one package's assets alone, with every other component's output dropped and
|
|
581
|
+
* the identities rewritten to IC-017's: `{ part, changes, notices }`, or `{ warnings }` when it cannot be planned. The
|
|
582
|
+
* part (assets, adapter input, discovery) is what verification re-reads after the apply. */
|
|
583
|
+
function planPackagePart({ registry, adapters, harness, scope, scopeRoot, ledger, context, pkg, assets } ) {
|
|
584
|
+
const realRoot = fs.realpathSync(pkg.storeDir);
|
|
585
|
+
// The native projection (codex's TOML agents and hooks) is DoFlow's own, never a package's: it is left out, so the
|
|
586
|
+
// adapter plans the package's tree assets and nothing that reads the DoFlow checkout.
|
|
587
|
+
const adapterInput = projectAdapterInput({ registry, harness: { ...harness, nativeProjection: undefined }, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies: [],
|
|
588
|
+
context: { repoRoot: realRoot, homeDir: context.homeDir ?? scopeRoot, projectRoot: scopeRoot, sourceVersion: context.sourceVersion, operation: context.operation } });
|
|
589
|
+
const adapter = resolveAdapter(adapters, harness);
|
|
590
|
+
const ids = new Set (assets.map((asset ) => asset.id));
|
|
591
|
+
let discovery ;
|
|
592
|
+
let result ;
|
|
593
|
+
try {
|
|
594
|
+
discovery = adapter.discover({ ...adapterInput, registry, ledger });
|
|
595
|
+
result = adapter.plan({ ...adapterInput, registry, ledger, discovery });
|
|
596
|
+
} catch (error ) {
|
|
597
|
+
return { warnings: [`package ${pkg.id} is not projected into ${harness.id}: ${error.message}`] };
|
|
598
|
+
}
|
|
599
|
+
const conflicts = adapterConflicts(result, harness).filter((reason ) => namesAsset(reason, ids));
|
|
600
|
+
if (conflicts.length) return { warnings: conflicts.map((reason ) => `package ${pkg.id} is not projected into ${harness.id}: ${reason}`) };
|
|
601
|
+
const changes = (result.changes || []).filter((change ) => ids.has(change.assetId))
|
|
602
|
+
.map((change ) => normalizeChange({ ...change, ownershipIdentity: packageIdentity(harness.id, change.assetId, change.identity ?? path.basename(change.target)) }, { harness, assets }));
|
|
603
|
+
const notices = adapterNotices(result).filter((notice ) => notice.startsWith('kept hand-edited '));
|
|
604
|
+
return { part: { packageId: pkg.id, assets, adapterInput, discovery }, changes, notices };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** The removals of a package's recorded rows on one harness (IC-017 unprojection), planned from the ledger alone, so a
|
|
608
|
+
* store entry that is gone or changed is no obstacle: a file whose bytes are still DoFlow's is removed, a file a person
|
|
609
|
+
* edited is kept and only its row released (named in a notice), a file already gone only releases its row. The change
|
|
610
|
+
* shape is the one every adapter's copy-tree removal reads (`kind`, `projection.renderer`), as the runtime prune's. */
|
|
611
|
+
function packageRemovals({ harness, scopeRoot, rows } ) {
|
|
612
|
+
const notices = [];
|
|
613
|
+
const changes = rows.map((row ) => {
|
|
614
|
+
const base = { harness: harness.id, assetId: row.assetId, target: row.target, operation: 'remove', kind: 'copy-tree-file', identity: row.identity,
|
|
615
|
+
ownershipIdentity: row.ownershipIdentity, projection: { renderer: 'copy-tree' } };
|
|
616
|
+
if (!fs.existsSync(row.target)) return Object.freeze({ ...base, fingerprint: null, retained: true, retainedFor: [] });
|
|
617
|
+
const current = sha256File(row.target, fs);
|
|
618
|
+
if (current !== row.fingerprint) {
|
|
619
|
+
notices.push(`kept person-edited ${path.relative(scopeRoot, row.target)}; DoFlow no longer manages it`);
|
|
620
|
+
return Object.freeze({ ...base, fingerprint: current, retained: true, retainedFor: [] });
|
|
621
|
+
}
|
|
622
|
+
return Object.freeze({ ...base, fingerprint: current });
|
|
623
|
+
});
|
|
624
|
+
return { changes, notices };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* The package work of a global-scope plan (IC-017), per harness: the changes that project each active package's skills
|
|
629
|
+
* and agents through the harness's own adapter (one `part` per package, for verification), and the removals of every
|
|
630
|
+
* recorded row that no longer belongs to an active, reachable contribution (disabled, revoked, removed, suspended,
|
|
631
|
+
* re-digested, or a `remove` run). A package fault never makes the plan unsafe (FR-009): it becomes a warning, and that
|
|
632
|
+
* package's files on that harness stay as they are. Nothing happens at project scope (NFR-005), and nothing is read
|
|
633
|
+
* when no package is recorded.
|
|
634
|
+
*/
|
|
635
|
+
function planPackageWork({ registry, adapters, scope, scopeRoot, harnesses, ledger, context } ) {
|
|
636
|
+
const byHarness = new Map ();
|
|
637
|
+
const warnings = [];
|
|
638
|
+
if (scope !== 'global' || !harnesses.length) return { byHarness, warnings };
|
|
639
|
+
const recorded = (ledger?.resources ?? []).filter((row ) => isPackageAsset(row.assetId) && typeof row.target === 'string');
|
|
640
|
+
const active = context.operation === 'remove' ? [] : activePackageEntries(registry, scopeRoot, context);
|
|
641
|
+
if (!active.length && !recorded.length) return { byHarness, warnings };
|
|
642
|
+
for (const harness of harnesses) {
|
|
643
|
+
const work = { changes: [] , notices: [] , parts: [] };
|
|
644
|
+
const wanted = new Set ();
|
|
645
|
+
for (const pkg of active) {
|
|
646
|
+
const realRoot = fs.realpathSync(pkg.storeDir);
|
|
647
|
+
const assets = [];
|
|
648
|
+
for (const entry of pkg.entries) {
|
|
649
|
+
const made = packageAsset(registry, harness.id, pkg, entry, realRoot);
|
|
650
|
+
if (made.warning) warnings.push({ harness: harness.id, packageId: pkg.id, warning: made.warning });
|
|
651
|
+
else { assets.push(made.asset); wanted.add(made.asset.id); }
|
|
652
|
+
}
|
|
653
|
+
if (!assets.length) continue;
|
|
654
|
+
const planned = planPackagePart({ registry, adapters, harness, scope, scopeRoot, ledger, context, pkg, assets });
|
|
655
|
+
for (const warning of planned.warnings ?? []) warnings.push({ harness: harness.id, packageId: pkg.id, warning });
|
|
656
|
+
if (!planned.part) continue;
|
|
657
|
+
work.parts.push(planned.part);
|
|
658
|
+
work.changes.push(...planned.changes);
|
|
659
|
+
work.notices.push(...planned.notices);
|
|
660
|
+
}
|
|
661
|
+
const removals = packageRemovals({ harness, scopeRoot, rows: recorded.filter((row ) => row.harness === harness.id && !wanted.has(row.assetId)) });
|
|
662
|
+
work.changes.push(...removals.changes);
|
|
663
|
+
work.notices.push(...removals.notices);
|
|
664
|
+
if (work.parts.length || work.changes.length || work.notices.length) byHarness.set(harness.id, work);
|
|
665
|
+
}
|
|
666
|
+
return { byHarness, warnings };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** A harness's plan entry with its package work added, `requiredNativeResources` kept in step as withChanges does. */
|
|
670
|
+
function withPackageWork(target , work ) {
|
|
671
|
+
const changes = [...target.changes, ...work.changes];
|
|
672
|
+
const requiredNativeResources = target.requiredNativeResources === target.changes ? changes
|
|
673
|
+
: [...target.requiredNativeResources, ...work.changes.map((change ) => ({ harness: change.harness, component: 'package', target: change.target, operation: change.operation, identity: change.identity }))];
|
|
674
|
+
return { ...target, changes, requiredNativeResources, notices: [...target.notices, ...work.notices], packageParts: work.parts };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** The plan entry of a package-only plan for one harness: no DoFlow asset, only the package work. */
|
|
678
|
+
function packageHostTarget({ registry, harness, scope, scopeRoot, context, work } ) {
|
|
679
|
+
const adapterInput = projectAdapterInput({ registry, harness: { ...harness, nativeProjection: undefined }, scope, scopeRoot, assets: [], mcp: [], mcpAdoptable: [], policies: [],
|
|
680
|
+
context: { repoRoot: registry.repoRoot, homeDir: context.homeDir ?? scopeRoot, projectRoot: scopeRoot, operation: context.operation } });
|
|
681
|
+
return { harness: harness.id, adapter: harness.adapter, assets: [], mcp: [], mcpSelected: null, policies: [], adapterInput, discovery: null,
|
|
682
|
+
changes: work.changes, requiredNativeResources: work.changes, conflicts: [], prerequisites: [], notices: work.notices, skipped: false,
|
|
683
|
+
packagesOnly: true, packageParts: work.parts };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** The verification of one package part: the adapter's verify of that package's assets, filtered to them, with the
|
|
687
|
+
* identities rewritten as the plan wrote them. A throw is a conflict, so the run stops before the ledger is written. */
|
|
688
|
+
function verifyPackagePart({ part, adapter, harness, plan, context, operation, plannedWrites } ) {
|
|
689
|
+
const ids = new Set (part.assets.map((asset ) => asset.id));
|
|
690
|
+
let raw ;
|
|
691
|
+
try {
|
|
692
|
+
raw = normalizeVerification(adapter.verify({ ...part.adapterInput, registry: context.registry ?? plan.registry, discovery: part.discovery, ledger: plan.ledger,
|
|
693
|
+
operation, context: { ...part.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
|
|
694
|
+
} catch (error ) {
|
|
695
|
+
return { ok: false, statuses: [], resources: [], conflicts: [`package ${part.packageId}: ${error.message}`] };
|
|
696
|
+
}
|
|
697
|
+
const conflicts = (raw.conflicts || []).filter((reason ) => namesAsset(reason, ids));
|
|
698
|
+
const statuses = raw.statuses.filter((status ) => ids.has(status?.assetId));
|
|
699
|
+
const resources = raw.resources.filter((resource ) => ids.has(resource?.assetId))
|
|
700
|
+
.map((resource ) => ({ ...resource, ownershipIdentity: packageIdentity(harness.id, resource.assetId, resource.identity ?? path.basename(resource.target)) }));
|
|
701
|
+
const failed = statuses.some((status ) => ['missing', 'conflict', 'error', 'invalid'].includes(status?.status));
|
|
702
|
+
return { ok: conflicts.length === 0 && !failed, statuses, resources, conflicts };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** A harness's verification with its package parts' verifications merged in, so each harness keeps one entry. */
|
|
706
|
+
function withPackageVerification(verification , target , options ) {
|
|
707
|
+
let merged = verification;
|
|
708
|
+
for (const part of target.packageParts ?? []) {
|
|
709
|
+
const one = verifyPackagePart({ ...options, part });
|
|
710
|
+
merged = { ...merged, statuses: [...merged.statuses, ...one.statuses], resources: [...merged.resources, ...one.resources],
|
|
711
|
+
conflicts: [...(merged.conflicts || []), ...one.conflicts], ok: merged.ok !== false && one.ok };
|
|
712
|
+
}
|
|
713
|
+
return merged;
|
|
714
|
+
}
|
|
715
|
+
|
|
456
716
|
function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], ledger, context = {} } ) {
|
|
457
717
|
assertScope(scope);
|
|
458
718
|
if (!registry) throw new Error('registry is required');
|
|
459
719
|
const baseLedger = ledger ?? defaultLedger({ scope, scopeRoot });
|
|
460
|
-
|
|
720
|
+
// Hook tiers (maint-hooks): a harness outside `context.hookHarnesses` is planned against a registry
|
|
721
|
+
// in which its hooks capability is unavailable, so it gets content only; the hooks an earlier run
|
|
722
|
+
// wrote for it are removed in the same plan. No list (an in-process caller) keeps every harness's hooks.
|
|
723
|
+
const fullRegistry = registry;
|
|
724
|
+
// A removal takes the hooks the ledger records, whatever the setting says now (maint-hooks).
|
|
725
|
+
registry = tieredRegistry(registry, context.operation === 'remove' ? hookHoldersFromLedger(registry, baseLedger) : context.hookHarnesses);
|
|
726
|
+
const harnesses = normalizeTargets(registry, targets);
|
|
727
|
+
// `packagesOnly` (the package verbs): only the package targets, for a runtime that ships no DoFlow content to plan.
|
|
728
|
+
const packagesOnly = context.packagesOnly === true;
|
|
729
|
+
// Row 127 rule 3a: a retired harness's file its replacement takes over is decided before any live
|
|
730
|
+
// plan, since the replacement plans that one write itself (`context.handovers`, IC-007).
|
|
731
|
+
const sweeping = context.operation !== 'remove' && !packagesOnly;
|
|
732
|
+
const handovers = sweeping
|
|
733
|
+
? planHandovers({ registry, ledger: baseLedger, scope, scopeRoot, planned: harnesses.filter((harness ) => harness.scopes.includes(registryScope(scope))).map((harness ) => harness.id) })
|
|
734
|
+
: new Map();
|
|
735
|
+
const handedTo = handoversFor(handovers);
|
|
736
|
+
const harnessPlans = packagesOnly ? [] : harnesses.map((harness) => {
|
|
461
737
|
if (!harness.scopes.includes(registryScope(scope))) {
|
|
462
738
|
return { harness: harness.id, assets: [], changes: [], conflicts: [`Harness '${harness.id}' does not support ${scope} scope`], prerequisites: [], notices: [], skipped: true, mcpSelected: null };
|
|
463
739
|
}
|
|
@@ -467,7 +743,9 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
467
743
|
const policies = renderPolicies(registry, { harness: harness.id });
|
|
468
744
|
const adoptable = capable ? selectMcpServers(registry, catalogIds(registry, mcpAdoptable[harness.id] ?? [])) : [];
|
|
469
745
|
const adapter = resolveAdapter(adapters, harness);
|
|
470
|
-
const
|
|
746
|
+
const baseContext = handedTo.has(harness.id) ? { ...context, handovers: handedTo.get(harness.id) } : context;
|
|
747
|
+
const harnessContext = hooksTieredOff(harness) ? withoutHookSources(baseContext) : baseContext;
|
|
748
|
+
const project = (mcp ) => projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp, mcpAdoptable: adoptable, policies, context: harnessContext });
|
|
471
749
|
// `keep` asks the adapter what it owns before the real projection: the probe carries no
|
|
472
750
|
// selection, so discovery reads the files as they are.
|
|
473
751
|
const ids = selection === MCP_KEEP
|
|
@@ -478,30 +756,54 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
478
756
|
const input = { ...adapterInput, registry, ledger: baseLedger };
|
|
479
757
|
const discovery = adapter.discover(input);
|
|
480
758
|
const result = adapter.plan({ ...input, discovery });
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const requiredNativeResources = result.requiredNativeResources ?? changes;
|
|
486
|
-
if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
|
|
487
|
-
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
759
|
+
const target = targetFromResult({ harness, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, result });
|
|
760
|
+
return hooksTieredOff(harness)
|
|
761
|
+
? planHookTierRemovals({ target, registry: fullRegistry, adapter, harness: harnessFor(fullRegistry, harness.id), scope, scopeRoot, ledger: baseLedger, context, normalizeChange, adapterConflicts })
|
|
762
|
+
: target;
|
|
488
763
|
});
|
|
489
764
|
// The MCP index names every server a harness of this scope uses: the selections planned here and
|
|
490
765
|
// the recorded selections of the harnesses this run does not touch.
|
|
491
766
|
const selectedMcp = selectMcpServers(registry, catalogIds(registry,
|
|
492
767
|
[...retainedMcpIds, ...harnessPlans.flatMap((item) => (item.skipped ? [] : item.mcpSelected ?? []))]));
|
|
768
|
+
// Package skills and agents ride the same plan, at global scope only (IC-017), for the harnesses it plans; each
|
|
769
|
+
// harness keeps one plan entry, its package work added to it.
|
|
770
|
+
let packages ;
|
|
771
|
+
try {
|
|
772
|
+
packages = planPackageWork({ registry, adapters, scope, scopeRoot, ledger: baseLedger, context,
|
|
773
|
+
harnesses: harnesses.filter((harness ) => harness.scopes.includes(registryScope(scope))) });
|
|
774
|
+
} catch (error ) {
|
|
775
|
+
packages = { byHarness: new Map(), warnings: [{ harness: null, packageId: null, warning: `package skills and agents are not projected: ${error.message}` }] };
|
|
776
|
+
}
|
|
777
|
+
const withPackages = packagesOnly
|
|
778
|
+
? harnesses.filter((harness ) => packages.byHarness.has(harness.id))
|
|
779
|
+
.map((harness ) => packageHostTarget({ registry, harness, scope, scopeRoot, context, work: packages.byHarness.get(harness.id) }))
|
|
780
|
+
: harnessPlans.map((target ) => (!target.skipped && packages.byHarness.has(target.harness) ? withPackageWork(target, packages.byHarness.get(target.harness)) : target));
|
|
493
781
|
// Only once every harness's plan is known: whether a file may be deleted depends on the rows
|
|
494
782
|
// the WHOLE plan leaves standing, which no single harness's plan can see.
|
|
495
|
-
const retained = markRetainedRemovals(
|
|
496
|
-
const annotated = context.operation === 'remove' ? retained
|
|
783
|
+
const retained = markRetainedRemovals(withPackages, baseLedger, scope, registry);
|
|
784
|
+
const annotated = context.operation === 'remove' || packagesOnly ? retained
|
|
497
785
|
: pruneRetiredRuntimeFiles(retained, baseLedger, { scopeRoot, repoRoot: context.repoRoot });
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
786
|
+
// The shared hooks tree may have moved under harnesses this run does not name (IC-037): their
|
|
787
|
+
// front doors come along, as targets of this same plan.
|
|
788
|
+
const companions = packagesOnly ? [] : planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
|
|
789
|
+
targets: annotated, targetFromResult });
|
|
790
|
+
const planned = [...annotated, ...companions];
|
|
791
|
+
const liveChanges = planned.flatMap((item ) => item.changes);
|
|
792
|
+
// Row 127 IC-003: what a retired harness left in this scope's ledger is released, deleted or
|
|
793
|
+
// stripped by the same run, once every live change is known (a row a live change already removes
|
|
794
|
+
// is that change's). Never on remove, which changes only what `-t` names, nor for package work.
|
|
795
|
+
const retiredHarnesses = sweeping
|
|
796
|
+
? planRetiredHarnesses({ registry, ledger: baseLedger, scope, scopeRoot, livePlanChanges: liveChanges, handovers }) : [];
|
|
797
|
+
const changes = [...liveChanges, ...retiredHarnesses.flatMap((item ) => item.changes)];
|
|
798
|
+
const conflicts = planned.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
|
|
799
|
+
const prerequisites = planned.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
|
|
800
|
+
const requiredNativeResources = planned.flatMap((item ) => item.requiredNativeResources || []);
|
|
801
|
+
const notices = [...planned.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice }))),
|
|
802
|
+
...packages.warnings.map((item ) => ({ harness: item.harness, notice: `warning: ${item.warning}` }))];
|
|
803
|
+
return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: planned, changes, requiredNativeResources, conflicts, prerequisites, notices,
|
|
804
|
+
retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0,
|
|
805
|
+
...(retiredHarnesses.length ? { retiredHarnesses } : {}),
|
|
806
|
+
...(packagesOnly ? { packagesOnly: true } : {}), ...(packages.warnings.length ? { packageWarnings: packages.warnings } : {}) });
|
|
505
807
|
}
|
|
506
808
|
|
|
507
809
|
function verificationResources(verification , changes , scope , recoveryRef ) {
|
|
@@ -559,11 +861,20 @@ function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl =
|
|
|
559
861
|
fsImpl.writeFileSync(file, content);
|
|
560
862
|
}
|
|
561
863
|
|
|
562
|
-
function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, fsImpl = fs } ) {
|
|
864
|
+
function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, retiredHarnesses = [], fsImpl = fs } ) {
|
|
563
865
|
// upgradeLedger makes every written ledger v2-shaped (tombstone log present) while v1 inputs
|
|
564
866
|
// stay readable — migration happens on write, never as a separate user-facing step.
|
|
565
867
|
const next = JSON.parse(JSON.stringify(upgradeLedger(ledger ?? defaultLedger({ scope, scopeRoot }))));
|
|
566
868
|
if (!Array.isArray(next.tombstones)) next.tombstones = [];
|
|
869
|
+
// A retired harness's rows leave in this write, whatever the sweep did with each file, and its
|
|
870
|
+
// `targets` entry goes once no (deferred) row of it remains.
|
|
871
|
+
const retiredKeys = retiredReleaseKeys(retiredHarnesses);
|
|
872
|
+
if (retiredKeys.size) {
|
|
873
|
+
next.resources = next.resources.filter((resource ) => !retiredKeys.has(ownershipKey(resource)));
|
|
874
|
+
for (const { harness } of retiredHarnesses) {
|
|
875
|
+
if (!next.resources.some((resource ) => resource.harness === harness)) delete next.targets[harness];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
567
878
|
// Snapshot pre-run claims by ownership identity (harness+asset+identity) so a re-verification
|
|
568
879
|
// that relocates the same claim to a new target is detectable as a MOVE, not remove-plus-add.
|
|
569
880
|
const priorByIdentity = new Map();
|
|
@@ -644,9 +955,14 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
|
|
|
644
955
|
const plannedWrites = context.checkPlannedWrites
|
|
645
956
|
? target.changes.filter((change ) => !change.retained && (change.operation === 'create' || change.operation === 'update')).map((change ) => change.target)
|
|
646
957
|
: undefined;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
958
|
+
// A hook-front-door companion (IC-037) is verified for its doors alone: the rest of that
|
|
959
|
+
// harness is not part of this run and may legitimately trail the current source. A package-only entry has no
|
|
960
|
+
// DoFlow asset to verify, only its package parts (IC-017).
|
|
961
|
+
let verification = target.packagesOnly ? { harness: target.harness, ok: true, statuses: [], resources: [], conflicts: [] }
|
|
962
|
+
: normalizeVerification(adapter[target.hookDoorsOnly ? 'verifyHookDoors' : 'verify']({ ...target.adapterInput,
|
|
963
|
+
registry: context.registry ?? plan.registry, discovery: target.discovery, ledger: plan.ledger,
|
|
964
|
+
operation, context: { ...target.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
|
|
965
|
+
verification = withPackageVerification(verification, target, { adapter, harness, plan, context, operation, plannedWrites });
|
|
650
966
|
if (operation === 'remove') verification = normalizeRemovalVerification(verification, target.changes, plan.ledger);
|
|
651
967
|
verifications.push({ ...verification, hookWiring: hookWiringStatus(harness, target, verification) });
|
|
652
968
|
}
|
|
@@ -654,24 +970,32 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
|
|
|
654
970
|
return { verifications, conflicts, ok: conflicts.length === 0 && verifications.every((result) => result.ok !== false) };
|
|
655
971
|
}
|
|
656
972
|
|
|
657
|
-
function assertPlanApplicable(plan , stateRoot , acceptPrerequisites ) {
|
|
973
|
+
function assertPlanApplicable(plan , stateRoot , acceptPrerequisites , mode = 'apply') {
|
|
974
|
+
// A repeated remove: the ledger holds nothing for these harnesses and the plan changes nothing. That is a
|
|
975
|
+
// finished job, not the malformed plan refused below; the CLI answers this code with a note and exit 0.
|
|
976
|
+
if (mode === 'remove' && plan.safe && !plan.changes.length && !(plan.retiredHarnesses ?? []).some((item ) => item.changes.length)
|
|
977
|
+
&& !(plan.ledger?.resources ?? []).some((resource ) => plan.targets.some((target ) => target.harness === resource.harness))) {
|
|
978
|
+
throw Object.assign(new Error(`Nothing to remove for ${plan.targets.map((target ) => target.harness).join(', ')}: DoFlow owns no native resources there.`), { code: 'DOFLOW_NOTHING_TO_REMOVE' });
|
|
979
|
+
}
|
|
658
980
|
if (!plan.safe && !(acceptPrerequisites && plan.conflicts.length === 0)) throw new Error('Refusing to apply an unsafe lifecycle plan');
|
|
659
981
|
if (!stateRoot) throw new Error('stateRoot is required before lifecycle mutation');
|
|
660
|
-
|
|
982
|
+
// A retirement is native work no live harness lists: a plan holding only that is applicable.
|
|
983
|
+
const retiring = (plan.retiredHarnesses ?? []).some((item ) => item.changes.length);
|
|
984
|
+
if ((!plan.requiredNativeResources?.length && !retiring) || !plan.changes.length) throw new Error('Refusing to apply a lifecycle plan with no required native resources');
|
|
661
985
|
}
|
|
662
986
|
|
|
663
987
|
/** Registry-declared hooks assets (`kind: "hooks"` in core/registry/assets.json) this harness's
|
|
664
|
-
* plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex
|
|
665
|
-
*
|
|
988
|
+
* plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex deploys
|
|
989
|
+
* hooks via its own bespoke code path (see targetNeedsHooks below) rather than a
|
|
666
990
|
* registered asset, so they never appear here. */
|
|
667
991
|
function hookAssetIds(target ) {
|
|
668
992
|
return new Set((target.assets || []).filter((asset ) => asset.kind === 'hooks').map((asset ) => asset.id));
|
|
669
993
|
}
|
|
670
994
|
|
|
671
995
|
/** Whether this harness's plan includes at least one hooks-bearing change. Claude's hooks are a
|
|
672
|
-
* registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex
|
|
673
|
-
*
|
|
674
|
-
*
|
|
996
|
+
* registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex deploys hooks
|
|
997
|
+
* via its own bespoke code path and tags the resulting change with `nativeComponent: 'hooks'`
|
|
998
|
+
* instead (see src/adapters/codex/index.js). Either
|
|
675
999
|
* signal is sufficient — this stays harness-agnostic on purpose. */
|
|
676
1000
|
function targetNeedsHooks(target ) {
|
|
677
1001
|
const hookIds = hookAssetIds(target);
|
|
@@ -680,20 +1004,10 @@ function targetNeedsHooks(target ) {
|
|
|
680
1004
|
|
|
681
1005
|
/** Whether a verified resource is the hooks resource this harness owns. A registry-declared hooks
|
|
682
1006
|
* asset (Claude, Kiro) is matched by assetId; Codex's bespoke hooks-file resource is matched by
|
|
683
|
-
* `kind
|
|
684
|
-
* matched by its `projection.renderer` tag — see src/adapters/codex/index.js's
|
|
685
|
-
* `lifecycleResource(..., kind: 'hooks-file', ...)` and src/adapters/gemini/index.js's verify(),
|
|
686
|
-
* which tags its hooks resource `projection: { renderer: 'gemini-hooks' }`. */
|
|
1007
|
+
* `kind` — see src/adapters/codex/index.js's `lifecycleResource(..., kind: 'hooks-file', ...)`. */
|
|
687
1008
|
function verificationOwnsHooks(verification , hookIds ) {
|
|
688
1009
|
return (verification?.resources || []).some((resource ) =>
|
|
689
|
-
hookIds.has(resource.assetId) || resource.kind === 'hooks-file'
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/** Gemini's hooks trust, observed through the adapter's own API. How trust is computed — the
|
|
693
|
-
* fingerprint semantics, which context fields matter — lives in src/adapters/gemini (review A3:
|
|
694
|
-
* this file used to import planGeminiHooks and re-derive the plan itself). */
|
|
695
|
-
function geminiHookTrust(target ) {
|
|
696
|
-
return geminiHookTrustFor(target);
|
|
1010
|
+
hookIds.has(resource.assetId) || resource.kind === 'hooks-file');
|
|
697
1011
|
}
|
|
698
1012
|
|
|
699
1013
|
/** General, per-harness hook-wiring status — the one place install status distinguishes an
|
|
@@ -702,18 +1016,12 @@ function geminiHookTrust(target ) {
|
|
|
702
1016
|
* - 'absent': this harness owns no hooks resource in the current verification (nothing installed).
|
|
703
1017
|
* - 'installed-pending': a hooks resource is installed, but a prerequisite is unmet — Codex's
|
|
704
1018
|
* static `capabilities.hooks.prerequisites` (`trusted-project`, `hook-review`), which DoFlow has
|
|
705
|
-
* no way to confirm a human satisfied, so it is reported unmet unconditionally
|
|
706
|
-
* trust computation via geminiHookTrust above.
|
|
1019
|
+
* no way to confirm a human satisfied, so it is reported unmet unconditionally.
|
|
707
1020
|
* - 'active': installed with no unmet prerequisite (Claude and Kiro today, since neither declares
|
|
708
1021
|
* any `capabilities.hooks.prerequisites` and Kiro hooks activate automatically per kiro.dev). */
|
|
709
1022
|
function hookWiringStatus(harness , target , verification ) {
|
|
710
1023
|
if (!target || target.skipped) return { status: 'absent', prerequisites: [] };
|
|
711
1024
|
if (!verificationOwnsHooks(verification, hookAssetIds(target))) return { status: 'absent', prerequisites: [] };
|
|
712
|
-
if (harness.id === 'gemini') {
|
|
713
|
-
const trust = geminiHookTrust(target);
|
|
714
|
-
if (trust?.required && !trust.trusted) return { status: 'installed-pending', prerequisites: ['hook-trust'] };
|
|
715
|
-
return { status: 'active', prerequisites: [] };
|
|
716
|
-
}
|
|
717
1025
|
const prerequisites = harness.capabilities?.hooks?.prerequisites || [];
|
|
718
1026
|
return prerequisites.length
|
|
719
1027
|
? { status: 'installed-pending', prerequisites: [...prerequisites] }
|
|
@@ -734,11 +1042,17 @@ function assertBashAvailableForHooks(plan , mode , hasBashCapableShellFn
|
|
|
734
1042
|
}
|
|
735
1043
|
|
|
736
1044
|
function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.ledger, context = {}, mode = 'apply', acceptPrerequisites = false, writeLedgerFn = writeLedger, writeRecoveryRecordFn = writeRecoveryRecord, hasBashCapableShellFn = hasBashCapableShell } ) {
|
|
737
|
-
assertPlanApplicable(plan, stateRoot, acceptPrerequisites);
|
|
1045
|
+
assertPlanApplicable(plan, stateRoot, acceptPrerequisites, mode);
|
|
738
1046
|
assertBashAvailableForHooks(plan, mode, hasBashCapableShellFn);
|
|
739
1047
|
const recovery = writeRecoveryRecordFn(stateRoot, { status: 'pending', changes: plan.changes });
|
|
740
1048
|
const completedHarnesses = [];
|
|
741
1049
|
try {
|
|
1050
|
+
// The retirement first (IC-003): it touches only files no live harness claims, and a throw here
|
|
1051
|
+
// leaves every live harness unapplied.
|
|
1052
|
+
for (const retired of plan.retiredHarnesses ?? []) {
|
|
1053
|
+
applyRetiredHarnesses([retired]);
|
|
1054
|
+
completedHarnesses.push(`retired:${retired.harness}`);
|
|
1055
|
+
}
|
|
742
1056
|
for (const target of plan.targets) {
|
|
743
1057
|
if (target.skipped || target.changes.length === 0) continue;
|
|
744
1058
|
// A retained change is a ledger release, not a native mutation: the adapter must never see
|
|
@@ -749,7 +1063,12 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
749
1063
|
if (!applicable.length) continue;
|
|
750
1064
|
const harness = harnessFor(registry, target.harness);
|
|
751
1065
|
const adapter = resolveAdapter(adapters, harness);
|
|
752
|
-
|
|
1066
|
+
// Hook-tier removals (maint-hooks) run through the adapter's own remove path, before the content
|
|
1067
|
+
// apply, so a shared native file is read by each step as the step before left it.
|
|
1068
|
+
const tierRemovals = mode === 'remove' ? [] : applicable.filter((change ) => change.hookTier === true);
|
|
1069
|
+
if (tierRemovals.length) adapter.remove({ ...target.hookTierInput, registry, changes: tierRemovals, ledger, recoveryRef: recovery.id });
|
|
1070
|
+
const rest = tierRemovals.length ? applicable.filter((change ) => change.hookTier !== true) : applicable;
|
|
1071
|
+
if (rest.length) adapter[mode === 'remove' ? 'remove' : 'apply']({ ...target.adapterInput, registry, changes: rest, ledger, recoveryRef: recovery.id });
|
|
753
1072
|
completedHarnesses.push(target.harness);
|
|
754
1073
|
}
|
|
755
1074
|
} catch (error ) {
|
|
@@ -767,7 +1086,8 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
767
1086
|
throw new Error('Lifecycle verification failed; ledger was not updated');
|
|
768
1087
|
}
|
|
769
1088
|
try {
|
|
770
|
-
|
|
1089
|
+
// A package-only plan selects no MCP server; the index stays as the last install wrote it.
|
|
1090
|
+
if (!plan.packagesOnly) applyMcpIndex({ scopeRoot: plan.scopeRoot, selectedMcp: plan.mcp, mode, retain: !removalIsTotal(plan, registry) });
|
|
771
1091
|
} catch (error ) {
|
|
772
1092
|
// Same reasoning as the harness-loop catch above: every native resource already applied and
|
|
773
1093
|
// verified successfully by this point, so leaving the record at 'pending' on a failure here
|
|
@@ -775,9 +1095,13 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
775
1095
|
writeRecoveryRecordFn(stateRoot, { ...recovery.record, status: 'failed', completedHarnesses, verification, error: error.message });
|
|
776
1096
|
throw error;
|
|
777
1097
|
}
|
|
778
|
-
const nextLedger = updateLedger({ ledger, scope: plan.scope, scopeRoot: plan.scopeRoot, verifications: verification.verifications, changes: plan.changes, recoveryRef: recovery.id
|
|
1098
|
+
const nextLedger = updateLedger({ ledger, scope: plan.scope, scopeRoot: plan.scopeRoot, verifications: verification.verifications, changes: plan.changes, recoveryRef: recovery.id,
|
|
1099
|
+
retiredHarnesses: plan.retiredHarnesses ?? [] });
|
|
779
1100
|
writeLedgerFn(stateRoot, nextLedger);
|
|
780
1101
|
writeRecoveryRecordFn(stateRoot, { ...recovery.record, status: 'verified', verification });
|
|
1102
|
+
if (plan.scope === 'global' && plan.changes.some((change ) => isPackageAsset(change.assetId))) {
|
|
1103
|
+
settleSuspendedProjection({ homeDir: plan.scopeRoot });
|
|
1104
|
+
}
|
|
781
1105
|
return { recovery, verification, ledger: nextLedger, retained: plan.retained ?? [] };
|
|
782
1106
|
}
|
|
783
1107
|
|
|
@@ -804,9 +1128,33 @@ function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan
|
|
|
804
1128
|
return { ledger: next, recorded };
|
|
805
1129
|
}
|
|
806
1130
|
|
|
1131
|
+
function settleSuspendedProjection({ homeDir } ) {
|
|
1132
|
+
try {
|
|
1133
|
+
const { readState, recordSuspension } = require('../runtime/packages/state.js');
|
|
1134
|
+
const { stateRoot, readLedger } = require('../state/index.js');
|
|
1135
|
+
const root = stateRoot({ scope: 'global', projectRoot: homeDir, homeDir });
|
|
1136
|
+
const ledger = readLedger(root);
|
|
1137
|
+
const read = readState({ homeDir });
|
|
1138
|
+
if (!ledger || read.unreadable) return;
|
|
1139
|
+
for (const suspension of read.state.suspended) {
|
|
1140
|
+
if (!suspension.projected) continue;
|
|
1141
|
+
const prefix = `pkg.${suspension.package}.`;
|
|
1142
|
+
const stillProjected = ledger.resources.some((row ) =>
|
|
1143
|
+
typeof row.assetId === 'string' && row.assetId.startsWith(prefix));
|
|
1144
|
+
if (!stillProjected) {
|
|
1145
|
+
recordSuspension({ homeDir }, {
|
|
1146
|
+
packageId: suspension.package, reason: suspension.reason, since: suspension.since, projected: false,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} catch {
|
|
1151
|
+
// Settlement is best-effort and never blocks the verb that removed package rows.
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
807
1155
|
function removeLifecycle(options ) {
|
|
808
1156
|
const plan = planLifecycle({ ...options, context: { ...(options.context || {}), operation: 'remove' } });
|
|
809
1157
|
return applyLifecycle({ ...options, plan, mode: 'remove', acceptPrerequisites: options.acceptPrerequisites });
|
|
810
1158
|
}
|
|
811
1159
|
|
|
812
|
-
module.exports = { OPERATIONS, MCP_KEEP, recordMcpOwnership, registryScope, normalizeTargets, normalizeChange, matchesChange, normalizeRemovalVerification, adapterConflicts, planLifecycle, verifyLifecycle, applyLifecycle, removeLifecycle, updateLedger, mcpIndexPath, applyMcpIndex, targetNeedsHooks, assertBashAvailableForHooks, hookWiringStatus, markRetainedRemovals, retentionSummary,
|
|
1160
|
+
module.exports = { OPERATIONS, MCP_KEEP, recordMcpOwnership, isPackageAsset, registryScope, normalizeTargets, normalizeChange, matchesChange, normalizeRemovalVerification, adapterConflicts, planLifecycle, verifyLifecycle, applyLifecycle, removeLifecycle, updateLedger, mcpIndexPath, applyMcpIndex, targetNeedsHooks, assertBashAvailableForHooks, hookWiringStatus, markRetainedRemovals, retentionSummary, PRUNED_TREES, settleSuspendedProjection };
|