@khoavu882/doflow 2.0.0-beta.5 → 2.0.1
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 +16 -16
- package/bin/doflow.js +22 -12
- 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/mcp-tool-guard.sh +4 -1
- 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/pre-implement-gate.sh +4 -1
- 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 +26 -9
- 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/hooks.json +0 -10
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- 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/pre-implement-gate.sh +4 -1
- 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 +94 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-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 +51 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -11
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
- 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 +11 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
- 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 +16 -2
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
- 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 +20 -7
- 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-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
- 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 +58 -20
- 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 +35 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1445 -366
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +599 -996
- 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/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/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 +272 -0
- package/core/registry/storage.json +687 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -6
- 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 +3 -2
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- 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 +49 -5
- package/core/shared/scripts/doflow/bin/doflow-run +152 -34
- package/core/shared/skills/do/SKILL.md +14 -12
- 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 +45 -97
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -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 +48 -148
- 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 +23 -20
- package/core/shared/skills/do-diagnose/SKILL.md +18 -6
- package/core/shared/skills/do-document/SKILL.md +12 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +19 -7
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
- 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 +16 -2
- 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 +10 -11
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +13 -14
- package/core/shared/skills/do-plan/SKILL.md +34 -20
- 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/shared/workflows/do-flow-chain.md +2 -0
- 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 +46 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +61 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +291 -46
- 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 +22 -3
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +337 -32
- package/src/cli/runtime-commands.js +220 -60
- 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 +4 -16
- 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 +472 -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/pointer.js +67 -12
- 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 +158 -79
- 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 +52 -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 +624 -18
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +281 -26
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- 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 +88 -10
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +38 -7
- package/src/runtime/lifecycle/overview.js +26 -2
- 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 +487 -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/attendance.js +137 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +273 -0
- package/src/runtime/roadmap/loop.js +1719 -0
- package/src/runtime/roadmap/merge.js +390 -0
- package/src/runtime/roadmap/rows.js +1122 -0
- package/src/runtime/roadmap/seed.js +1063 -0
- package/src/runtime/roadmap/worktree-cleanup.js +147 -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 +85 -14
- 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 +373 -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/verb-view.js +267 -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 -54
- 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 +256 -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 +610 -67
- 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/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- 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/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
- 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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// SemVer 2.0.0 parsing and precedence, and the package host-range grammar (IC-016).
|
|
4
|
+
// No dependency: DoFlow ships `2.0.0-beta.N`, so prerelease precedence is part of the contract.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const HOST_RANGE_GRAMMAR = 'one or more comparators separated by single spaces, all of which must hold: '
|
|
11
|
+
+ '>=V, <V, =V or a bare V (exact), where V is a full semver 2.0.0 version; '
|
|
12
|
+
+ 'an upper bound (<V, =V or a bare V) is required; no ^, ~, ||, x or *';
|
|
13
|
+
|
|
14
|
+
function parseSemVer(value ) {
|
|
15
|
+
const match = value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
const prerelease = match[4] ? match[4].split('.') : [];
|
|
18
|
+
if (prerelease.some((identifier) => /^\d+$/.test(identifier)
|
|
19
|
+
&& identifier.length > 1 && identifier.startsWith('0'))) return null;
|
|
20
|
+
return { major: match[1], minor: match[2], patch: match[3], prerelease };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareNumeric(left , right ) {
|
|
24
|
+
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
|
|
25
|
+
if (left === right) return 0;
|
|
26
|
+
return left < right ? -1 : 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function compareSemVer(leftValue , rightValue ) {
|
|
30
|
+
const left = parseSemVer(leftValue);
|
|
31
|
+
const right = parseSemVer(rightValue);
|
|
32
|
+
if (!left || !right) return null;
|
|
33
|
+
|
|
34
|
+
for (const key of ['major', 'minor', 'patch'] ) {
|
|
35
|
+
const order = compareNumeric(left[key], right[key]);
|
|
36
|
+
if (order !== 0) return order;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0;
|
|
40
|
+
if (left.prerelease.length === 0) return 1;
|
|
41
|
+
if (right.prerelease.length === 0) return -1;
|
|
42
|
+
|
|
43
|
+
const length = Math.min(left.prerelease.length, right.prerelease.length);
|
|
44
|
+
for (let index = 0; index < length; index++) {
|
|
45
|
+
const leftId = left.prerelease[index];
|
|
46
|
+
const rightId = right.prerelease[index];
|
|
47
|
+
const leftNumeric = /^\d+$/.test(leftId);
|
|
48
|
+
const rightNumeric = /^\d+$/.test(rightId);
|
|
49
|
+
if (leftNumeric && rightNumeric) {
|
|
50
|
+
const order = compareNumeric(leftId, rightId);
|
|
51
|
+
if (order !== 0) return order;
|
|
52
|
+
} else if (leftNumeric !== rightNumeric) {
|
|
53
|
+
return leftNumeric ? -1 : 1;
|
|
54
|
+
} else if (leftId !== rightId) {
|
|
55
|
+
return leftId < rightId ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (left.prerelease.length === right.prerelease.length) return 0;
|
|
59
|
+
return left.prerelease.length < right.prerelease.length ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function refused(message ) {
|
|
63
|
+
return { ok: false, error: { code: 'bad-type', message, allowed: HOST_RANGE_GRAMMAR } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Never throws: a refused range comes back as { ok: false, error } for the caller to turn into a finding.
|
|
67
|
+
function parseHostRange(text ) {
|
|
68
|
+
if (typeof text !== 'string') return refused('the host range must be a string');
|
|
69
|
+
const comparators = [];
|
|
70
|
+
for (const token of text.split(' ')) {
|
|
71
|
+
const match = token.match(/^(>=|<|=)?(.+)$/);
|
|
72
|
+
if (!match || parseSemVer(match[2]) === null) {
|
|
73
|
+
return refused(`"${token}" is not a comparator of the host range grammar`);
|
|
74
|
+
}
|
|
75
|
+
comparators.push({ op: (match[1] || '=') , version: match[2] });
|
|
76
|
+
}
|
|
77
|
+
if (!comparators.some((comparator) => comparator.op !== '>=')) {
|
|
78
|
+
return refused('the host range has no upper bound');
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, comparators };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `range` is the range text or a parseHostRange result; a refused range or an invalid version satisfies nothing.
|
|
84
|
+
function satisfiesHostRange(version , range ) {
|
|
85
|
+
const parsed = typeof range === 'string' ? parseHostRange(range) : range;
|
|
86
|
+
if (!parsed.ok) return false;
|
|
87
|
+
return parsed.comparators.every(({ op, version: bound }) => {
|
|
88
|
+
const order = compareSemVer(version, bound);
|
|
89
|
+
if (order === null) return false;
|
|
90
|
+
return op === '>=' ? order >= 0 : op === '<' ? order < 0 : order === 0;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { parseSemVer, compareSemVer, parseHostRange, satisfiesHostRange, HOST_RANGE_GRAMMAR };
|
package/src/install/backup.js
CHANGED
|
@@ -24,7 +24,7 @@ const { sourceCommit: gitSourceCommit } = require('../helper/git.js');
|
|
|
24
24
|
const {
|
|
25
25
|
legacyBackupReadRoot, isLegacyBackupReadRoot, scopeRootFromCanonicalBackupRoot,
|
|
26
26
|
} = require('./paths.js');
|
|
27
|
-
const { validateLedger } = require('../state/index.js');
|
|
27
|
+
const { validateLedger, ownershipKey } = require('../state/index.js');
|
|
28
28
|
|
|
29
29
|
/** Where a listed/restorable backup was found. 'current' = the canonical `.doflow/backups` root the
|
|
30
30
|
* installer writes to; 'legacy' = the pre-7de6d5f `.claude/backups` root, readable only. Callers
|
|
@@ -69,6 +69,8 @@ function backupId(op , date ) {
|
|
|
69
69
|
const MANIFEST_FILE = '.manifest.json';
|
|
70
70
|
const OWNER_FILE = '.owner.json';
|
|
71
71
|
const TEMP_PREFIX = '.tmp-';
|
|
72
|
+
/** A backup retention is removing: renamed here first so a half-removed backup is never listed or counted. */
|
|
73
|
+
const REMOVING_PREFIX = '.tmp-del-';
|
|
72
74
|
const STALE_TEMP_MS = 24 * 60 * 60 * 1000;
|
|
73
75
|
const MAX_ID_ATTEMPTS = 1000;
|
|
74
76
|
const RENAME_COLLISION_CODES = new Set(['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES']);
|
|
@@ -129,7 +131,9 @@ function createFileBackup({
|
|
|
129
131
|
fsImpl.mkdirSync(backupRoot, { recursive: true });
|
|
130
132
|
fsImpl.mkdirSync(tmpDir);
|
|
131
133
|
tmpCreated = true;
|
|
132
|
-
|
|
134
|
+
// When this run started, not `date`: that is the time the backup is named for, and a caller may pass any
|
|
135
|
+
// date. isStaleTemp measures a temp's age from this field.
|
|
136
|
+
const owner = { pid: process.pid, hostname: os.hostname(), startedAt: new Date().toISOString() };
|
|
133
137
|
fsImpl.writeFileSync(path.join(tmpDir, OWNER_FILE), `${JSON.stringify(owner)}\n`, { flag: 'wx' });
|
|
134
138
|
} catch (err) { throw writeFailure(err); }
|
|
135
139
|
|
|
@@ -234,6 +238,35 @@ function defaultIsAlive(pid ) {
|
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
|
|
241
|
+
/**
|
|
242
|
+
* The id of this process's pid namespace where the platform exposes one (Linux: the target of
|
|
243
|
+
* `/proc/self/ns/pid`, such as `pid:[4026531836]`), else the empty string. A pid means something only
|
|
244
|
+
* inside the namespace that issued it; two containers can share a hostname and not a pid space.
|
|
245
|
+
*/
|
|
246
|
+
function pidNamespaceId() {
|
|
247
|
+
try { return fs.readlinkSync('/proc/self/ns/pid'); } catch { return ''; }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Eight hex digits that identify the host and pid namespace in a rename-aside name. A pid means
|
|
252
|
+
* something only in the namespace it was issued in, so an aside is judged by pid only by a process
|
|
253
|
+
* in the same namespace on the same host. Where no namespace id exists the tag is the hostname alone.
|
|
254
|
+
*/
|
|
255
|
+
function hostTag(hostname = os.hostname(), pidNamespace = pidNamespaceId()) {
|
|
256
|
+
return crypto.createHash('sha1').update(`${hostname}\0${pidNamespace}`).digest('hex').slice(0, 8);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The owner a rename-aside folder (`.tmp-del-<id>-<host tag>-<pid>-<8 hex>`) names, or null when the
|
|
261
|
+
* name does not carry one. Ids contain `-`, so the fields are read from the end. Names without a host
|
|
262
|
+
* tag (an earlier layout that never shipped: it existed only on unreleased commits, so no migration)
|
|
263
|
+
* are unparseable and are kept.
|
|
264
|
+
*/
|
|
265
|
+
function removingOwner(name ) {
|
|
266
|
+
const m = /-([0-9a-f]{8})-([0-9]+)-[0-9a-f]{8}$/.exec(name);
|
|
267
|
+
return m ? { host: m[1], pid: Number(m[2]) } : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
237
270
|
function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } ) {
|
|
238
271
|
let owner = null;
|
|
239
272
|
try { owner = JSON.parse(fsImpl.readFileSync(path.join(dir, OWNER_FILE), 'utf8')); } catch { /* no owner */ }
|
|
@@ -247,15 +280,25 @@ function isStaleTemp(dir , { now, fsImpl, isAlive, hostname } )
|
|
|
247
280
|
/**
|
|
248
281
|
* Remove `.tmp-*` directories left in `backupRoot` by runs that died: the owner process is gone on
|
|
249
282
|
* this host, or the temp is older than 24 hours. Returns how many were removed.
|
|
283
|
+
*
|
|
284
|
+
* A `.tmp-del-*` rename-aside is a backup a retention pass is deleting, so age says nothing about it
|
|
285
|
+
* (it keeps the backup's own, possibly old, mtime and may be rolled back if the delete fails). It is
|
|
286
|
+
* removed only by the host and pid namespace named in it, and only when the pid in its name is not
|
|
287
|
+
* alive; any other host's or namespace's aside, and one whose name carries no owner, is left alone.
|
|
250
288
|
*/
|
|
251
|
-
function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname() } = {}) {
|
|
289
|
+
function sweepStaleTemps(backupRoot , { now = Date.now(), fsImpl = fs, isAlive = defaultIsAlive, hostname = os.hostname(), pidNamespace = pidNamespaceId() } = {}) {
|
|
252
290
|
assertMutableBackupRoot(backupRoot, 'clean up backups');
|
|
253
291
|
if (!fsImpl.existsSync(backupRoot)) return 0;
|
|
254
292
|
let removed = 0;
|
|
255
293
|
for (const e of fsImpl.readdirSync(backupRoot, { withFileTypes: true })) {
|
|
256
294
|
if (!e.isDirectory() || !e.name.startsWith(TEMP_PREFIX)) continue;
|
|
257
295
|
const dir = path.join(backupRoot, e.name);
|
|
258
|
-
if (
|
|
296
|
+
if (e.name.startsWith(REMOVING_PREFIX)) {
|
|
297
|
+
// Removed only by the host that made it, and only once its pid is gone there. Another host's
|
|
298
|
+
// (or an unparseable) aside is kept: its pass may be live where this one cannot see it.
|
|
299
|
+
const owner = removingOwner(e.name);
|
|
300
|
+
if (owner === null || owner.host !== hostTag(hostname, pidNamespace) || isAlive(owner.pid)) continue;
|
|
301
|
+
} else if (!isStaleTemp(dir, { now, fsImpl, isAlive, hostname })) continue;
|
|
259
302
|
try {
|
|
260
303
|
fsImpl.rmSync(dir, { recursive: true, force: true });
|
|
261
304
|
removed += 1;
|
|
@@ -347,7 +390,7 @@ function formatBytes(n ) {
|
|
|
347
390
|
|
|
348
391
|
/** Security guard: a backup id is a directory *name*, never a path — reject traversal/absolute ids. */
|
|
349
392
|
function assertSafeBackupId(bid ) {
|
|
350
|
-
if (!bid || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
|
|
393
|
+
if (!bid || bid === '.' || bid.includes('/') || bid.includes('\\') || bid === '..' || bid.includes('..')) {
|
|
351
394
|
throw new Error(`Invalid backup id: '${bid}'`);
|
|
352
395
|
}
|
|
353
396
|
}
|
|
@@ -441,6 +484,9 @@ function refusalOf(entry , target , { scope, scopeRoot, bkDir, fsImpl
|
|
|
441
484
|
} catch {
|
|
442
485
|
return 'content missing from backup';
|
|
443
486
|
}
|
|
487
|
+
// A restore chmods the file it writes to this mode, so a manifest that lacks one (or holds anything
|
|
488
|
+
// but a permission mask) is refused here, with a name, rather than failing inside the write.
|
|
489
|
+
if (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777) return 'invalid mode';
|
|
444
490
|
return null;
|
|
445
491
|
}
|
|
446
492
|
|
|
@@ -473,6 +519,8 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
473
519
|
untargeted: 0,
|
|
474
520
|
snapshot: [],
|
|
475
521
|
v1: null,
|
|
522
|
+
// --target narrows the ledger with the files: the retired-harness rule of mergeRestoredLedger is off.
|
|
523
|
+
narrowed: Boolean(explicitTargets),
|
|
476
524
|
};
|
|
477
525
|
const wanted = new Set(targets);
|
|
478
526
|
|
|
@@ -535,13 +583,25 @@ function planRestore({ bid, backupRoot, scope, scopeRoot, targets, explicitTarge
|
|
|
535
583
|
* the backup does not hold and rows of MCP entries (a file holding only MCP entries is never backed
|
|
536
584
|
* up) stay as they are; restoring the whole old ledger would make DoFlow forget files and entries it
|
|
537
585
|
* still has on disk. With no current ledger the backup's is taken whole.
|
|
586
|
+
*
|
|
587
|
+
* A harness in `retired` (retired in the registry now, row 127) is the exception: every row of it
|
|
588
|
+
* and its `targets` entry come back as the backup holds them. A sweep that retired it released some
|
|
589
|
+
* rows with no file effect (a file a live harness still claims), so no file of theirs is in the backup,
|
|
590
|
+
* yet the run dropped them. Only the sweep ever changes a retired row, so the backup's copy is never
|
|
591
|
+
* staler than the current one; a current row of it the backup lacks stays. A restored row whose file
|
|
592
|
+
* a live harness now claims is released by the next sweep (claim rule 2), file untouched.
|
|
538
593
|
*/
|
|
539
|
-
function mergeRestoredLedger(backup , current , restored ) {
|
|
594
|
+
function mergeRestoredLedger(backup , current , restored , retired = new Set()) {
|
|
540
595
|
if (!current) return backup;
|
|
541
596
|
const at = (target ) => typeof target === 'string' && restored.has(path.resolve(target));
|
|
597
|
+
const whole = (row ) => retired.has(row?.harness);
|
|
598
|
+
const back = backup.resources.filter((r ) => at(r.target) || whole(r));
|
|
599
|
+
const backKeys = new Set(back.filter(whole).map(ownershipKey));
|
|
600
|
+
const targets = Object.entries(backup.targets ?? {}).filter(([id]) => retired.has(id));
|
|
542
601
|
return {
|
|
543
602
|
...current,
|
|
544
|
-
|
|
603
|
+
...(targets.length ? { targets: { ...current.targets, ...Object.fromEntries(targets) } } : {}),
|
|
604
|
+
resources: [...current.resources.filter((r ) => !at(r.target) && !(whole(r) && backKeys.has(ownershipKey(r)))), ...back],
|
|
545
605
|
tombstones: [
|
|
546
606
|
...(current.tombstones ?? []).filter((t ) => !at(t.fromTarget)),
|
|
547
607
|
...(backup.tombstones ?? []).filter((t ) => at(t.fromTarget)),
|
|
@@ -549,21 +609,32 @@ function mergeRestoredLedger(backup , current , restored )
|
|
|
549
609
|
};
|
|
550
610
|
}
|
|
551
611
|
|
|
612
|
+
/** Ids the registry retires (row 127 IC-001): `registry` when a caller passes one, else the shipped
|
|
613
|
+
* registry. One that cannot be read retires nothing, which leaves the merge per restored file. */
|
|
614
|
+
function retiredIds(registry ) {
|
|
615
|
+
if (registry === undefined) {
|
|
616
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
617
|
+
try { registry = require('../registry/index.js').loadRegistry({ repoRoot: REPO_ROOT }); } catch { registry = null; }
|
|
618
|
+
}
|
|
619
|
+
return new Set((Array.isArray(registry?.retired) ? registry.retired : []).map((entry ) => entry?.id).filter((id ) => typeof id === 'string'));
|
|
620
|
+
}
|
|
621
|
+
|
|
552
622
|
/** The bytes a ledger restore item writes: the backup's ledger merged onto the current one. */
|
|
553
|
-
function restoredLedgerBytes(item , restored , fsImpl ) {
|
|
623
|
+
function restoredLedgerBytes(item , restored , retired , fsImpl ) {
|
|
554
624
|
const backup = validateLedger(JSON.parse(fsImpl.readFileSync(item.stored, 'utf8')));
|
|
555
625
|
const current = fsImpl.existsSync(item.path) ? validateLedger(JSON.parse(fsImpl.readFileSync(item.path, 'utf8'))) : null;
|
|
556
|
-
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored)), null, 2)}\n`);
|
|
626
|
+
return Buffer.from(`${JSON.stringify(validateLedger(mergeRestoredLedger(backup, current, restored, retired)), null, 2)}\n`);
|
|
557
627
|
}
|
|
558
628
|
|
|
559
629
|
/**
|
|
560
630
|
* Carry out a plan from planRestore. Format 2 writes each restore item through a temp file renamed
|
|
561
631
|
* over the target (or straight through a symlinked target, which stays a link); a failed item is
|
|
562
632
|
* reported and the rest continue. The ownership ledger goes last and restores only the rows of the
|
|
563
|
-
* files written back before it
|
|
633
|
+
* files written back before it, plus every row of a retired harness unless --target narrowed the
|
|
634
|
+
* restore (mergeRestoredLedger); `registry` is for tests, omitted the shipped one is read. It never deletes a file and never writes
|
|
564
635
|
* outside `plan.restore`. A 1.19 plan goes through restoreBackup unchanged.
|
|
565
636
|
*/
|
|
566
|
-
function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs } ) {
|
|
637
|
+
function executeRestore(plan , { backupRoot, dryRun = false, registry, fsImpl = fs } ) {
|
|
567
638
|
if (plan.format !== 2) {
|
|
568
639
|
const toolDirs = Object.fromEntries(plan.v1.tools.map((t ) => [t.tool, t.dstDir]));
|
|
569
640
|
restoreBackup({ bid: plan.bid, backupRoot, dirs: toolDirs, dryRun });
|
|
@@ -583,7 +654,7 @@ function executeRestore(plan , { backupRoot, dryRun = false, fsImpl = fs }
|
|
|
583
654
|
try {
|
|
584
655
|
const dir = path.dirname(item.path);
|
|
585
656
|
fsImpl.mkdirSync(dir, { recursive: true });
|
|
586
|
-
const bytes = item.ledger ? restoredLedgerBytes(item, files, fsImpl) : fsImpl.readFileSync(item.stored);
|
|
657
|
+
const bytes = item.ledger ? restoredLedgerBytes(item, files, plan.narrowed ? new Set() : retiredIds(registry), fsImpl) : fsImpl.readFileSync(item.stored);
|
|
587
658
|
let isLink = false;
|
|
588
659
|
try {
|
|
589
660
|
isLink = fsImpl.lstatSync(item.path).isSymbolicLink();
|
|
@@ -685,7 +756,12 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
|
|
|
685
756
|
const protectedCandidates = candidates.filter((c) => protectedIds.has(c.id));
|
|
686
757
|
const ordered = [...protectedCandidates, ...candidates.filter((c) => !protectedIds.has(c.id))];
|
|
687
758
|
const keepCount = keep === 0 ? ordered.length : Math.max(keep - reserve, protectedCandidates.length);
|
|
688
|
-
|
|
759
|
+
// The protected backup is the run's own, so one newer than it was made by another run that finished
|
|
760
|
+
// after it; this pass only reaps history older than its own backup.
|
|
761
|
+
const newestProtected = protectedCandidates[0];
|
|
762
|
+
const removal = ordered.slice(keepCount)
|
|
763
|
+
.filter((c) => !newestProtected || compareSortKeys(c, newestProtected) >= 0)
|
|
764
|
+
.map((c) => c.id);
|
|
689
765
|
|
|
690
766
|
if (dryRun || keep === 0) {
|
|
691
767
|
if (dryRun) result.wouldRemove = removal;
|
|
@@ -693,16 +769,30 @@ function applyRetention({ backupRoot, keep, protect = [], reserve = 0, dryRun =
|
|
|
693
769
|
return result;
|
|
694
770
|
}
|
|
695
771
|
|
|
772
|
+
// Also deletes what an earlier pass renamed aside and died before deleting (a `.tmp-del-*` whose pid
|
|
773
|
+
// is dead): already out of every listing, so it goes at once. One whose pid is alive belongs to a
|
|
774
|
+
// pass that may still roll it back, so it is never touched here, whatever its age.
|
|
696
775
|
result.sweptTemps = sweepStaleTemps(backupRoot, { now, fsImpl });
|
|
697
776
|
for (const id of removal) {
|
|
698
777
|
const dir = path.join(backupRoot, id);
|
|
699
778
|
// Another run's retention pass may have removed it already.
|
|
700
779
|
if (!fsImpl.existsSync(dir)) continue;
|
|
780
|
+
// One rename takes the backup out of the listing whole; the deletion then happens out of sight.
|
|
781
|
+
const aside = path.join(backupRoot, `${REMOVING_PREFIX}${id}-${hostTag()}-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
701
782
|
try {
|
|
702
|
-
fsImpl.
|
|
783
|
+
fsImpl.renameSync(dir, aside);
|
|
784
|
+
} catch (err ) {
|
|
785
|
+
if (err.code !== 'ENOENT') result.failed.push({ id, error: err.message });
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
fsImpl.rmSync(aside, { recursive: true, force: true });
|
|
703
790
|
result.removed.push(id);
|
|
704
791
|
} catch (err ) {
|
|
792
|
+
// Not removed: put it back under its id so it is still counted and listed, as when it was
|
|
793
|
+
// removed in place. If that fails too the aside folder is not listed and the next pass deletes it.
|
|
705
794
|
result.failed.push({ id, error: err.message });
|
|
795
|
+
try { fsImpl.renameSync(aside, dir); } catch { /* the next pass cleans the aside folder */ }
|
|
706
796
|
}
|
|
707
797
|
}
|
|
708
798
|
result.kept = ordered.filter((c) => fsImpl.existsSync(path.join(backupRoot, c.id))).length;
|
|
@@ -718,7 +808,7 @@ function retentionNote(prune , keep ) {
|
|
|
718
808
|
|
|
719
809
|
module.exports = {
|
|
720
810
|
backupId, restoreBackup, listBackups, assertSafeBackupId, scopeRelative, retentionNote,
|
|
721
|
-
backupReadRoots, BACKUP_ORIGIN_CURRENT, BACKUP_ORIGIN_LEGACY,
|
|
811
|
+
backupReadRoots, hostTag, BACKUP_ORIGIN_CURRENT, BACKUP_ORIGIN_LEGACY,
|
|
722
812
|
BackupError, DEFAULT_BACKUP_RETENTION, createFileBackup, sweepStaleTemps, classifyBackupDir,
|
|
723
813
|
sortKey, backupSize, formatBytes, planRestore, executeRestore, mergeRestoredLedger, applyRetention,
|
|
724
814
|
};
|
package/src/install/context.js
CHANGED
|
@@ -46,7 +46,7 @@ function printContext(ctx ) {
|
|
|
46
46
|
'[CONTEXT] Resolved install context',
|
|
47
47
|
` scope : ${ctx.scope} (root: ${ctx.scopeRoot})`,
|
|
48
48
|
` targets : ${ctx.targets.join(', ')}`,
|
|
49
|
-
...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t]}`),
|
|
49
|
+
...ctx.targets.map((t ) => ` ${t.padEnd(7)}-> ${ctx.dirs[t] ?? '(resolved by adapter)'}`),
|
|
50
50
|
` repo root : ${ctx.repoRoot}`,
|
|
51
51
|
` source commit : ${ctx.sourceCommit}`,
|
|
52
52
|
` node : ${ctx.nodeVersion}`,
|
package/src/install/targets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// targets.js — target validation + install-dir resolution.
|
|
3
|
-
// PARITY: sync.sh is global-only — $HOME/.{claude,codex
|
|
3
|
+
// PARITY: sync.sh is global-only — $HOME/.{claude,codex}, hardcoded, NOT CLAUDE_CONFIG_DIR.
|
|
4
4
|
// doflow extends this with a second, DoFlow-only scope: project-local install (this repo's own
|
|
5
5
|
// .claude/ is a real precedent — Claude Code, Codex, and Copilot all support project-scoped config
|
|
6
6
|
// alongside global, see agent-docs/research/context-setup-claude-codex-copilot.md).
|
|
@@ -15,25 +15,43 @@
|
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
|
|
18
|
-
const VALID = ['claude', 'codex', '
|
|
18
|
+
const VALID = ['claude', 'codex', 'copilot', 'kiro', 'opencode', 'pi', 'antigravity'];
|
|
19
19
|
|
|
20
20
|
/** The target used when `--target` is omitted. Deliberately one harness, not all of VALID: an
|
|
21
21
|
* install with no flag should touch the one tool the user almost certainly has, not write
|
|
22
22
|
* configuration into every harness DoFlow knows about — several of which may not be installed at
|
|
23
23
|
* all. That mattered less when VALID was three entries and matters more as it grows. Ask for the
|
|
24
|
-
* rest explicitly with `--target claude,codex,
|
|
24
|
+
* rest explicitly with `--target claude,codex,antigravity`. */
|
|
25
25
|
const DEFAULT_TARGETS = ['claude'];
|
|
26
26
|
|
|
27
27
|
const TARGET_ALIASES = {
|
|
28
28
|
agy: 'antigravity',
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/** The registry's retired record for `id`, or null. The registry is read only for an id outside
|
|
32
|
+
* VALID, and a registry that cannot be read leaves the id unknown rather than failing differently. */
|
|
33
|
+
function retiredTarget(id , registry ) {
|
|
34
|
+
const { loadRegistry, retiredFor } = require('../registry/index.js');
|
|
35
|
+
if (!registry) {
|
|
36
|
+
const { REPO_ROOT } = require('../helper/repo-root.js');
|
|
37
|
+
try { registry = loadRegistry({ repoRoot: REPO_ROOT }); } catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
return retiredFor(registry, id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Default to DEFAULT_TARGETS; validate any explicitly requested. A retired id (row 127 IC-002) is
|
|
43
|
+
* refused like an unknown one, naming its replacement. `registry` is for tests; omitted, the
|
|
44
|
+
* shipped registry is read. */
|
|
45
|
+
function resolveTargets(requested , { registry } = {}) {
|
|
33
46
|
const rawTargets = requested && requested.length ? requested : [...DEFAULT_TARGETS];
|
|
34
47
|
const targets = rawTargets.map((t) => TARGET_ALIASES[t] || t);
|
|
35
48
|
for (const t of targets) {
|
|
36
49
|
if (!VALID.includes(t)) {
|
|
50
|
+
const retired = retiredTarget(t, registry);
|
|
51
|
+
if (retired) {
|
|
52
|
+
const alias = Object.keys(TARGET_ALIASES).find((key) => TARGET_ALIASES[key] === retired.replacement);
|
|
53
|
+
throw new Error(`Target '${t}' was retired on ${retired.retiredOn}; use '${retired.replacement}'${alias ? ` (alias ${alias})` : ''}. Its DoFlow files are released by the next 'doflow install' or 'doflow update' in that scope.`);
|
|
54
|
+
}
|
|
37
55
|
throw new Error(`Unknown target: '${t}' (valid: ${VALID.join(', ')})`);
|
|
38
56
|
}
|
|
39
57
|
}
|
|
@@ -47,16 +65,14 @@ function resolveTargets(requested ) {
|
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* Map tool -> install root.
|
|
50
|
-
* global=true -> $HOME/.{claude,codex,
|
|
51
|
-
* (sync.sh parity for claude/codex
|
|
52
|
-
*
|
|
53
|
-
* global=false (dflt) -> <resolved projectRoot>/.{claude,codex,kiro,opencode,pi}, but .
|
|
54
|
-
*
|
|
55
|
-
*
|
|
68
|
+
* global=true -> $HOME/.{claude,codex,copilot,kiro,pi}, $HOME/.config/opencode
|
|
69
|
+
* (sync.sh parity for claude/codex; the other harnesses follow each one's
|
|
70
|
+
* own documented global root — see below)
|
|
71
|
+
* global=false (dflt) -> <resolved projectRoot>/.{claude,codex,kiro,opencode,pi}, but .github for
|
|
72
|
+
* copilot — deliberately asymmetric with its own global-scope directory, for reasons native to
|
|
73
|
+
* the harness (see per-key notes below). antigravity has no entry: its adapter resolves its
|
|
74
|
+
* own paths from core/registry/harnesses.json.
|
|
56
75
|
*
|
|
57
|
-
* gemini -> project-scoped Gemini config follows the Antigravity customization convention, not
|
|
58
|
-
* `.gemini/`, so it is asymmetric with its own global directory and with the other
|
|
59
|
-
* targets.
|
|
60
76
|
* copilot -> Copilot has no single native root the way Claude's `.claude` is one: instructions,
|
|
61
77
|
* skills, agents, and MCP each live under a different path (see design.md §4's
|
|
62
78
|
* native-surfaces table). `.github` is the project root DoFlow treats as Copilot's
|
|
@@ -67,7 +83,7 @@ function resolveTargets(requested ) {
|
|
|
67
83
|
* the clean, symmetric case.
|
|
68
84
|
* opencode -> Global config is `~/.config/opencode`, NOT `~/.opencode/` — a path DoFlow's own
|
|
69
85
|
* docs asserted incorrectly for several releases (see src/adapters/opencode/index.js).
|
|
70
|
-
* Project scope is `.opencode`, asymmetric with the global path the same way
|
|
86
|
+
* Project scope is `.opencode`, asymmetric with the global path the same way copilot's
|
|
71
87
|
* is.
|
|
72
88
|
* pi -> Global config is `~/.pi/agent` — note the `agent/` segment; `~/.pi/settings.json`
|
|
73
89
|
* is not a location Pi reads. Project scope is `.pi` (see
|
|
@@ -79,7 +95,6 @@ function toolDirs(opts = {}) {
|
|
|
79
95
|
return {
|
|
80
96
|
claude: path.join(root, '.claude'),
|
|
81
97
|
codex: path.join(root, '.codex'),
|
|
82
|
-
gemini: global ? path.join(root, '.gemini') : path.join(root, '.agents'),
|
|
83
98
|
copilot: global ? path.join(root, '.copilot') : path.join(root, '.github'),
|
|
84
99
|
kiro: path.join(root, '.kiro'),
|
|
85
100
|
opencode: global ? path.join(root, '.config', 'opencode') : path.join(root, '.opencode'),
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('node:crypto');
|
|
4
4
|
const CLASS_ORDER = Object.freeze(['unknown', 'unsupported', 'observation', 'advisory', 'blocking']);
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const HARNESS_ORDER = Object.freeze(['claude', 'codex', 'opencode', 'pi', 'copilot', 'kiro', 'antigravity']);
|
|
6
|
+
const BEHAVIOUR_ORDER = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
|
|
7
7
|
const NEVER_BLOCKING_EVENTS = new Set([
|
|
8
8
|
'pi\u0000tool_execution_start', 'antigravity\u0000PostToolUse', 'antigravity\u0000PreInvocation',
|
|
9
|
-
'antigravity\u0000PostInvocation', '
|
|
9
|
+
'antigravity\u0000PostInvocation', 'claude\u0000PostToolUse', 'codex\u0000PostToolUse',
|
|
10
10
|
]);
|
|
11
11
|
function stable(value ) {
|
|
12
12
|
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
@@ -42,16 +42,4 @@ function enforcementFor(coverage , query
|
|
|
42
42
|
reason: weakest ? `${weakest.class} at (${weakest.policy}, ${weakest.harness}, ${weakest.event}, ${weakest.profile})` : `unknown at (${query.policy}, ${query.harness}, *, ${query.profile ?? '*'})`,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
const order = (items , value ) => {
|
|
47
|
-
const index = items.indexOf(value);
|
|
48
|
-
return index < 0 ? items.length : index;
|
|
49
|
-
};
|
|
50
|
-
const rows = [...coverage.rows].sort((a , b ) => order(POLICY_ORDER, a.policy) - order(POLICY_ORDER, b.policy)
|
|
51
|
-
|| order(HARNESS_ORDER, a.harness) - order(HARNESS_ORDER, b.harness)
|
|
52
|
-
|| a.event.localeCompare(b.event) || a.profile.localeCompare(b.profile));
|
|
53
|
-
const lines = [`Schema ${coverage.schemaVersion}; revision ${coverage.revision}; ${coverage.rowsDigest}`, '| Policy | Harness | Native event | Profile | Class | Verification | Prerequisites | Does not cover |', '|---|---|---|---|---|---|---|---|'];
|
|
54
|
-
for (const row of rows) lines.push(`| ${row.policy} | ${row.harness} | ${row.event} | ${row.profile} | ${row.class} | ${row.verification} | ${(row.prerequisites || []).join(', ')} | ${(row.doesNotCover || []).join(', ')} |`);
|
|
55
|
-
return lines.join('\n');
|
|
56
|
-
}
|
|
57
|
-
module.exports = { enforcementFor, renderEnforcementMatrix, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|
|
45
|
+
module.exports = { enforcementFor, BEHAVIOUR_ORDER, HARNESS_ORDER, rowsDigest, UnknownEnforcementTarget, NEVER_BLOCKING_EVENTS };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
|
|
4
|
+
|
|
5
|
+
// Hook tiers (maint-hooks). Content (guidance, skills, agents, MCP) goes to every harness; DoFlow's
|
|
6
|
+
// enforcement (hooks, gates, the bridge) goes only to the harnesses the `hooks.harnesses` setting
|
|
7
|
+
// names. A harness outside that list is planned against a registry copy in which its `hooks`
|
|
8
|
+
// capability is unavailable, so the same seams that already handle a harness without hooks drop
|
|
9
|
+
// its hook assets, render its lifecycle policies as guidance fallbacks and skip its bespoke hook
|
|
10
|
+
// config; the removal of what an earlier install wrote is planned from the ledger, as an uninstall
|
|
11
|
+
// of those resources alone.
|
|
12
|
+
|
|
13
|
+
const HOOKS_KEY = 'hooks.harnesses';
|
|
14
|
+
const TIER_REASON = `not in ${HOOKS_KEY}`;
|
|
15
|
+
const WORKERS_KEY = 'workers.enabled';
|
|
16
|
+
|
|
17
|
+
/** The hook tier plus the worker harnesses the person configured (`workers.enabled` set in a settings
|
|
18
|
+
* file; null when it is the shipped default), for the run's warnings (maint-hooks R-4). */
|
|
19
|
+
function resolveHookTier({ env = process.env, loadSettings } = {}) {
|
|
20
|
+
if (!loadSettings) {
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const { configHome } = require('../runtime/config-home.js');
|
|
24
|
+
const dir = configHome(env).dir;
|
|
25
|
+
if (!dir || !fs.existsSync(path.join(dir, 'settings.json'))) {
|
|
26
|
+
const registry = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'core', 'registry', 'settings.json'), 'utf8'));
|
|
27
|
+
return { harnesses: [...registry.keys[HOOKS_KEY].default], workers: null };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const load = loadSettings ?? require('../runtime/settings/resolve.js').loadSettings;
|
|
31
|
+
const view = load({ global: true, env: { ...env }, only: [HOOKS_KEY, WORKERS_KEY] });
|
|
32
|
+
const resolution = view.get(HOOKS_KEY, 'global');
|
|
33
|
+
const workers = view.get(WORKERS_KEY, 'global');
|
|
34
|
+
const configured = Array.isArray(workers?.value) && workers?.origin?.layer && workers.origin.layer !== 'default';
|
|
35
|
+
return {
|
|
36
|
+
harnesses: Array.isArray(resolution?.value) ? [...resolution.value] : [],
|
|
37
|
+
workers: configured ? workers.value.filter((id ) => typeof id === 'string' && id !== 'primary').map((id ) => id.split(/[:/]/)[0]) : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One WARN line per planned harness that gets no hooks although it is the primary session (claude)
|
|
42
|
+
* or a worker the person configured: there it runs without DoFlow's gates. */
|
|
43
|
+
function hookTierWarnings(registry , targets , tier ) {
|
|
44
|
+
const warnings = [];
|
|
45
|
+
for (const harness of registry.harnesses) {
|
|
46
|
+
if (!targets.includes(harness.id) || tier.harnesses.includes(harness.id) || !getsHooks(registry, harness)) continue;
|
|
47
|
+
const role = harness.id === 'claude' ? 'the primary session' : tier.workers?.includes(harness.id) ? `a worker in ${WORKERS_KEY}` : null;
|
|
48
|
+
if (role) warnings.push(`${harness.id} is ${role} but not in ${HOOKS_KEY}: it runs without DoFlow's hooks or gates`);
|
|
49
|
+
}
|
|
50
|
+
return warnings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether a harness is outside the hook tier. `hookHarnesses` undefined means no tier was resolved
|
|
54
|
+
* (an in-process caller): every harness keeps its hooks, as before the setting existed. */
|
|
55
|
+
function outsideHookTier(harnessId , hookHarnesses ) {
|
|
56
|
+
return Array.isArray(hookHarnesses) && !hookHarnesses.includes(harnessId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Whether DoFlow installs any hook for a harness: its hooks are supported, or a hook asset applies
|
|
60
|
+
* to it (Pi's guard extension). OpenCode gets none, so the tier leaves it as it is. */
|
|
61
|
+
function getsHooks(registry , harness ) {
|
|
62
|
+
const hooks = harness.capabilities?.hooks;
|
|
63
|
+
if (!hooks || hooks.status === 'unavailable') return false;
|
|
64
|
+
if (hooks.status === 'supported') return true;
|
|
65
|
+
const { selectAssets } = require('../registry/index.js');
|
|
66
|
+
return selectAssets(registry, { harness: harness.id, capability: 'hooks' }).length > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The registry a harness outside the tier is planned against: the same object graph, with that
|
|
70
|
+
* harness's `hooks` capability unavailable. A harness DoFlow installs no hook for, or that is in the
|
|
71
|
+
* tier, is returned unchanged. */
|
|
72
|
+
function tieredRegistry(registry , hookHarnesses ) {
|
|
73
|
+
if (!Array.isArray(hookHarnesses)) return registry;
|
|
74
|
+
let changed = false;
|
|
75
|
+
const harnesses = registry.harnesses.map((harness ) => {
|
|
76
|
+
const hooks = harness.capabilities?.hooks;
|
|
77
|
+
if (!outsideHookTier(harness.id, hookHarnesses) || !getsHooks(registry, harness)) return harness;
|
|
78
|
+
changed = true;
|
|
79
|
+
return { ...harness, capabilities: { ...harness.capabilities, hooks: { ...hooks, status: 'unavailable', reason: TIER_REASON, tiered: true } } };
|
|
80
|
+
});
|
|
81
|
+
return changed ? { ...registry, harnesses } : registry;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The harness ids whose hooks a removal must take: those the ledger records hook resources for. A
|
|
85
|
+
* removal plans what an install wrote, so a harness installed outside the tier (no hook rows) plans
|
|
86
|
+
* no hook removal, and never claims a shared hook file another harness still owns. */
|
|
87
|
+
function hookHoldersFromLedger(registry , ledger ) {
|
|
88
|
+
const { selectAssets } = require('../registry/index.js');
|
|
89
|
+
const rows = ledger?.resources ?? [];
|
|
90
|
+
return registry.harnesses.filter((harness ) => {
|
|
91
|
+
const hookIds = new Set (selectAssets(registry, { harness: harness.id, capability: 'hooks' }).map((asset ) => asset.id));
|
|
92
|
+
return rows.some((row ) => row?.harness === harness.id && isHookResource(row, hookIds));
|
|
93
|
+
}).map((harness ) => harness.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Whether a (possibly tiered) harness declaration had its hooks switched off by the tier. */
|
|
97
|
+
function hooksTieredOff(harness ) {
|
|
98
|
+
return harness?.capabilities?.hooks?.tiered === true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The context keys that hand an adapter a bespoke hook source (Codex hooks.json). A harness outside the tier is planned without them, and with `hooksOff` set for the
|
|
102
|
+
* adapters whose hooks ride a non-hook asset (Claude's settings.json). */
|
|
103
|
+
const HOOK_SOURCE_KEYS = ['codexHooksSourceFile', 'codexHooksSourceDir'];
|
|
104
|
+
|
|
105
|
+
function withoutHookSources(context ) {
|
|
106
|
+
const next = { ...context, hooksOff: true };
|
|
107
|
+
for (const key of HOOK_SOURCE_KEYS) delete next[key];
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Renderers and resource kinds that only ever carry hook state, whatever asset id they ride on
|
|
112
|
+
* (Codex files its bespoke hook resources under a guidance asset id). */
|
|
113
|
+
const HOOK_RENDERERS = new Set(['codex-hooks', 'antigravity-hooks', 'pi-guard-extension']);
|
|
114
|
+
const HOOK_KINDS = new Set(['hooks-file', 'hooks-script']);
|
|
115
|
+
|
|
116
|
+
function isHookResource(item , hookIds ) {
|
|
117
|
+
return hookIds.has(item?.assetId) || item?.nativeComponent === 'hooks' || HOOK_KINDS.has(item?.kind)
|
|
118
|
+
|| HOOK_RENDERERS.has(item?.projection?.renderer);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add to a harness's content plan the removal of the hooks an earlier install wrote for it, planned by
|
|
123
|
+
* the adapter's own remove path against the full registry and kept to hook changes only. Nothing is
|
|
124
|
+
* planned when the ledger holds no hook row for the harness. The removals carry `hookTier: true`, so
|
|
125
|
+
* applyLifecycle runs them through `adapter.remove` with `hookTierInput`.
|
|
126
|
+
*/
|
|
127
|
+
function planHookTierRemovals({ target, registry, adapter, harness, scope, scopeRoot, ledger, context, normalizeChange, adapterConflicts } ) {
|
|
128
|
+
const { selectAssets } = require('../registry/index.js');
|
|
129
|
+
const { projectAdapterInput } = require('../adapters/index.js');
|
|
130
|
+
const { renderPolicies } = require('./policies.js');
|
|
131
|
+
if (harness.capabilities?.hooks?.status === 'unavailable') return target;
|
|
132
|
+
const hookIds = new Set (selectAssets(registry, { harness: harness.id, capability: 'hooks' }).map((asset ) => asset.id));
|
|
133
|
+
const rows = (ledger?.resources ?? []).filter((row ) => row?.harness === harness.id && isHookResource(row, hookIds));
|
|
134
|
+
if (!rows.length) return target;
|
|
135
|
+
const assets = selectAssets(registry, { harness: harness.id });
|
|
136
|
+
const policies = renderPolicies(registry, { harness: harness.id });
|
|
137
|
+
const input = projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp: [], mcpAdoptable: [], policies,
|
|
138
|
+
context: { ...context, operation: 'remove' } });
|
|
139
|
+
const discovery = adapter.discover({ ...input, registry, ledger });
|
|
140
|
+
const result = adapter.plan({ ...input, registry, ledger, discovery });
|
|
141
|
+
// A removal the content plan already carries (an adapter that plans its own hooks off) is not planned twice.
|
|
142
|
+
const planned = new Set(target.changes.map((change ) => `${change.ownershipIdentity}\0${change.target}`));
|
|
143
|
+
const removals = (result.changes || []).filter((change ) => isHookResource(change, hookIds)
|
|
144
|
+
&& !planned.has(`${change.ownershipIdentity ?? change.ownership}\0${change.target}`))
|
|
145
|
+
.map((change ) => Object.freeze({ ...normalizeChange(change, { harness, assets }), hookTier: true }));
|
|
146
|
+
const conflicts = adapterConflicts(result, harness).filter((reason ) => /hook/i.test(String(reason)));
|
|
147
|
+
if (!removals.length && !conflicts.length) return target;
|
|
148
|
+
const used = new Set(removals.map((change ) => change.assetId));
|
|
149
|
+
const extraAssets = assets.filter((asset ) => used.has(asset.id) && !target.assets.some((own ) => own.id === asset.id));
|
|
150
|
+
const changes = [...target.changes, ...removals];
|
|
151
|
+
const requiredNativeResources = target.requiredNativeResources === target.changes ? changes : [...target.requiredNativeResources, ...removals];
|
|
152
|
+
return {
|
|
153
|
+
...target, assets: [...target.assets, ...extraAssets], changes, requiredNativeResources,
|
|
154
|
+
conflicts: [...target.conflicts, ...conflicts],
|
|
155
|
+
notices: [...target.notices, `removing ${harness.id} hooks: ${TIER_REASON} (content stays)`],
|
|
156
|
+
hookTierInput: input,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { TIER_REASON, resolveHookTier, hookTierWarnings, tieredRegistry, hookHoldersFromLedger, hooksTieredOff, withoutHookSources, planHookTierRemovals };
|