@khoavu882/doflow 2.0.0-beta.5 → 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 -2
- 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/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-compact.sh +23 -27
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- 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/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- 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 +1 -0
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +8 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +10 -2
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +2 -11
- 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 +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +3 -2
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +47 -8
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- 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 +1442 -363
- package/core/registry/lifecycle.json +485 -304
- 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 +679 -31
- 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 +149 -34
- 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 +18 -17
- 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 +15 -58
- 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 +26 -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 +4 -7
- package/core/shared/skills/do-plan/SKILL.md +26 -15
- 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 +143 -66
- package/src/adapters/claude/index.js +24 -4
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/index.js +48 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +290 -44
- 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 +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- 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 +2 -1
- package/src/cli/index.js +322 -29
- package/src/cli/runtime-commands.js +209 -57
- 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 +20 -3
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- 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 +20 -11
- 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 +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -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 +154 -78
- 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-ledger.js +34 -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 +33 -3
- 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 +2 -2
- 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 +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- 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 +60 -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 +1 -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 +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +374 -64
- 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/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 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- 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 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- 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 -251
- package/src/adapters/gemini/index.js +0 -429
package/src/lifecycle/index.js
CHANGED
|
@@ -8,15 +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 { hookTrustFor: geminiHookTrustFor } = require('../adapters/gemini/index.js');
|
|
19
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');
|
|
20
23
|
|
|
21
24
|
const OPERATIONS = new Set(['create', 'merge', 'update', 'remove']);
|
|
22
25
|
/** A harness's MCP selection meaning "the servers it owns now", read through its adapter's
|
|
@@ -199,12 +202,14 @@ function releasedOwnershipKeys(harnessPlans , scope ) {
|
|
|
199
202
|
* applied. Membership is decided per ROW, not per harness: a harness that is releasing its claim
|
|
200
203
|
* on this file is not a claimant even if it is releasing nothing else, and a harness in the same
|
|
201
204
|
* batch that keeps its row (an install that relocates one asset while a sibling's projection is
|
|
202
|
-
* unchanged) still is.
|
|
203
|
-
|
|
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 ) {
|
|
204
208
|
const byTarget = new Map();
|
|
205
209
|
for (const resource of ledger?.resources || []) {
|
|
206
210
|
if (typeof resource?.target !== 'string' || typeof resource?.harness !== 'string') continue;
|
|
207
211
|
if (releasedKeys.has(ownershipKey(resource))) continue;
|
|
212
|
+
if (isRetired(registry, resource.harness)) continue;
|
|
208
213
|
const key = claimKey(resource);
|
|
209
214
|
if (!byTarget.has(key)) byTarget.set(key, new Set());
|
|
210
215
|
byTarget.get(key).add(resource.harness);
|
|
@@ -212,16 +217,10 @@ function survivingClaimantsByTarget(ledger , releasedKeys ) {
|
|
|
212
217
|
return byTarget;
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
/** What one claim covers: a whole file, except an MCP entry, which is claimed on its own because
|
|
216
|
-
* several harnesses keep their servers side by side in one MCP file. */
|
|
217
|
-
function claimKey(record ) {
|
|
218
|
-
return record.kind === 'mcp-server' ? `${record.target}\u0000${record.identity}` : record.target;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
220
|
/** NFR-007: a removal reclaims only what no other harness still claims.
|
|
222
221
|
*
|
|
223
222
|
* Several assets project to ONE destination for several harnesses — `scripts.doflow` is a single
|
|
224
|
-
* `<project>/.doflow/scripts` tree for claude
|
|
223
|
+
* `<project>/.doflow/scripts` tree for claude and codex, and antigravity and copilot both
|
|
225
224
|
* resolve to `<root>/.agents` at project scope — while ownership is recorded per harness. A
|
|
226
225
|
* removal that deleted every file its own rows named therefore took the shared runtime out from
|
|
227
226
|
* under the harnesses that are still installed, leaving them with rows pointing at files that no
|
|
@@ -233,8 +232,19 @@ function claimKey(record ) {
|
|
|
233
232
|
* stays, and the last claimant's removal is the one that reclaims it. The neutral ledger is the
|
|
234
233
|
* only thing consulted — it is already the record of who owns what, and a second ownership
|
|
235
234
|
* registry would be one more thing to keep in sync. */
|
|
236
|
-
function markRetainedRemovals(harnessPlans , ledger , scope ) {
|
|
237
|
-
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
|
+
}
|
|
238
248
|
if (!claimants.size) return harnessPlans;
|
|
239
249
|
return harnessPlans.map((target ) => {
|
|
240
250
|
if (target.skipped) return target;
|
|
@@ -255,10 +265,11 @@ function markRetainedRemovals(harnessPlans , ledger , scope ) {
|
|
|
255
265
|
}
|
|
256
266
|
|
|
257
267
|
/** Whether this plan leaves no ownership row standing anywhere — the only case in which a shared,
|
|
258
|
-
* install-wide artifact that no single harness owns is genuinely unclaimed.
|
|
259
|
-
|
|
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 ) {
|
|
260
271
|
const released = releasedOwnershipKeys(plan.targets || [], plan.scope);
|
|
261
|
-
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));
|
|
262
273
|
}
|
|
263
274
|
|
|
264
275
|
/** What a removal kept and why, ready to print. A command that says "removed" while silently
|
|
@@ -308,13 +319,17 @@ function harnessMcpSelection(registry , harness , raw ) {
|
|
|
308
319
|
return [];
|
|
309
320
|
}
|
|
310
321
|
|
|
311
|
-
/** The
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* ships is dead for every harness at once.
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
|
|
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']);
|
|
318
333
|
|
|
319
334
|
function sha256File(file , fsImpl ) {
|
|
320
335
|
return crypto.createHash('sha256').update(fsImpl.readFileSync(file)).digest('hex');
|
|
@@ -364,12 +379,22 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
364
379
|
const recorded = new Set(rows.map((row ) => row.target));
|
|
365
380
|
const shown = (file ) => path.relative(scopeRoot, file);
|
|
366
381
|
|
|
367
|
-
|
|
368
|
-
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
|
+
}
|
|
369
395
|
if (!executor) continue;
|
|
370
|
-
const
|
|
371
|
-
const
|
|
372
|
-
const destDir = sharedTreeDestDir(scopeRoot, nativeDir);
|
|
396
|
+
const assetId = asset.id;
|
|
397
|
+
const destDir = sharedDest(executor, asset);
|
|
373
398
|
const sourceDir = path.resolve(repoRoot, asset.source);
|
|
374
399
|
if (!destDir || !fsImpl.existsSync(sourceDir) || !fsImpl.existsSync(destDir)) continue;
|
|
375
400
|
const produced = discoverTree({ sourceDir, destDir, fsImpl }).files.map((file ) => file.destAbs);
|
|
@@ -386,7 +411,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
386
411
|
const removalsAt = new Map ();
|
|
387
412
|
for (const target of planned) {
|
|
388
413
|
for (const change of target.changes) {
|
|
389
|
-
if (change.operation !== 'remove' || change.assetId
|
|
414
|
+
if (change.operation !== 'remove' || !group.includes(change.assetId) || !isUnder(change.target, destDir) || producedSet.has(change.target)) continue;
|
|
390
415
|
if (!removalsAt.has(change.target)) removalsAt.set(change.target, []);
|
|
391
416
|
removalsAt.get(change.target) .push(change);
|
|
392
417
|
}
|
|
@@ -397,11 +422,19 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
397
422
|
if (!change.retained) replaced.get(change.harness) .set(change, Object.freeze({ ...change, retained: true, retainedFor: [] }));
|
|
398
423
|
}
|
|
399
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
|
+
}
|
|
400
433
|
|
|
401
434
|
for (const file of new Set([...rowsAt.keys(), ...removalsAt.keys()])) {
|
|
402
435
|
const fileRows = rowsAt.get(file) ?? [];
|
|
403
436
|
// Another asset's live row on this path is a claim this pass does not judge.
|
|
404
|
-
if (fileRows.some((row ) => row.assetId
|
|
437
|
+
if (fileRows.some((row ) => !group.includes(row.assetId))) continue;
|
|
405
438
|
const removals = removalsAt.get(file) ?? [];
|
|
406
439
|
const identity = path.relative(destDir, file);
|
|
407
440
|
// The rows no planned removal of this file releases on its own.
|
|
@@ -424,14 +457,25 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
424
457
|
}
|
|
425
458
|
if (sameFile(file)) { keepFile(file); continue; } // the produced file under an old spelling
|
|
426
459
|
const current = sha256File(file, fsImpl);
|
|
427
|
-
|
|
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) {
|
|
428
466
|
// A planned removal here was already kept by planTree, and its adapter named it.
|
|
429
467
|
if (!removals.length) notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, changed since DoFlow wrote it`);
|
|
430
468
|
continue;
|
|
431
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
|
+
}
|
|
432
476
|
notices.get(executor.harness) .push(`prune ${shown(file)}: no longer shipped, unchanged since DoFlow wrote it`);
|
|
433
477
|
// removeTree re-runs containment and the same-file check from this right before the unlink.
|
|
434
|
-
const pruneGuard = { root: destDir, source: sourceDir };
|
|
478
|
+
const pruneGuard = { root: destDir, source: sourceDir, scope: scopeRoot };
|
|
435
479
|
if (!removals.length) {
|
|
436
480
|
added.get(executor.harness) .push(synthetic({ fingerprint: current, supersedes: superseded, pruneGuard }));
|
|
437
481
|
continue;
|
|
@@ -446,7 +490,7 @@ function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot,
|
|
|
446
490
|
// Files no row names and the source does not produce: never DoFlow's to delete, but named.
|
|
447
491
|
for (const rel of walkRelFiles(destDir, fsImpl)) {
|
|
448
492
|
const file = path.join(destDir, rel);
|
|
449
|
-
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;
|
|
450
494
|
notices.get(executor.harness) .push(`kept ${shown(file)}: not shipped by this version and not recorded as DoFlow's`);
|
|
451
495
|
}
|
|
452
496
|
}
|
|
@@ -466,11 +510,230 @@ function targetFromResult({ harness, assets, mcp, mcpSelected, policies, adapter
|
|
|
466
510
|
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
467
511
|
}
|
|
468
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
|
+
|
|
469
716
|
function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], ledger, context = {} } ) {
|
|
470
717
|
assertScope(scope);
|
|
471
718
|
if (!registry) throw new Error('registry is required');
|
|
472
719
|
const baseLedger = ledger ?? defaultLedger({ scope, scopeRoot });
|
|
473
|
-
|
|
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) => {
|
|
474
737
|
if (!harness.scopes.includes(registryScope(scope))) {
|
|
475
738
|
return { harness: harness.id, assets: [], changes: [], conflicts: [`Harness '${harness.id}' does not support ${scope} scope`], prerequisites: [], notices: [], skipped: true, mcpSelected: null };
|
|
476
739
|
}
|
|
@@ -480,7 +743,9 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
480
743
|
const policies = renderPolicies(registry, { harness: harness.id });
|
|
481
744
|
const adoptable = capable ? selectMcpServers(registry, catalogIds(registry, mcpAdoptable[harness.id] ?? [])) : [];
|
|
482
745
|
const adapter = resolveAdapter(adapters, harness);
|
|
483
|
-
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 });
|
|
484
749
|
// `keep` asks the adapter what it owns before the real projection: the probe carries no
|
|
485
750
|
// selection, so discovery reads the files as they are.
|
|
486
751
|
const ids = selection === MCP_KEEP
|
|
@@ -491,29 +756,54 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
491
756
|
const input = { ...adapterInput, registry, ledger: baseLedger };
|
|
492
757
|
const discovery = adapter.discover(input);
|
|
493
758
|
const result = adapter.plan({ ...input, discovery });
|
|
494
|
-
|
|
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;
|
|
495
763
|
});
|
|
496
764
|
// The MCP index names every server a harness of this scope uses: the selections planned here and
|
|
497
765
|
// the recorded selections of the harnesses this run does not touch.
|
|
498
766
|
const selectedMcp = selectMcpServers(registry, catalogIds(registry,
|
|
499
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));
|
|
500
781
|
// Only once every harness's plan is known: whether a file may be deleted depends on the rows
|
|
501
782
|
// the WHOLE plan leaves standing, which no single harness's plan can see.
|
|
502
|
-
const retained = markRetainedRemovals(
|
|
503
|
-
const annotated = context.operation === 'remove' ? retained
|
|
783
|
+
const retained = markRetainedRemovals(withPackages, baseLedger, scope, registry);
|
|
784
|
+
const annotated = context.operation === 'remove' || packagesOnly ? retained
|
|
504
785
|
: pruneRetiredRuntimeFiles(retained, baseLedger, { scopeRoot, repoRoot: context.repoRoot });
|
|
505
786
|
// The shared hooks tree may have moved under harnesses this run does not name (IC-037): their
|
|
506
787
|
// front doors come along, as targets of this same plan.
|
|
507
|
-
const companions = planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
|
|
788
|
+
const companions = packagesOnly ? [] : planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
|
|
508
789
|
targets: annotated, targetFromResult });
|
|
509
790
|
const planned = [...annotated, ...companions];
|
|
510
|
-
const
|
|
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)];
|
|
511
798
|
const conflicts = planned.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
|
|
512
799
|
const prerequisites = planned.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
|
|
513
800
|
const requiredNativeResources = planned.flatMap((item ) => item.requiredNativeResources || []);
|
|
514
|
-
const notices = planned.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice })))
|
|
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}` }))];
|
|
515
803
|
return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: planned, changes, requiredNativeResources, conflicts, prerequisites, notices,
|
|
516
|
-
retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0
|
|
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 } : {}) });
|
|
517
807
|
}
|
|
518
808
|
|
|
519
809
|
function verificationResources(verification , changes , scope , recoveryRef ) {
|
|
@@ -571,11 +861,20 @@ function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl =
|
|
|
571
861
|
fsImpl.writeFileSync(file, content);
|
|
572
862
|
}
|
|
573
863
|
|
|
574
|
-
function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, fsImpl = fs } ) {
|
|
864
|
+
function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, retiredHarnesses = [], fsImpl = fs } ) {
|
|
575
865
|
// upgradeLedger makes every written ledger v2-shaped (tombstone log present) while v1 inputs
|
|
576
866
|
// stay readable — migration happens on write, never as a separate user-facing step.
|
|
577
867
|
const next = JSON.parse(JSON.stringify(upgradeLedger(ledger ?? defaultLedger({ scope, scopeRoot }))));
|
|
578
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
|
+
}
|
|
579
878
|
// Snapshot pre-run claims by ownership identity (harness+asset+identity) so a re-verification
|
|
580
879
|
// that relocates the same claim to a new target is detectable as a MOVE, not remove-plus-add.
|
|
581
880
|
const priorByIdentity = new Map();
|
|
@@ -657,10 +956,13 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
|
|
|
657
956
|
? target.changes.filter((change ) => !change.retained && (change.operation === 'create' || change.operation === 'update')).map((change ) => change.target)
|
|
658
957
|
: undefined;
|
|
659
958
|
// A hook-front-door companion (IC-037) is verified for its doors alone: the rest of that
|
|
660
|
-
// harness is not part of this run and may legitimately trail the current source.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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 });
|
|
664
966
|
if (operation === 'remove') verification = normalizeRemovalVerification(verification, target.changes, plan.ledger);
|
|
665
967
|
verifications.push({ ...verification, hookWiring: hookWiringStatus(harness, target, verification) });
|
|
666
968
|
}
|
|
@@ -668,24 +970,32 @@ function verifyLifecycle({ plan, adapters, context = {} } ) {
|
|
|
668
970
|
return { verifications, conflicts, ok: conflicts.length === 0 && verifications.every((result) => result.ok !== false) };
|
|
669
971
|
}
|
|
670
972
|
|
|
671
|
-
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
|
+
}
|
|
672
980
|
if (!plan.safe && !(acceptPrerequisites && plan.conflicts.length === 0)) throw new Error('Refusing to apply an unsafe lifecycle plan');
|
|
673
981
|
if (!stateRoot) throw new Error('stateRoot is required before lifecycle mutation');
|
|
674
|
-
|
|
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');
|
|
675
985
|
}
|
|
676
986
|
|
|
677
987
|
/** Registry-declared hooks assets (`kind: "hooks"` in core/registry/assets.json) this harness's
|
|
678
|
-
* plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex
|
|
679
|
-
*
|
|
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
|
|
680
990
|
* registered asset, so they never appear here. */
|
|
681
991
|
function hookAssetIds(target ) {
|
|
682
992
|
return new Set((target.assets || []).filter((asset ) => asset.kind === 'hooks').map((asset ) => asset.id));
|
|
683
993
|
}
|
|
684
994
|
|
|
685
995
|
/** Whether this harness's plan includes at least one hooks-bearing change. Claude's hooks are a
|
|
686
|
-
* registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex
|
|
687
|
-
*
|
|
688
|
-
*
|
|
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
|
|
689
999
|
* signal is sufficient — this stays harness-agnostic on purpose. */
|
|
690
1000
|
function targetNeedsHooks(target ) {
|
|
691
1001
|
const hookIds = hookAssetIds(target);
|
|
@@ -694,20 +1004,10 @@ function targetNeedsHooks(target ) {
|
|
|
694
1004
|
|
|
695
1005
|
/** Whether a verified resource is the hooks resource this harness owns. A registry-declared hooks
|
|
696
1006
|
* asset (Claude, Kiro) is matched by assetId; Codex's bespoke hooks-file resource is matched by
|
|
697
|
-
* `kind
|
|
698
|
-
* matched by its `projection.renderer` tag — see src/adapters/codex/index.js's
|
|
699
|
-
* `lifecycleResource(..., kind: 'hooks-file', ...)` and src/adapters/gemini/index.js's verify(),
|
|
700
|
-
* which tags its hooks resource `projection: { renderer: 'gemini-hooks' }`. */
|
|
1007
|
+
* `kind` — see src/adapters/codex/index.js's `lifecycleResource(..., kind: 'hooks-file', ...)`. */
|
|
701
1008
|
function verificationOwnsHooks(verification , hookIds ) {
|
|
702
1009
|
return (verification?.resources || []).some((resource ) =>
|
|
703
|
-
hookIds.has(resource.assetId) || resource.kind === 'hooks-file'
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/** Gemini's hooks trust, observed through the adapter's own API. How trust is computed — the
|
|
707
|
-
* fingerprint semantics, which context fields matter — lives in src/adapters/gemini (review A3:
|
|
708
|
-
* this file used to import planGeminiHooks and re-derive the plan itself). */
|
|
709
|
-
function geminiHookTrust(target ) {
|
|
710
|
-
return geminiHookTrustFor(target);
|
|
1010
|
+
hookIds.has(resource.assetId) || resource.kind === 'hooks-file');
|
|
711
1011
|
}
|
|
712
1012
|
|
|
713
1013
|
/** General, per-harness hook-wiring status — the one place install status distinguishes an
|
|
@@ -716,18 +1016,12 @@ function geminiHookTrust(target ) {
|
|
|
716
1016
|
* - 'absent': this harness owns no hooks resource in the current verification (nothing installed).
|
|
717
1017
|
* - 'installed-pending': a hooks resource is installed, but a prerequisite is unmet — Codex's
|
|
718
1018
|
* static `capabilities.hooks.prerequisites` (`trusted-project`, `hook-review`), which DoFlow has
|
|
719
|
-
* no way to confirm a human satisfied, so it is reported unmet unconditionally
|
|
720
|
-
* trust computation via geminiHookTrust above.
|
|
1019
|
+
* no way to confirm a human satisfied, so it is reported unmet unconditionally.
|
|
721
1020
|
* - 'active': installed with no unmet prerequisite (Claude and Kiro today, since neither declares
|
|
722
1021
|
* any `capabilities.hooks.prerequisites` and Kiro hooks activate automatically per kiro.dev). */
|
|
723
1022
|
function hookWiringStatus(harness , target , verification ) {
|
|
724
1023
|
if (!target || target.skipped) return { status: 'absent', prerequisites: [] };
|
|
725
1024
|
if (!verificationOwnsHooks(verification, hookAssetIds(target))) return { status: 'absent', prerequisites: [] };
|
|
726
|
-
if (harness.id === 'gemini') {
|
|
727
|
-
const trust = geminiHookTrust(target);
|
|
728
|
-
if (trust?.required && !trust.trusted) return { status: 'installed-pending', prerequisites: ['hook-trust'] };
|
|
729
|
-
return { status: 'active', prerequisites: [] };
|
|
730
|
-
}
|
|
731
1025
|
const prerequisites = harness.capabilities?.hooks?.prerequisites || [];
|
|
732
1026
|
return prerequisites.length
|
|
733
1027
|
? { status: 'installed-pending', prerequisites: [...prerequisites] }
|
|
@@ -748,11 +1042,17 @@ function assertBashAvailableForHooks(plan , mode , hasBashCapableShellFn
|
|
|
748
1042
|
}
|
|
749
1043
|
|
|
750
1044
|
function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.ledger, context = {}, mode = 'apply', acceptPrerequisites = false, writeLedgerFn = writeLedger, writeRecoveryRecordFn = writeRecoveryRecord, hasBashCapableShellFn = hasBashCapableShell } ) {
|
|
751
|
-
assertPlanApplicable(plan, stateRoot, acceptPrerequisites);
|
|
1045
|
+
assertPlanApplicable(plan, stateRoot, acceptPrerequisites, mode);
|
|
752
1046
|
assertBashAvailableForHooks(plan, mode, hasBashCapableShellFn);
|
|
753
1047
|
const recovery = writeRecoveryRecordFn(stateRoot, { status: 'pending', changes: plan.changes });
|
|
754
1048
|
const completedHarnesses = [];
|
|
755
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
|
+
}
|
|
756
1056
|
for (const target of plan.targets) {
|
|
757
1057
|
if (target.skipped || target.changes.length === 0) continue;
|
|
758
1058
|
// A retained change is a ledger release, not a native mutation: the adapter must never see
|
|
@@ -763,7 +1063,12 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
763
1063
|
if (!applicable.length) continue;
|
|
764
1064
|
const harness = harnessFor(registry, target.harness);
|
|
765
1065
|
const adapter = resolveAdapter(adapters, harness);
|
|
766
|
-
|
|
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 });
|
|
767
1072
|
completedHarnesses.push(target.harness);
|
|
768
1073
|
}
|
|
769
1074
|
} catch (error ) {
|
|
@@ -781,7 +1086,8 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
781
1086
|
throw new Error('Lifecycle verification failed; ledger was not updated');
|
|
782
1087
|
}
|
|
783
1088
|
try {
|
|
784
|
-
|
|
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) });
|
|
785
1091
|
} catch (error ) {
|
|
786
1092
|
// Same reasoning as the harness-loop catch above: every native resource already applied and
|
|
787
1093
|
// verified successfully by this point, so leaving the record at 'pending' on a failure here
|
|
@@ -789,9 +1095,13 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
789
1095
|
writeRecoveryRecordFn(stateRoot, { ...recovery.record, status: 'failed', completedHarnesses, verification, error: error.message });
|
|
790
1096
|
throw error;
|
|
791
1097
|
}
|
|
792
|
-
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 ?? [] });
|
|
793
1100
|
writeLedgerFn(stateRoot, nextLedger);
|
|
794
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
|
+
}
|
|
795
1105
|
return { recovery, verification, ledger: nextLedger, retained: plan.retained ?? [] };
|
|
796
1106
|
}
|
|
797
1107
|
|
|
@@ -818,9 +1128,33 @@ function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan
|
|
|
818
1128
|
return { ledger: next, recorded };
|
|
819
1129
|
}
|
|
820
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
|
+
|
|
821
1155
|
function removeLifecycle(options ) {
|
|
822
1156
|
const plan = planLifecycle({ ...options, context: { ...(options.context || {}), operation: 'remove' } });
|
|
823
1157
|
return applyLifecycle({ ...options, plan, mode: 'remove', acceptPrerequisites: options.acceptPrerequisites });
|
|
824
1158
|
}
|
|
825
1159
|
|
|
826
|
-
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 };
|