@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
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `doflow package` (122 IC-012). Read-only, on the no-upkeep list: `points` lists the contribution points;
|
|
5
|
+
* `check --path <dir>` prints the IC-004 findings of a package directory and its digests; `list [--id]` and
|
|
6
|
+
* `show --id` print the package view (IC-010). Writing: `add --source <spec>` and `update --id <id>` write a plan
|
|
7
|
+
* file and nothing else; `add --plan <planId>` and `update --plan <planId>` apply it (store and lock row, see
|
|
8
|
+
* store.ts) and run storage upkeep like other writers. Exit 0 done, listed or clean, 1 the point registry is
|
|
9
|
+
* unusable, a package is neither `active` nor `disabled`, an id is unknown, the package has errors or an apply
|
|
10
|
+
* is refused, 2 usage; later actions land in the tasks that own them.
|
|
11
|
+
*
|
|
12
|
+
* The verb actions (B.6): `enable`, `disable`, `trust`, `exempt`, `revoke` and `verify [--repair]` are open to any
|
|
13
|
+
* caller, never prompt or wait (beyond the global run lock), and each appends one audit row to `state.json` under the
|
|
14
|
+
* scope's `run.lock`. `enable` and `disable` set the lock bit; `trust` and `exempt` only print the line a person
|
|
15
|
+
* appends to `grants.jsonl` (DoFlow never writes it); `revoke` and `verify` write `state.json`. Each emits one
|
|
16
|
+
* `package.verb` telemetry event (`action`, `result`, `tier_max`, no id or path). `remove` (B.7) is the seventh: it
|
|
17
|
+
* deletes the package and prints what still names it. An id no package carries writes nothing for any of them.
|
|
18
|
+
*
|
|
19
|
+
* Projection (C.3, IC-017): `enable`, `disable`, `revoke`, `verify`, `remove` and a digest-changing `update --plan` bring
|
|
20
|
+
* the projected skills and agents of every third-party package in line with the view (`syncProjection`), through the
|
|
21
|
+
* lifecycle at global scope for the harnesses of the global lock's `targets`; `list` and `show` name the harnesses that
|
|
22
|
+
* cannot take a package's skills or agents, and say when no harness is installed at global scope.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { finishRuntime, captureCaught } = require('../cli-result.js');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
const { loadPoints, pointRows } = require('./points.js');
|
|
28
|
+
const { checkPackage, MANIFEST_FILE } = require('./manifest.js');
|
|
29
|
+
const { scaffoldPackage } = require('./scaffold.js');
|
|
30
|
+
const { REPO_ROOT } = require('../../helper/repo-root.js');
|
|
31
|
+
const { packageView, resetPackageView, hostVersion } = require('./view.js');
|
|
32
|
+
const { readLock, writeLock, removeLock, defaultLock, lockPackages, LOCK_VERSION_BASE } = require('../../state/lockfile.js');
|
|
33
|
+
const fs = require('node:fs');
|
|
34
|
+
const os = require('node:os');
|
|
35
|
+
const { planPackage, planRemote, applyPlan, removeEntry, PLAN_ID } = require('./store.js');
|
|
36
|
+
const { readIndex, pickVersion } = require('./index-file.js');
|
|
37
|
+
const { configHome } = require('../config-home.js');
|
|
38
|
+
const { GRANTS_FILE, readGrants, duplicateKey } = require('./grants.js');
|
|
39
|
+
const { parseSource, fetchGrantLine } = require('./source.js');
|
|
40
|
+
const { acquireRunLock } = require('../../state/run-lock.js');
|
|
41
|
+
const { readState, appendAudit, recordRevocation, recordSuspension, clearSuspension, recordTombstone, repairState, grantRevoked } = require('./state.js');
|
|
42
|
+
const { capabilityDigest } = require('./digest.js');
|
|
43
|
+
const { satisfiesHostRange } = require('../../helper/semver.js');
|
|
44
|
+
const { isPackageId } = require('../../helper/package-ids.js');
|
|
45
|
+
const { TelemetryWriter } = require('../trace/telemetry.js');
|
|
46
|
+
// The settings readers load only for `package remove`'s reference scan, so `install` and every other verb stay off them (settings-hot-path).
|
|
47
|
+
|
|
48
|
+
const TIER_ORDER = ['inert', 'instruction', 'executable'];
|
|
49
|
+
const VERB_ACTIONS = ['enable', 'disable', 'trust', 'exempt', 'revoke', 'verify', 'remove'];
|
|
50
|
+
|
|
51
|
+
function pointLine(row ) {
|
|
52
|
+
return `point ${row.id}: tier=${row.tier} status=${row.status} entry=${row.entry} versions=${row.versions.join(',') || '-'}`
|
|
53
|
+
+ (row.opensWith ? ` opensWith=${row.opensWith}` : '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findingLine(f ) {
|
|
57
|
+
return `${f.severity} ${f.code} ${f.field}: ${f.message}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `check --path <dir>`: the findings and digests of a package directory; writes nothing. */
|
|
61
|
+
function handleCheck({ pkgPath, json, repoRoot, out } ) {
|
|
62
|
+
if (typeof pkgPath !== 'string' || pkgPath.length === 0) {
|
|
63
|
+
const message = 'check requires one --path <dir>';
|
|
64
|
+
out(json ? JSON.stringify({ ok: false, error: 'usage', message }) : `doflow package: ${message}`);
|
|
65
|
+
return finishRuntime(2);
|
|
66
|
+
}
|
|
67
|
+
const root = repoRoot || REPO_ROOT;
|
|
68
|
+
const dir = path.resolve(pkgPath);
|
|
69
|
+
const result = checkPackage(dir, { repoRoot: root, hostVersion: hostVersion(root), bundled: [] });
|
|
70
|
+
if (json) {
|
|
71
|
+
out(JSON.stringify({ path: dir, ok: result.ok, findings: result.findings, ...(result.contentDigest ? { contentDigest: result.contentDigest, capabilityDigest: result.capabilityDigest } : {}) }));
|
|
72
|
+
} else {
|
|
73
|
+
for (const f of result.findings) out(findingLine(f));
|
|
74
|
+
out(result.ok ? `package ok: ${dir}` : `package refused: ${dir}`);
|
|
75
|
+
if (result.ok) { out(`contentDigest ${result.contentDigest}`); out(`capabilityDigest ${result.capabilityDigest}`); }
|
|
76
|
+
}
|
|
77
|
+
return finishRuntime(result.ok ? 0 : 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stateLine(p ) {
|
|
81
|
+
return `package ${p.id} ${p.version ?? '-'} ${p.state}${p.firstParty ? ' first-party' : ''}${p.enabled ? '' : ' disabled'}`
|
|
82
|
+
+ (p.reasons.length > 0 ? ` reasons=${p.reasons.join(',')}` : '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The global lock's `version`; 1 with no lock, null for an unreadable one. Read-only. */
|
|
86
|
+
function globalLockVersion(homeDir ) {
|
|
87
|
+
try { return readLock({ scope: 'global', ...(homeDir === undefined ? {} : { homeDir }) })?.version ?? LOCK_VERSION_BASE; } catch { return null; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The one line `list` prints when the global lock is version 2 (IC-007): a DoFlow older than the lock format refuses to read it. `--json` keeps its key set (the Console contract); its `lockVersion` carries the same fact. */
|
|
91
|
+
const DOWNGRADE_LIMIT = 'the global lock is version 2 because it records packages: an older DoFlow cannot read this lock and refuses it';
|
|
92
|
+
|
|
93
|
+
/** The view options a handler was given (`undefined` ones fall back to the view's own defaults). */
|
|
94
|
+
const viewOptions = (o ) => ({ repoRoot: o.repoRoot, homeDir: o.homeDir, env: o.env, projectRoot: o.projectRoot, tmpDir: o.tmpDir, settingsView: o.settingsView });
|
|
95
|
+
|
|
96
|
+
/** A fresh package view: the process memo is cleared first, because a verb or a person just changed what it reads. */
|
|
97
|
+
function freshView(o ) {
|
|
98
|
+
resetPackageView();
|
|
99
|
+
return packageView(viewOptions(o));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The grant lines an `active` third-party package runs on (IC-008: every run that uses a grant prints the line it
|
|
104
|
+
* matched, the file path and its mtime). Empty for a first-party or inert package, and for any other state.
|
|
105
|
+
*/
|
|
106
|
+
function matchedGrants(pkg , o , cache = {}) {
|
|
107
|
+
if (pkg.firstParty || pkg.state !== 'active' || !(pkg.grant?.trust || pkg.grant?.exempt)) return [];
|
|
108
|
+
cache.read ??= readGrants({ env: o.env, projectRoot: o.projectRoot, ...(o.tmpDir === undefined ? {} : { tmpDir: o.tmpDir }) });
|
|
109
|
+
const kinds = [pkg.grant.trust ? 'trust' : null, pkg.grant.exempt ? 'exempt' : null];
|
|
110
|
+
return cache.read.grants
|
|
111
|
+
.filter((g ) => kinds.includes(g.grant) && g.package === pkg.id && g.version === pkg.version && g.contentDigest === pkg.contentDigest
|
|
112
|
+
&& (g.grant !== 'trust' || g.capabilityDigest === pkg.capabilityDigest))
|
|
113
|
+
.map((g ) => ({ kind: g.grant, line: g.raw, lineNumber: g.line, path: cache.read.path, mtime: cache.read.mtime }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const matchedText = (matched ) => matched.flatMap((m ) => [
|
|
117
|
+
`matched ${m.kind} grant (line ${m.lineNumber} of the grants file):`, m.line, `grants file ${m.path}, modified ${m.mtime}`,
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `list [--id]` and `show --id`: the package view, read-only. `lockVersion` is the `version` of the global lock
|
|
122
|
+
* on disk, 1 when there is none (the version written while no package is recorded) and null when it cannot be
|
|
123
|
+
* read; the lock is only read here, never written. `list` exits 1 when a package is neither
|
|
124
|
+
* `active` nor `disabled` or any finding is an error (a corrupt point registry among them); an unknown id is exit 1.
|
|
125
|
+
* Text output also prints the grant line, file path and mtime an active trusted package runs on; `--json` keeps the
|
|
126
|
+
* Console contract unchanged.
|
|
127
|
+
*/
|
|
128
|
+
function handleView(o ) {
|
|
129
|
+
const { action, id, json, out } = o;
|
|
130
|
+
if (action === 'show' && (typeof id !== 'string' || id.length === 0)) {
|
|
131
|
+
const message = 'show requires one --id <id>';
|
|
132
|
+
out(json ? JSON.stringify({ ok: false, error: 'usage', message }) : `doflow package: ${message}`);
|
|
133
|
+
return finishRuntime(2);
|
|
134
|
+
}
|
|
135
|
+
const view = freshView(o);
|
|
136
|
+
const wanted = typeof id === 'string' ? view.packages.filter((p ) => p.id === id) : view.packages;
|
|
137
|
+
if (typeof id === 'string' && wanted.length === 0) {
|
|
138
|
+
const message = `no package with id '${id}'`;
|
|
139
|
+
out(json ? JSON.stringify({ ok: false, error: 'unknown-package', message }) : `doflow package: ${message}`);
|
|
140
|
+
return finishRuntime(1);
|
|
141
|
+
}
|
|
142
|
+
const findings = typeof id === 'string' ? view.findings.filter((f ) => f.package === id) : view.findings;
|
|
143
|
+
const cache = {};
|
|
144
|
+
const targets = json ? [] : globalTargets(o);
|
|
145
|
+
if (action === 'show') {
|
|
146
|
+
if (json) out(JSON.stringify(wanted[0]));
|
|
147
|
+
else {
|
|
148
|
+
out(stateLine(wanted[0]));
|
|
149
|
+
for (const line of matchedText(matchedGrants(wanted[0], o, cache))) out(line);
|
|
150
|
+
for (const line of reachLines(o, wanted[0], targets)) out(line);
|
|
151
|
+
}
|
|
152
|
+
return finishRuntime(0);
|
|
153
|
+
}
|
|
154
|
+
const lockVersion = globalLockVersion(o.homeDir);
|
|
155
|
+
const v2 = lockVersion !== null && lockVersion > LOCK_VERSION_BASE;
|
|
156
|
+
if (json) out(JSON.stringify({ points: view.points, packages: wanted, findings, lockVersion }));
|
|
157
|
+
else {
|
|
158
|
+
for (const p of wanted) {
|
|
159
|
+
out(stateLine(p));
|
|
160
|
+
for (const line of matchedText(matchedGrants(p, o, cache))) out(line);
|
|
161
|
+
for (const line of reachLines(o, p, targets)) out(line);
|
|
162
|
+
}
|
|
163
|
+
if (wanted.length === 0) out('no packages');
|
|
164
|
+
for (const f of findings) out(findingLine(f));
|
|
165
|
+
if (v2) out(DOWNGRADE_LIMIT);
|
|
166
|
+
}
|
|
167
|
+
const unhealthy = wanted.some((p ) => p.state !== 'active' && p.state !== 'disabled');
|
|
168
|
+
return finishRuntime(unhealthy || findings.some((f ) => f.severity === 'error') ? 1 : 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function usage(json , out , message ) {
|
|
172
|
+
out(json ? JSON.stringify({ ok: false, error: 'usage', message }) : `doflow package: ${message}`);
|
|
173
|
+
return finishRuntime(2);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function refusal(json , out , findings , what ) {
|
|
177
|
+
if (json) out(JSON.stringify({ ok: false, error: findings[0]?.code ?? 'refused', findings }));
|
|
178
|
+
else { for (const f of findings) out(findingLine(f)); out(what); }
|
|
179
|
+
return finishRuntime(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Compare source objects by keys and values, ignoring key order. */
|
|
183
|
+
function sameSource(left , right ) {
|
|
184
|
+
const leftKeys = Object.keys(left).sort();
|
|
185
|
+
const rightKeys = Object.keys(right).sort();
|
|
186
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && left[key] === right[key]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Local and granted remote adds converge on the same digest-bound plan and apply. */
|
|
190
|
+
function handlePlan({ action, source, index, packageVersion, id, json, repoRoot, homeDir, env, projectRoot, tmpDir, now, registry, out } ) {
|
|
191
|
+
let remote = null;
|
|
192
|
+
let pinned = null;
|
|
193
|
+
let expected ;
|
|
194
|
+
let label = typeof source === 'string' ? source : id;
|
|
195
|
+
if (action === 'add') {
|
|
196
|
+
if (index !== undefined) {
|
|
197
|
+
if (typeof source === 'string') return usage(json, out, 'add takes --source or --index, not both');
|
|
198
|
+
if (typeof index !== 'string' || index.length === 0 || typeof id !== 'string' || id.length === 0) {
|
|
199
|
+
return usage(json, out, 'add --index requires one --index <file> and --id <id>');
|
|
200
|
+
}
|
|
201
|
+
const read = readIndex(index);
|
|
202
|
+
if (!read.ok) return refusal(json, out, read.findings, `package refused: ${index}`);
|
|
203
|
+
const picked = pickVersion(read.index, id, { version: packageVersion, host: hostVersion(repoRoot || REPO_ROOT) });
|
|
204
|
+
if (!picked.ok) return refusal(json, out, picked.findings, `package refused: ${index}`);
|
|
205
|
+
pinned = picked.entry.source;
|
|
206
|
+
expected = { id, version: picked.entry.version };
|
|
207
|
+
remote = pinned.type === 'git'
|
|
208
|
+
? { type: 'git', url: pinned.url, ref: pinned.commit, ...(pinned.path === undefined ? {} : { path: pinned.path }) }
|
|
209
|
+
: { type: 'npm', name: pinned.name, version: pinned.version };
|
|
210
|
+
label = `${index} (${id} ${picked.entry.version})`;
|
|
211
|
+
} else {
|
|
212
|
+
if (packageVersion !== undefined) return usage(json, out, '--version <v> requires --index');
|
|
213
|
+
if (typeof source !== 'string' || source.length === 0) return usage(json, out, 'add requires one --source <spec>, --index <file> --id <id>, or --plan <planId>');
|
|
214
|
+
const parsed = parseSource(source);
|
|
215
|
+
if (!parsed.ok) return refusal(json, out, [parsed.finding], `package refused: ${source}`);
|
|
216
|
+
if (parsed.source.type !== 'local') remote = parsed.source;
|
|
217
|
+
else {
|
|
218
|
+
let isDir = false;
|
|
219
|
+
try { isDir = fs.statSync(path.resolve(source)).isDirectory(); } catch { /* not a directory */ }
|
|
220
|
+
if (!isDir) return usage(json, out, `--source must be a local package directory (an absolute or cwd-relative path to an existing directory); '${source}' is not one`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (remote) {
|
|
224
|
+
const line = fetchGrantLine(remote, grantTime(now ?? Date.now()));
|
|
225
|
+
const grants = readGrants({ env, projectRoot, ...(tmpDir === undefined ? {} : { tmpDir }) });
|
|
226
|
+
const matched = grants.grants.some((grant ) => grant.grant === 'fetch' && sameSource(grant.source, remote));
|
|
227
|
+
if (!matched) {
|
|
228
|
+
const finding = { package: null, field: 'source', code: 'grant-missing', severity: 'error', message: `no fetch grant names ${label}`, allowed: line };
|
|
229
|
+
if (json) out(JSON.stringify({ ok: false, error: 'grant-missing', findings: [finding], grantLine: line }));
|
|
230
|
+
else {
|
|
231
|
+
out(findingLine(finding));
|
|
232
|
+
const dir = configHome(env).dir;
|
|
233
|
+
out(dir === null ? 'there is no config home, so no fetch grant can exist' : `a person appends this line to ${path.join(dir, GRANTS_FILE)} (DoFlow never writes that file):`);
|
|
234
|
+
out(line);
|
|
235
|
+
out(`package refused: ${label}`);
|
|
236
|
+
}
|
|
237
|
+
return finishRuntime(1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
if (index !== undefined || packageVersion !== undefined) return usage(json, out, '--index and --version <v> are for add');
|
|
242
|
+
if (typeof id !== 'string' || id.length === 0) return usage(json, out, 'update requires one --id <id> or one --plan <planId>');
|
|
243
|
+
}
|
|
244
|
+
const result = remote
|
|
245
|
+
? planRemote({ source: remote, pinned, expected, homeDir, repoRoot, env, now, registry })
|
|
246
|
+
: planPackage({ action, source: typeof source === 'string' ? path.resolve(source) : undefined, id, homeDir, repoRoot, registry });
|
|
247
|
+
if (!result.ok) return refusal(json, out, result.findings, `package refused: ${remote ? label : typeof source === 'string' ? path.resolve(source) : id}`);
|
|
248
|
+
const { plan, planFile } = result;
|
|
249
|
+
const prior = plan.grantLines.trust ? priorGrant({ env, projectRoot, tmpDir, homeDir }, plan.grantLines.trust) : null;
|
|
250
|
+
if (json) {
|
|
251
|
+
const said = prior?.status === 'granted' ? { alreadyGranted: prior.lineNumber } : prior?.status === 'replace' ? { replaceLine: prior.lineNumber } : {};
|
|
252
|
+
out(JSON.stringify({ ok: true, planFile, plan, ...said }));
|
|
253
|
+
return finishRuntime(0);
|
|
254
|
+
}
|
|
255
|
+
if (plan.source.type === 'git') out(`pinned ${plan.source.url} ${plan.source.commit}`);
|
|
256
|
+
if (plan.source.type === 'npm') out(`pinned ${plan.source.name}@${plan.source.version}`);
|
|
257
|
+
const from = plan.source.type === 'local' ? plan.source.path : plan.source.type === 'git' ? plan.source.url : plan.source.name;
|
|
258
|
+
out(`plan ${plan.planId}: ${action} ${plan.package.id} ${plan.package.version} from ${from}`);
|
|
259
|
+
out(`written ${planFile} (a plan changes no record and activates nothing; DoFlow keeps it for 24 hours)`);
|
|
260
|
+
for (const c of plan.inventory) out(` ${c.point} ${c.qualifiedId} ${c.tier}`);
|
|
261
|
+
if (plan.inventory.length === 0) out(' no contributions');
|
|
262
|
+
for (const f of plan.findings) out(findingLine(f));
|
|
263
|
+
for (const [harness, row] of Object.entries (plan.projection)) {
|
|
264
|
+
const names = ['skill', 'agent'].flatMap((point) => (row[point] ?? []).map((name ) => `${point} ${name}`));
|
|
265
|
+
out(` projection ${harness}: ${names.length ? names.join(', ') : 'nothing'} (${row.listingBytes} listing bytes of always-loaded context)`);
|
|
266
|
+
}
|
|
267
|
+
for (const w of result.projectionWarnings ?? []) out(`warning: ${w}`);
|
|
268
|
+
if (result.instructions > 0 && Object.keys(plan.projection).length === 0) out(NO_GLOBAL(plan.package.id));
|
|
269
|
+
out(`contentDigest ${plan.contentDigest}`);
|
|
270
|
+
out(`capabilityDigest ${plan.capabilityDigest}`);
|
|
271
|
+
if (plan.grantLines.trust) {
|
|
272
|
+
const file = grantsPath(env);
|
|
273
|
+
const lines = file === null ? ['this package needs a trust grant, but there is no config home, so none can exist', plan.grantLines.trust]
|
|
274
|
+
: appendInstruction(env, plan.grantLines.trust, 'this package needs a trust grant', prior);
|
|
275
|
+
for (const line of lines) out(line);
|
|
276
|
+
}
|
|
277
|
+
out(`apply with: doflow package --action ${action} --plan ${plan.planId}`);
|
|
278
|
+
return finishRuntime(0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The ISO 8601 UTC time of a grant line: whole seconds, as the plan prints it (IC-008). */
|
|
282
|
+
const grantTime = (when ) => new Date(when).toISOString().replace(/\.\d+Z$/, 'Z');
|
|
283
|
+
|
|
284
|
+
/** The `trust` line for a package's recorded digests; key order is the IC-008 order the plan prints. */
|
|
285
|
+
function trustLineFor(row , policy , now , rebind ) {
|
|
286
|
+
const line = { grant: 'trust', package: row.id, version: row.version, contentDigest: row.contentDigest, capabilityDigest: row.capabilityDigest, policy, at: grantTime(now) };
|
|
287
|
+
if (rebind && rebind.length > 0) line.rebind = rebind;
|
|
288
|
+
return JSON.stringify(line);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The trust policy version the running DoFlow's point registry declares. */
|
|
292
|
+
function trustPolicy(repoRoot ) {
|
|
293
|
+
const loaded = loadPoints(repoRoot ? { repoRoot } : {});
|
|
294
|
+
return loaded.ok ? loaded.registry.trustPolicyVersion : 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The grants file path a person appends to, or null when no config home resolves (so no grant can exist). */
|
|
298
|
+
const grantsPath = (env ) => {
|
|
299
|
+
const dir = configHome(env).dir;
|
|
300
|
+
return dir === null ? null : path.join(dir, GRANTS_FILE);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* What the person does with a `trust` or `exempt` line DoFlow is about to print (IC-008, FU-92eftv), judged from the
|
|
305
|
+
* grants file as it reads now (`o`: `env`, `projectRoot`, `tmpDir`, `homeDir`): `new` (append it), `granted` (line
|
|
306
|
+
* `lineNumber` has its key and grants it) or `replace` (line `lineNumber` has its key but is revoked, or exempts another
|
|
307
|
+
* host: a second line with that key voids the file, so the printed line takes its place). A grants file that is
|
|
308
|
+
* unreadable or ignored grants nothing to judge: `new`, as before. Reads only.
|
|
309
|
+
*/
|
|
310
|
+
function priorGrant(o , line ) {
|
|
311
|
+
const grant = JSON.parse(line);
|
|
312
|
+
const read = readGrants({ env: o.env, projectRoot: o.projectRoot, ...(o.tmpDir === undefined ? {} : { tmpDir: o.tmpDir }) });
|
|
313
|
+
const same = read.grants.find((g ) => duplicateKey(g) === duplicateKey(grant));
|
|
314
|
+
if (!same) return { status: 'new' };
|
|
315
|
+
const holds = (grant.grant !== 'exempt' || same.host === grant.host)
|
|
316
|
+
&& !grantRevoked(readState({ homeDir: o.homeDir }), same)
|
|
317
|
+
&& (!grant.rebind || (Array.isArray(same.rebind) && grant.rebind.every((id ) => same.rebind.includes(id))));
|
|
318
|
+
return { status: holds ? 'granted' : 'replace', lineNumber: same.line };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The text for a line the person already has (`granted`) or puts in place of line N (`replace`); null for `new`. */
|
|
322
|
+
function priorText(file , line , prior , what ) {
|
|
323
|
+
if (prior.status === 'granted') return [`${what}; already granted by line ${prior.lineNumber} of ${file}`];
|
|
324
|
+
if (prior.status === 'replace') {
|
|
325
|
+
return [`${what}; a person edits that file (DoFlow never writes it): replace line ${prior.lineNumber} of ${file} with this line (a second line with the same key voids the file):`, line];
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The `--json` keys for a printed line: `alreadyGranted: N` and no line, or `grantLines` (plus `replaceLine: N`). */
|
|
331
|
+
const priorJson = (kind , line , prior ) => (prior.status === 'granted'
|
|
332
|
+
? { alreadyGranted: prior.lineNumber }
|
|
333
|
+
: { grantLines: { [kind]: line }, ...(prior.status === 'replace' ? { replaceLine: prior.lineNumber } : {}) });
|
|
334
|
+
|
|
335
|
+
/** `add --plan <planId>` and `update --plan <planId>`: apply a plan. Exit 0 applied, 1 refused or failed. */
|
|
336
|
+
function handleApply(o ) {
|
|
337
|
+
const { action, plan, json, repoRoot, homeDir, env, hooks, out } = o;
|
|
338
|
+
if (typeof plan !== 'string' || !PLAN_ID.test(plan)) return usage(json, out, '--plan takes a plan id printed by add or update (16 hex characters)');
|
|
339
|
+
try {
|
|
340
|
+
let before = new Map ();
|
|
341
|
+
try { before = new Map(lockPackages(readLock({ scope: 'global', homeDir })).map((r ) => [r.id, r.contentDigest])); } catch { /* the apply reports an unreadable lock */ }
|
|
342
|
+
const result = applyPlan({ planId: plan, action, homeDir, repoRoot, env, hooks });
|
|
343
|
+
if (!result.ok) return refusal(json, out, result.findings, `package refused: plan ${plan}`);
|
|
344
|
+
// FR-021: an update that changed the content digest above inert voids trust; print the line for the new digests.
|
|
345
|
+
let retrust = [];
|
|
346
|
+
let retrustLine = null;
|
|
347
|
+
let retrustNote = null;
|
|
348
|
+
let retrustPrior = { status: 'new' };
|
|
349
|
+
if (result.action === 'update' && before.has(result.id) && before.get(result.id) !== result.contentDigest) {
|
|
350
|
+
const pkg = freshView(o).packages.find((p ) => p.id === result.id);
|
|
351
|
+
if (pkg && TIER_ORDER.indexOf(pkg.tierMax) > 0) {
|
|
352
|
+
retrustLine = trustLineFor({ id: result.id, version: result.version, contentDigest: result.contentDigest, capabilityDigest: result.capabilityDigest }, trustPolicy(repoRoot), o.now);
|
|
353
|
+
retrustPrior = priorGrant(o, retrustLine);
|
|
354
|
+
const file = grantsPath(env);
|
|
355
|
+
retrust = file === null
|
|
356
|
+
? [`${result.id} acts again only after a person adds it, but there is no config home, so no grant can exist; the line would be:`, retrustLine]
|
|
357
|
+
: priorText(file, retrustLine, retrustPrior, retrustPrior.status === 'granted' ? `${result.id} needs no new trust line` : `${result.id} acts again only after a person adds it`)
|
|
358
|
+
?? [`${result.id} acts again only after a person adds it: append this line to ${file} (an older trust line for ${result.id} ${result.version} may stay: it no longer grants; DoFlow never writes that file):`, retrustLine];
|
|
359
|
+
retrustNote = file === null
|
|
360
|
+
? 'the package acts again only after a person adds that line to the grants file; there is no config home, so no grant can exist'
|
|
361
|
+
: retrustPrior.status === 'granted' ? null
|
|
362
|
+
: retrustPrior.status === 'replace' ? `the package acts again only after a person puts that line in place of line ${retrustPrior.lineNumber} of ${file}`
|
|
363
|
+
: `the package acts again only after a person adds that line to ${file}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// A changed content digest leaves the package without a valid grant above inert: its skills and agents go (IC-017).
|
|
367
|
+
let sync = null;
|
|
368
|
+
if (result.action === 'update' && before.has(result.id) && before.get(result.id) !== result.contentDigest) {
|
|
369
|
+
const hold = acquireRunLock({ scopeRoot: homeDir, scope: 'global', command: 'package update' });
|
|
370
|
+
try { sync = syncProjection(o); } finally { hold.release(); }
|
|
371
|
+
}
|
|
372
|
+
if (json) out(JSON.stringify({ ...result, ...(retrustLine ? { ...priorJson('trust', retrustLine, retrustPrior), grantFile: grantsPath(env), ...(retrustNote ? { note: retrustNote } : {}) } : {}), ...(sync ? projectionJson(sync) : {}) }));
|
|
373
|
+
else {
|
|
374
|
+
out(`${result.action === 'add' ? 'added' : 'updated'} ${result.id} ${result.version} ${result.enabled ? 'enabled' : 'disabled'}`);
|
|
375
|
+
out(`store ${result.storeDir}`);
|
|
376
|
+
out(`contentDigest ${result.contentDigest}`);
|
|
377
|
+
if (result.leftover) out('warning: the previous store entry could not be removed');
|
|
378
|
+
for (const line of retrust) out(line);
|
|
379
|
+
if (retrustNote) out(retrustNote);
|
|
380
|
+
for (const line of sync?.lines ?? []) out(line);
|
|
381
|
+
}
|
|
382
|
+
return finishRuntime(0);
|
|
383
|
+
} catch (error ) {
|
|
384
|
+
if (error?.name === 'RunLockTimeoutError' || error?.name === 'RunLockError') {
|
|
385
|
+
out(json ? JSON.stringify({ ok: false, error: 'run-lock', message: error.message }) : error.message);
|
|
386
|
+
return finishRuntime(1);
|
|
387
|
+
}
|
|
388
|
+
captureCaught(error, 'package', 1);
|
|
389
|
+
console.error(`[ERROR] package: ${error?.message || String(error)}`);
|
|
390
|
+
return finishRuntime(1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── projection of package skills and agents (C.3, IC-017) ───────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
/** The harnesses of the global lock's `targets`: where package skills and agents go. [] with no lock or an unreadable one. */
|
|
397
|
+
function globalTargets(o ) {
|
|
398
|
+
try { return (readLock({ scope: 'global', homeDir: o.homeDir })?.targets ?? []).map((t ) => t?.harness).filter((h ) => typeof h === 'string'); } catch { return []; }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The skill and agent contributions a package's view row lists. */
|
|
402
|
+
const instructionsOf = (pkg ) => (pkg?.contributions ?? []).filter((c ) => c.point === 'skill' || c.point === 'agent');
|
|
403
|
+
|
|
404
|
+
/** The line saying a package's skills and agents reach no harness, because DoFlow is not installed at global scope. */
|
|
405
|
+
const NO_GLOBAL = (id ) => `no harness is installed at global scope, so ${id}'s skills and agents are projected into none (a project-scope install never takes them)`;
|
|
406
|
+
|
|
407
|
+
/** The registry `list`, `show` and the projection read (`o.registry` is a test seam). */
|
|
408
|
+
const registryOf = (o ) => o.registry ?? require('../../registry/index.js').loadRegistry({ repoRoot: o.repoRoot ?? REPO_ROOT });
|
|
409
|
+
|
|
410
|
+
/** The harness-reach warnings (IC-017, NFR-003) of one package for `list` and `show`: a harness of the global lock that
|
|
411
|
+
* cannot take one of its skills or agents, or the one line saying no harness is installed at global scope. */
|
|
412
|
+
function reachLines(o , pkg , targets ) {
|
|
413
|
+
const entries = instructionsOf(pkg);
|
|
414
|
+
if (pkg.firstParty || entries.length === 0) return [];
|
|
415
|
+
if (targets.length === 0) return [NO_GLOBAL(pkg.id)];
|
|
416
|
+
const { projectionReach } = require('./store.js');
|
|
417
|
+
const registry = registryOf(o);
|
|
418
|
+
const lines = [];
|
|
419
|
+
for (const harness of targets) {
|
|
420
|
+
for (const c of entries) {
|
|
421
|
+
const reach = projectionReach(registry, harness, c.point);
|
|
422
|
+
if (!reach.ok) lines.push(`warning: ${harness} cannot take ${c.point} ${c.qualifiedId}: ${reach.reason}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return lines;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Bring the projected skills and agents of every third-party package in line with the package view (IC-017), under the
|
|
430
|
+
* run lock the caller holds: an active package's are projected into each harness of the global lock's `targets`, any
|
|
431
|
+
* other package's recorded files are removed (a person-edited copy is kept and named). It runs the lifecycle at global
|
|
432
|
+
* scope only, planning the package targets alone, so the installed runtime (no DoFlow content) can run it. A fault is a
|
|
433
|
+
* warning line and never changes the verb's exit code. Returns `{ lines, projected, unprojected, kept, warnings }`.
|
|
434
|
+
*/
|
|
435
|
+
function packageHasProjection(homeDir , packageId ) {
|
|
436
|
+
try {
|
|
437
|
+
const { stateRoot, readLedger } = require('../../state/index.js');
|
|
438
|
+
const root = stateRoot({ scope: 'global', projectRoot: homeDir, homeDir });
|
|
439
|
+
return (readLedger(root)?.resources ?? []).some((row ) =>
|
|
440
|
+
typeof row.assetId === 'string' && row.assetId.startsWith(`pkg.${packageId}.`));
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function syncProjection(o ) {
|
|
447
|
+
const result = { lines: [], projected: [], unprojected: [], kept: [], warnings: [] };
|
|
448
|
+
const targets = globalTargets(o);
|
|
449
|
+
if (targets.length === 0) return result;
|
|
450
|
+
try {
|
|
451
|
+
const lifecycle = require('../../lifecycle/index.js');
|
|
452
|
+
const { stateRoot, readLedger, defaultLedger } = require('../../state/index.js');
|
|
453
|
+
const registry = registryOf(o);
|
|
454
|
+
const root = stateRoot({ scope: 'global', projectRoot: o.homeDir, homeDir: o.homeDir });
|
|
455
|
+
const ledger = readLedger(root) ?? defaultLedger({ scope: 'global', scopeRoot: o.homeDir });
|
|
456
|
+
const adapters = require('../../cli/shared.js').buildAdapterRegistry();
|
|
457
|
+
resetPackageView();
|
|
458
|
+
const view = packageView({ ...viewOptions(o), settingsView: {} });
|
|
459
|
+
const plan = lifecycle.planLifecycle({ registry, adapters, scope: 'global', scopeRoot: o.homeDir, targets, ledger,
|
|
460
|
+
context: { packagesOnly: true, packageView: view, homeDir: o.homeDir, operation: 'package' } });
|
|
461
|
+
result.warnings = (plan.packageWarnings ?? []).map((w ) => w.warning);
|
|
462
|
+
if (plan.changes.length) lifecycle.applyLifecycle({ plan, registry, adapters, stateRoot: root, ledger, acceptPrerequisites: true });
|
|
463
|
+
const byId = (list ) => {
|
|
464
|
+
const grouped = new Map ();
|
|
465
|
+
for (const change of list) {
|
|
466
|
+
const id = change.assetId.slice('pkg.'.length);
|
|
467
|
+
if (!grouped.has(id)) grouped.set(id, new Set());
|
|
468
|
+
grouped.get(id) .add(change.harness);
|
|
469
|
+
}
|
|
470
|
+
return [...grouped].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([qualifiedId, harnesses]) => ({ qualifiedId, harnesses: [...harnesses].sort() }));
|
|
471
|
+
};
|
|
472
|
+
const live = plan.changes.filter((change ) => lifecycle.isPackageAsset(change.assetId) && !change.retained);
|
|
473
|
+
result.projected = byId(live.filter((change ) => change.operation !== 'remove'));
|
|
474
|
+
result.unprojected = byId(live.filter((change ) => change.operation === 'remove'));
|
|
475
|
+
result.kept = plan.targets.filter((t ) => t.packagesOnly).flatMap((t ) => t.notices);
|
|
476
|
+
} catch (error ) {
|
|
477
|
+
result.warnings.push(`the projection into harnesses failed: ${error?.message || String(error)}`);
|
|
478
|
+
}
|
|
479
|
+
result.lines = [
|
|
480
|
+
...result.projected.map((p ) => `projected ${p.qualifiedId} into ${p.harnesses.join(', ')}`),
|
|
481
|
+
...result.unprojected.map((p ) => `unprojected ${p.qualifiedId} from ${p.harnesses.join(', ')}`),
|
|
482
|
+
...result.kept, ...result.warnings.map((w ) => `warning: ${w}`),
|
|
483
|
+
];
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** The `projection` key a verb's `--json` output carries when the sync did or reported anything. */
|
|
488
|
+
function projectionJson(sync ) {
|
|
489
|
+
const { projected, unprojected, kept, warnings } = sync;
|
|
490
|
+
return projected.length || unprojected.length || kept.length || warnings.length ? { projection: { projected, unprojected, kept, warnings } } : {};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Whether the sync removed any file (the disable and revoke sentences depend on it). */
|
|
494
|
+
const removedAny = (sync ) => sync.unprojected.length > 0 || sync.kept.length > 0;
|
|
495
|
+
|
|
496
|
+
// ── verb actions (B.6): enable, disable, trust, exempt, revoke, verify ──────────────────────────────
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Lines telling a person what to do with `line` (`prior` from `priorGrant`): append it, replace line N with it, or
|
|
500
|
+
* nothing because line N already grants it; or that no grant can exist without a config home.
|
|
501
|
+
*/
|
|
502
|
+
function appendInstruction(env , line , what , prior = { status: 'new' }) {
|
|
503
|
+
const file = grantsPath(env);
|
|
504
|
+
if (file === null) return [`there is no config home (set XDG_CONFIG_HOME or HOME), so no grant can exist and ${what}; the line would be:`, line];
|
|
505
|
+
return priorText(file, line, prior, what) ?? [`${what}; a person appends this line to ${file} (DoFlow never writes that file):`, line];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const stamp = (o ) => new Date(o.now).toISOString();
|
|
509
|
+
|
|
510
|
+
/** One `audit` row; an unreadable `state.json` is reported by `verify`, never by the action that tried to log. */
|
|
511
|
+
function audit(o , verb , packageId , result ) {
|
|
512
|
+
try { appendAudit({ homeDir: o.homeDir }, { verb, packageId: isPackageId(packageId) ? packageId : '*', result, at: stamp(o) }); } catch { /* see verify */ }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** The lock row of a recorded package, or undefined. */
|
|
516
|
+
const rowOf = (o , id ) => lockPackages(readLock({ scope: 'global', homeDir: o.homeDir })).find((r ) => r.id === id);
|
|
517
|
+
|
|
518
|
+
/** The row that records the bundled package as disabled: closed shape (IC-007), digests derived as B.4 does. */
|
|
519
|
+
function bundledRow(o , pkg ) {
|
|
520
|
+
let capability = pkg.contentDigest;
|
|
521
|
+
try {
|
|
522
|
+
const loaded = loadPoints(o.repoRoot ? { repoRoot: o.repoRoot } : {});
|
|
523
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pkg.storeDir, MANIFEST_FILE), 'utf8'));
|
|
524
|
+
const tiers = Object.fromEntries(loaded.registry.points.map((p ) => [p.id, p.tier]));
|
|
525
|
+
capability = capabilityDigest(manifest, { trustPolicyVersion: loaded.registry.trustPolicyVersion, tiers });
|
|
526
|
+
} catch { /* the registry digest stands in: the row only has to record the bit */ }
|
|
527
|
+
return { id: pkg.id, version: pkg.version ?? hostVersion(o.repoRoot ?? REPO_ROOT) ?? '0.0.0', source: { type: 'bundled' }, contentDigest: pkg.contentDigest, capabilityDigest: capability, enabled: false, addedAt: grantTime(o.now) };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Set the lock bit of `pkg`. A bundled package is on by default: enabling drops its row, disabling adds one. Returns whether the lock changed. */
|
|
531
|
+
function setEnabled(o , pkg , enabled ) {
|
|
532
|
+
const lock = readLock({ scope: 'global', homeDir: o.homeDir });
|
|
533
|
+
const rows = lockPackages(lock);
|
|
534
|
+
const row = rows.find((r ) => r.id === pkg.id);
|
|
535
|
+
let next ;
|
|
536
|
+
if (pkg.firstParty) {
|
|
537
|
+
if (enabled) { if (!row) return false; next = rows.filter((r ) => r.id !== pkg.id); }
|
|
538
|
+
else { if (row && !row.enabled) return false; next = [...rows.filter((r ) => r.id !== pkg.id), bundledRow(o, pkg)]; }
|
|
539
|
+
} else {
|
|
540
|
+
if (!row || row.enabled === enabled) return false;
|
|
541
|
+
next = rows.map((r ) => (r.id === pkg.id ? { ...r, enabled } : r));
|
|
542
|
+
}
|
|
543
|
+
writeLock({ scope: 'global', homeDir: o.homeDir }, { ...(lock ?? defaultLock({ scope: 'global', scopeRoot: o.homeDir })), packages: next });
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** The unknown-id refusal shared by every id-taking verb: nothing is written, not even an audit row. */
|
|
548
|
+
function unknownId(o ) {
|
|
549
|
+
const message = `no package with id '${o.id}'`;
|
|
550
|
+
o.out(o.json ? JSON.stringify({ ok: false, error: 'unknown-package', message }) : `doflow package: ${message}`);
|
|
551
|
+
return finishRuntime(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const highestTier = (tiers ) => {
|
|
555
|
+
const ranks = tiers.map((t) => TIER_ORDER.indexOf(t )).filter((r) => r >= 0);
|
|
556
|
+
return ranks.length > 0 ? TIER_ORDER[Math.max(...ranks)] : null;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/** The printed result of `enable` and `disable`: the state, the trust line when one is owed, the matched grant lines. */
|
|
560
|
+
function reportState(o , after , extra = [], sync = null) {
|
|
561
|
+
const matched = matchedGrants(after, o);
|
|
562
|
+
const owes = after.state === 'needs-trust';
|
|
563
|
+
const row = owes ? rowOf(o, after.id) : null;
|
|
564
|
+
// The capability digest the view judges a grant against (recomputed under the running policy), not the lock's copy.
|
|
565
|
+
const line = row ? trustLineFor({ ...row, capabilityDigest: after.capabilityDigest }, trustPolicy(o.repoRoot), o.now) : null;
|
|
566
|
+
const prior = line ? priorGrant(o, line) : null;
|
|
567
|
+
const file = grantsPath(o.env);
|
|
568
|
+
if (o.json) {
|
|
569
|
+
o.out(JSON.stringify({ ok: true, action: o.action, id: after.id, state: after.state, reasons: after.reasons, enabled: after.enabled, matched,
|
|
570
|
+
...(line ? { ...priorJson('trust', line, prior), grantFile: file } : {}), ...(sync ? projectionJson(sync) : {}) }));
|
|
571
|
+
} else {
|
|
572
|
+
o.out(stateLine(after));
|
|
573
|
+
for (const text of extra) o.out(text);
|
|
574
|
+
if (line) {
|
|
575
|
+
o.out(`${after.id} is enabled but needs a trust grant (${after.reasons.join(', ')}); it acts only after a person adds the line`);
|
|
576
|
+
for (const text of appendInstruction(o.env, line, `${after.id} stays inactive until the grant exists`, prior)) o.out(text);
|
|
577
|
+
if (after.reasons.includes('state-unreadable')) o.out('state.json or the grants file is unreadable: see package --action verify --repair');
|
|
578
|
+
}
|
|
579
|
+
for (const text of matchedText(matched)) o.out(text);
|
|
580
|
+
for (const text of sync?.lines ?? []) o.out(text);
|
|
581
|
+
}
|
|
582
|
+
return finishRuntime(0);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function verbEnable(o , pkg , ctx ) {
|
|
586
|
+
if (pkg.state === 'incompatible') {
|
|
587
|
+
audit(o, 'enable', pkg.id, 'incompatible');
|
|
588
|
+
const message = `package ${pkg.id} cannot be enabled: incompatible (${pkg.reasons.join(', ')})`;
|
|
589
|
+
o.out(o.json ? JSON.stringify({ ok: false, error: 'incompatible', reasons: pkg.reasons, message }) : message);
|
|
590
|
+
return finishRuntime(1);
|
|
591
|
+
}
|
|
592
|
+
setEnabled(o, pkg, true);
|
|
593
|
+
// A package that is active now has its skills and agents projected; any other package is brought in line too (IC-017).
|
|
594
|
+
const sync = syncProjection(o);
|
|
595
|
+
const after = freshView(o).packages.find((p ) => p.id === pkg.id) ?? pkg;
|
|
596
|
+
ctx.tierMax = after.tierMax;
|
|
597
|
+
audit(o, 'enable', pkg.id, after.state);
|
|
598
|
+
if (instructionsOf(after).length && !after.firstParty && globalTargets(o).length === 0) sync.lines.push(NO_GLOBAL(after.id));
|
|
599
|
+
return reportState(o, after, [], sync);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function verbDisable(o , pkg , ctx ) {
|
|
603
|
+
setEnabled(o, pkg, false);
|
|
604
|
+
const sync = syncProjection(o);
|
|
605
|
+
const after = freshView(o).packages.find((p ) => p.id === pkg.id) ?? pkg;
|
|
606
|
+
ctx.tierMax = after.tierMax;
|
|
607
|
+
audit(o, 'disable', pkg.id, after.state);
|
|
608
|
+
return reportState(o, after, [removedAny(sync) ? 'the grant is kept' : 'the grant is kept; nothing is projected into a harness yet, so there is nothing to unproject'], sync);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function verbTrust(o , pkg ) {
|
|
612
|
+
audit(o, 'trust', pkg.id, 'ok');
|
|
613
|
+
if (pkg.firstParty) {
|
|
614
|
+
o.out(o.json ? JSON.stringify({ ok: true, action: 'trust', id: pkg.id, required: false }) : `${pkg.id} is a bundled first-party package and needs no grant`);
|
|
615
|
+
return finishRuntime(0);
|
|
616
|
+
}
|
|
617
|
+
const row = rowOf(o, pkg.id);
|
|
618
|
+
const stateRead = readState({ homeDir: o.homeDir });
|
|
619
|
+
const tombstone = (stateRead.state?.removed || []).find((r ) => r.package === pkg.id);
|
|
620
|
+
let rebind ;
|
|
621
|
+
let rebindLines = [];
|
|
622
|
+
if (tombstone) {
|
|
623
|
+
const workerContributions = (pkg.contributions || []).filter((c ) => c.point === 'worker').map((c ) => c.qualifiedId);
|
|
624
|
+
const tombstonedIds = workerContributions.filter((id ) => Array.isArray(tombstone.qualifiedIds) && tombstone.qualifiedIds.includes(id));
|
|
625
|
+
if (tombstonedIds.length > 0) {
|
|
626
|
+
const grants = readGrants({ env: o.env, projectRoot: o.projectRoot, ...(o.tmpDir === undefined ? {} : { tmpDir: o.tmpDir }) });
|
|
627
|
+
const trustLines = grants.grants.filter((g ) => g.grant === 'trust' && g.package === pkg.id && Array.isArray(g.rebind));
|
|
628
|
+
const latestRebind = trustLines.find((g ) => Date.parse(g.at) > Date.parse(tombstone.at) && tombstonedIds.every((id ) => g.rebind.includes(id)));
|
|
629
|
+
if (!latestRebind) {
|
|
630
|
+
rebind = tombstonedIds;
|
|
631
|
+
const scan = scanReferences(o, new Set(tombstonedIds), ['workers.billing', 'workers.enabled', 'workers.models']);
|
|
632
|
+
for (const d of scan.dangling.filter((item ) => item.kind === 'setting')) {
|
|
633
|
+
rebindLines.push(`rebind setting would apply again: ${d.layer} ${d.key} = ${JSON.stringify(d.value)}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// The capability digest the view judges a grant against (recomputed under the running policy), not the lock's copy.
|
|
639
|
+
const line = trustLineFor({ ...row, capabilityDigest: pkg.capabilityDigest }, trustPolicy(o.repoRoot), o.now, rebind);
|
|
640
|
+
const prior = priorGrant(o, line);
|
|
641
|
+
const required = TIER_ORDER.indexOf(pkg.tierMax) > 0;
|
|
642
|
+
if (o.json) {
|
|
643
|
+
o.out(JSON.stringify({ ok: true, action: 'trust', id: pkg.id, required, ...(rebind ? { rebind } : {}), ...priorJson('trust', line, prior), grantFile: grantsPath(o.env) }));
|
|
644
|
+
} else {
|
|
645
|
+
if (!required) o.out(`${pkg.id} is inert: no grant is required, the line below is only needed if its tier changes`);
|
|
646
|
+
for (const text of appendInstruction(o.env, line, `${pkg.id} ${row.version} acts when its recorded digests are trusted`, prior)) o.out(text);
|
|
647
|
+
for (const text of rebindLines) o.out(text);
|
|
648
|
+
}
|
|
649
|
+
return finishRuntime(0);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function verbExempt(o , pkg ) {
|
|
653
|
+
const refuse = (result , message ) => {
|
|
654
|
+
audit(o, 'exempt', pkg.id, result);
|
|
655
|
+
o.out(o.json ? JSON.stringify({ ok: false, error: result, message }) : message);
|
|
656
|
+
return finishRuntime(1);
|
|
657
|
+
};
|
|
658
|
+
if (pkg.firstParty) return refuse('bundled', `${pkg.id} is a bundled first-party package and needs no exemption`);
|
|
659
|
+
const host = hostVersion(o.repoRoot ?? REPO_ROOT);
|
|
660
|
+
let range ;
|
|
661
|
+
try { range = JSON.parse(fs.readFileSync(path.join(pkg.storeDir, MANIFEST_FILE), 'utf8')).doflow; } catch { range = null; }
|
|
662
|
+
if (typeof range !== 'string') return refuse('manifest-unreadable', `cannot read the host range of ${pkg.id}: its store entry has no readable manifest`);
|
|
663
|
+
if (host === null) return refuse('host-unknown', 'the running DoFlow version cannot be read, so no exemption can be written for it');
|
|
664
|
+
if (satisfiesHostRange(host, range)) return refuse('inside-range', `DoFlow ${host} is inside the range ${range} of ${pkg.id}: no exemption is needed`);
|
|
665
|
+
const row = rowOf(o, pkg.id);
|
|
666
|
+
const line = JSON.stringify({ grant: 'exempt', package: row.id, version: row.version, contentDigest: row.contentDigest, host, acceptRisk: true, at: grantTime(o.now) });
|
|
667
|
+
const prior = priorGrant(o, line);
|
|
668
|
+
audit(o, 'exempt', pkg.id, 'ok');
|
|
669
|
+
if (o.json) o.out(JSON.stringify({ ok: true, action: 'exempt', id: pkg.id, host, range, ...priorJson('exempt', line, prior), grantFile: grantsPath(o.env) }));
|
|
670
|
+
else for (const text of appendInstruction(o.env, line, `${pkg.id} ${row.version} runs on DoFlow ${host}, outside its range ${range}, only on this exemption`, prior)) o.out(text);
|
|
671
|
+
return finishRuntime(0);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function verbRevoke(o , pkg , ctx ) {
|
|
675
|
+
const row = pkg.firstParty ? null : rowOf(o, pkg.id);
|
|
676
|
+
// Whole seconds, rounded down: a line printed in the same second still sorts before the revocation (fail closed).
|
|
677
|
+
const at = new Date(Math.floor(o.now / 1000) * 1000).toISOString();
|
|
678
|
+
try {
|
|
679
|
+
recordRevocation({ homeDir: o.homeDir }, { packageId: pkg.id, contentDigest: row ? row.contentDigest : pkg.contentDigest, at });
|
|
680
|
+
} catch (error ) {
|
|
681
|
+
const message = `cannot record the revocation: ${error.message}`;
|
|
682
|
+
o.out(o.json ? JSON.stringify({ ok: false, error: 'state-unreadable', message }) : message);
|
|
683
|
+
return finishRuntime(1);
|
|
684
|
+
}
|
|
685
|
+
const sync = syncProjection(o);
|
|
686
|
+
const after = freshView(o).packages.find((p ) => p.id === pkg.id) ?? pkg;
|
|
687
|
+
ctx.tierMax = after.tierMax;
|
|
688
|
+
audit(o, 'revoke', pkg.id, after.state);
|
|
689
|
+
if (o.json) o.out(JSON.stringify({ ok: true, action: 'revoke', id: pkg.id, state: after.state, reasons: after.reasons, ...projectionJson(sync) }));
|
|
690
|
+
else {
|
|
691
|
+
o.out(stateLine(after));
|
|
692
|
+
o.out(`revoked trust for ${pkg.id} at its recorded content digest; only a trust line printed after ${at} counts${removedAny(sync) ? '' : ', and nothing is projected into a harness yet, so there is nothing to unproject'}`);
|
|
693
|
+
for (const text of sync.lines) o.out(text);
|
|
694
|
+
// The undo (FU-92eftv): the line `trust --id` prints has the revoked line's key, so it takes that line's place.
|
|
695
|
+
const file = grantsPath(o.env);
|
|
696
|
+
if (row && file !== null) {
|
|
697
|
+
const undo = priorGrant(o, trustLineFor({ ...row, capabilityDigest: after.capabilityDigest }, trustPolicy(o.repoRoot), o.now));
|
|
698
|
+
const run = `run doflow package --action trust --id ${pkg.id}`;
|
|
699
|
+
o.out(undo.status === 'replace' ? `to undo: ${run} and put its line in place of line ${undo.lineNumber} of ${file}`
|
|
700
|
+
: undo.status === 'granted' ? `line ${undo.lineNumber} of ${file} is dated after the revocation and still grants`
|
|
701
|
+
: `to undo: ${run} and add its line to ${file}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return finishRuntime(0);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ── remove (B.7) ────────────────────────────────────────────────────────────────────────────────────
|
|
708
|
+
|
|
709
|
+
/** The user, project and local settings files of the current directory, in that order (the reader `config list` uses). */
|
|
710
|
+
function settingsLayerFiles(o ) {
|
|
711
|
+
const files = [];
|
|
712
|
+
const home = configHome(o.env).dir;
|
|
713
|
+
if (home !== null) files.push({ layer: 'user', file: path.join(home, 'settings.json') });
|
|
714
|
+
const layers = require('../settings/project-layers.js').projectLayerSource(o.projectRoot ?? process.cwd()).layersFrom;
|
|
715
|
+
if (layers !== null) files.push({ layer: 'project', file: path.join(layers, 'settings.json') }, { layer: 'local', file: path.join(layers, 'settings.local.json') });
|
|
716
|
+
return files;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** The `findings.json` files under `<project>/.doflow/`, sorted. Symbolic links and dispatch worktrees are not entered. */
|
|
720
|
+
function findingsFiles(projectRoot ) {
|
|
721
|
+
const found = [];
|
|
722
|
+
const walk = (dir , depth ) => {
|
|
723
|
+
let entries ;
|
|
724
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a , b ) => (a.name < b.name ? -1 : 1)); } catch { return; }
|
|
725
|
+
for (const entry of entries) {
|
|
726
|
+
const full = path.join(dir, entry.name);
|
|
727
|
+
if (entry.isDirectory()) { if (entry.name !== 'dispatch-worktrees' && depth < 12) walk(full, depth + 1); } else if (entry.isFile() && entry.name === 'findings.json') found.push(full);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
walk(path.join(projectRoot, '.doflow'), 0);
|
|
731
|
+
return found;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The read-only removal scan (IC-012): the settings values and findings records that still name one of `ids`.
|
|
736
|
+
* Rows are `{ kind: 'setting', layer, key, value }` and `{ kind: 'record', path, field: 'protocol.id', value }`; a
|
|
737
|
+
* layer file that cannot be read is `unscanned`. Nothing is written.
|
|
738
|
+
*/
|
|
739
|
+
function scanReferences(o , ids , keys ) {
|
|
740
|
+
const dangling = [];
|
|
741
|
+
const unscanned = [];
|
|
742
|
+
const names = (value ) => {
|
|
743
|
+
if (typeof value === 'string') return ids.has(value);
|
|
744
|
+
if (Array.isArray(value)) return value.some((item ) => ids.has(item));
|
|
745
|
+
if (value && typeof value === 'object') return Object.keys(value).some((key) => ids.has(key));
|
|
746
|
+
return false;
|
|
747
|
+
};
|
|
748
|
+
if (keys.length > 0) {
|
|
749
|
+
const { loadSettingsRegistry } = require('../settings/registry.js');
|
|
750
|
+
const { readLayerFile } = require('../settings/files.js');
|
|
751
|
+
const loaded = loadSettingsRegistry({ repoRoot: o.repoRoot ?? REPO_ROOT });
|
|
752
|
+
for (const { layer, file } of settingsLayerFiles(o)) {
|
|
753
|
+
if (!loaded.ok) { unscanned.push({ layer, path: file, reason: 'the settings registry is unreadable' }); continue; }
|
|
754
|
+
const read = readLayerFile(file, { registry: loaded.registry, includeDocument: true });
|
|
755
|
+
if (read.status === 'invalid') { unscanned.push({ layer, path: file, reason: read.reason }); continue; }
|
|
756
|
+
for (const key of keys) {
|
|
757
|
+
const value = read.document?.[key];
|
|
758
|
+
if (names(value)) dangling.push({ kind: 'setting', layer, key, value });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
for (const file of findingsFiles(o.projectRoot ?? process.cwd())) {
|
|
763
|
+
try {
|
|
764
|
+
if (fs.statSync(file).size > 1048576) continue;
|
|
765
|
+
const id = JSON.parse(fs.readFileSync(file, 'utf8'))?.protocol?.id;
|
|
766
|
+
if (typeof id === 'string' && ids.has(id)) dangling.push({ kind: 'record', path: file, field: 'protocol.id', value: id });
|
|
767
|
+
} catch { /* not a findings record this scan can read */ }
|
|
768
|
+
}
|
|
769
|
+
return { dangling, unscanned };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Drop the lock row of `id`; a lock left with no package and no target is deleted (B.1's rule, as `recordLock` does). */
|
|
773
|
+
function dropLockRow(o , id ) {
|
|
774
|
+
const lock = readLock({ scope: 'global', homeDir: o.homeDir });
|
|
775
|
+
if (!lock) return;
|
|
776
|
+
const rows = lockPackages(lock).filter((r ) => r.id !== id);
|
|
777
|
+
if (rows.length === 0 && (lock.targets ?? []).length === 0) removeLock({ scope: 'global', homeDir: o.homeDir });
|
|
778
|
+
else writeLock({ scope: 'global', homeDir: o.homeDir }, { ...lock, packages: rows });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* `remove --id <id>`: disable, lifecycle removal, store entry, lock row, revocation, then the `removed` tombstone, all under
|
|
783
|
+
* the run lock; then the scan. The bundled package cannot be removed (FR-004: it can be disabled). An unreadable
|
|
784
|
+
* `state.json` refuses before anything changes, because the revocation and the tombstone could not be written.
|
|
785
|
+
*/
|
|
786
|
+
function verbRemove(o , pkg , ctx ) {
|
|
787
|
+
const refuse = (result , message ) => {
|
|
788
|
+
audit(o, 'remove', pkg.id, result);
|
|
789
|
+
o.out(o.json ? JSON.stringify({ ok: false, error: result, message }) : message);
|
|
790
|
+
return finishRuntime(1);
|
|
791
|
+
};
|
|
792
|
+
if (pkg.firstParty) return refuse('bundled', `${pkg.id} is the bundled first-party package and cannot be removed; turn it off with: doflow package --action disable --id ${pkg.id}`);
|
|
793
|
+
const row = rowOf(o, pkg.id);
|
|
794
|
+
if (readState({ homeDir: o.homeDir }).unreadable) return refuse('state-unreadable', 'cannot record the revocation: the package state file is unreadable; run: doflow package --action verify --repair');
|
|
795
|
+
|
|
796
|
+
const qualifiedIds = pkg.contributions.map((c ) => c.qualifiedId);
|
|
797
|
+
const loaded = loadPoints(o.repoRoot ? { repoRoot: o.repoRoot } : {});
|
|
798
|
+
const keys = loaded.ok ? [...new Set (loaded.registry.points.filter((p ) => pkg.contributions.some((c ) => c.point === p.id)).flatMap((p ) => p.referencedBy))] : [];
|
|
799
|
+
const scan = scanReferences(o, new Set(qualifiedIds), keys);
|
|
800
|
+
const grants = readGrants({ env: o.env, projectRoot: o.projectRoot, ...(o.tmpDir === undefined ? {} : { tmpDir: o.tmpDir }) });
|
|
801
|
+
const dangling = grants.entries.filter((g ) => g.grant !== 'fetch' && g.package === pkg.id).map((g ) => ({ line: g.line, kind: g.grant, version: g.version }));
|
|
802
|
+
|
|
803
|
+
setEnabled(o, pkg, false); // disable first: a run that stops below leaves the package off
|
|
804
|
+
// Lifecycle removal of the package's projected skills and agents and their ledger rows, before the store entry goes.
|
|
805
|
+
const sync = syncProjection(o);
|
|
806
|
+
const idDir = path.dirname(pkg.storeDir);
|
|
807
|
+
const leftover = fs.existsSync(idDir) && !removeEntry(idDir, path.basename(pkg.storeDir), fs);
|
|
808
|
+
dropLockRow(o, pkg.id);
|
|
809
|
+
// Whole seconds, rounded down, as `revoke` does: a trust line printed in the same second still sorts before the revocation.
|
|
810
|
+
recordRevocation({ homeDir: o.homeDir }, { packageId: pkg.id, contentDigest: row.contentDigest, at: new Date(Math.floor(o.now / 1000) * 1000).toISOString() });
|
|
811
|
+
recordTombstone({ homeDir: o.homeDir }, { packageId: pkg.id, qualifiedIds, at: stamp(o) });
|
|
812
|
+
audit(o, 'remove', pkg.id, 'removed');
|
|
813
|
+
|
|
814
|
+
const file = grantsPath(o.env);
|
|
815
|
+
if (o.json) {
|
|
816
|
+
o.out(JSON.stringify({ ok: true, action: 'remove', id: pkg.id, version: row.version, qualifiedIds, dangling: scan.dangling, unscanned: scan.unscanned, grants: dangling, grantFile: file, leftover, ...projectionJson(sync) }));
|
|
817
|
+
return finishRuntime(0);
|
|
818
|
+
}
|
|
819
|
+
o.out(`removed ${pkg.id} ${row.version}`);
|
|
820
|
+
for (const text of sync.lines) o.out(text);
|
|
821
|
+
if (leftover) o.out('warning: the store entry could not be removed');
|
|
822
|
+
for (const d of scan.dangling) {
|
|
823
|
+
o.out(d.kind === 'setting' ? `dangling setting ${d.layer} ${d.key} = ${JSON.stringify(d.value)}` : `dangling record ${d.path} ${d.field} = ${JSON.stringify(d.value)}`);
|
|
824
|
+
}
|
|
825
|
+
for (const u of scan.unscanned) o.out(`not scanned: ${u.layer} settings ${u.path} (${u.reason})`);
|
|
826
|
+
if (scan.dangling.length === 0 && scan.unscanned.length === 0) o.out('no dangling references');
|
|
827
|
+
else if (scan.dangling.length > 0) o.out('nothing was rewritten: edit those settings and records yourself');
|
|
828
|
+
for (const g of dangling) o.out(`grant line ${g.line} (${g.kind} ${g.version}) in ${file} still names ${pkg.id}; DoFlow never edits that file, and you may delete the line`);
|
|
829
|
+
return finishRuntime(0);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** `verify [--repair]`: re-hash every enabled third-party entry through the view; record or clear suspensions. */
|
|
833
|
+
function verbVerify(o , ctx ) {
|
|
834
|
+
let view = freshView(o);
|
|
835
|
+
let repaired = false;
|
|
836
|
+
let notes = [];
|
|
837
|
+
let read = readState({ homeDir: o.homeDir });
|
|
838
|
+
if (o.repair) {
|
|
839
|
+
if (read.unreadable) {
|
|
840
|
+
const grants = readGrants({ env: o.env, projectRoot: o.projectRoot, ...(o.tmpDir === undefined ? {} : { tmpDir: o.tmpDir }) });
|
|
841
|
+
repairState({ homeDir: o.homeDir }, { grantEntries: grants.entries, at: stamp(o) });
|
|
842
|
+
repaired = true;
|
|
843
|
+
view = freshView(o);
|
|
844
|
+
read = readState({ homeDir: o.homeDir });
|
|
845
|
+
} else notes = ['state.json is readable, so --repair left it as it is'];
|
|
846
|
+
}
|
|
847
|
+
const checked = view.packages.filter((p ) => !p.firstParty && p.enabled);
|
|
848
|
+
ctx.tierMax = highestTier(checked.map((p ) => p.tierMax));
|
|
849
|
+
const findings = [];
|
|
850
|
+
for (const p of checked) {
|
|
851
|
+
if (p.state === 'suspended' || p.state === 'incompatible') findings.push({ package: p.id, state: p.state, reasons: p.reasons });
|
|
852
|
+
}
|
|
853
|
+
for (const f of view.findings) if (f.severity === 'error' && f.package === null) findings.push({ package: null, state: f.code, reasons: [f.field] });
|
|
854
|
+
if (read.unreadable && !findings.some((f) => f.state === 'state-unreadable')) findings.push({ package: null, state: 'state-unreadable', reasons: ['state.json'] });
|
|
855
|
+
if (!read.unreadable) {
|
|
856
|
+
for (const p of checked) {
|
|
857
|
+
const had = read.state.suspended.find((s ) => s.package === p.id);
|
|
858
|
+
if (p.state === 'suspended') {
|
|
859
|
+
const reason = p.reasons[0];
|
|
860
|
+
recordSuspension({ homeDir: o.homeDir }, { packageId: p.id, reason, projected: packageHasProjection(o.homeDir, p.id), since: had?.reason === reason ? had.since : stamp(o) });
|
|
861
|
+
} else if (had) clearSuspension({ homeDir: o.homeDir }, { packageId: p.id });
|
|
862
|
+
}
|
|
863
|
+
if (!repaired) audit(o, 'verify', '*', findings.length === 0 ? 'ok' : `findings:${findings.length}`);
|
|
864
|
+
}
|
|
865
|
+
// Projects what is active and unprojects what is not, a suspended package's among them (IC-017).
|
|
866
|
+
const sync = syncProjection(o);
|
|
867
|
+
require('../../lifecycle/index.js').settleSuspendedProjection({ homeDir: o.homeDir });
|
|
868
|
+
if (o.json) o.out(JSON.stringify({ ok: findings.length === 0, action: 'verify', repaired, packages: checked.map((p ) => ({ id: p.id, state: p.state, reasons: p.reasons })), findings, ...projectionJson(sync) }));
|
|
869
|
+
else {
|
|
870
|
+
for (const text of notes) o.out(text);
|
|
871
|
+
if (repaired) o.out('state.json was unreadable and is rewritten: every trust and exempt line present now is revoked; to undo one, run doflow package --action trust --id <id> (or exempt) and put its line in place of the line it names');
|
|
872
|
+
for (const p of checked) o.out(p.state === 'suspended' || p.state === 'incompatible' ? `package ${p.id} ${p.state} reasons=${p.reasons.join(',')}` : `package ${p.id} intact`);
|
|
873
|
+
for (const f of findings.filter((x) => x.package === null)) {
|
|
874
|
+
o.out(`${f.state}: ${f.reasons.join(',')}${f.state === 'state-unreadable' ? '; run package --action verify --repair' : ''}`);
|
|
875
|
+
}
|
|
876
|
+
for (const text of sync.lines) o.out(text);
|
|
877
|
+
o.out(findings.length === 0 ? `verify: ${checked.length} enabled third-party package${checked.length === 1 ? '' : 's'} intact` : `verify: ${findings.length} finding${findings.length === 1 ? '' : 's'}`);
|
|
878
|
+
}
|
|
879
|
+
return finishRuntime(findings.length === 0 ? 0 : 1);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Run one verb under the global scope's `run.lock`, then emit `package.verb`. */
|
|
883
|
+
function handleVerb(o ) {
|
|
884
|
+
const { action, id, json, out } = o;
|
|
885
|
+
const ctx = { tierMax: null };
|
|
886
|
+
let code ;
|
|
887
|
+
if (action !== 'verify' && (typeof id !== 'string' || id.length === 0)) code = usage(json, out, `${action} requires one --id <id>`);
|
|
888
|
+
else if (action !== 'verify' && !freshView(o).packages.some((p ) => p.id === id)) code = unknownId(o); // before the run lock: an unknown id leaves not even the lock's directory behind
|
|
889
|
+
else {
|
|
890
|
+
try {
|
|
891
|
+
const hold = acquireRunLock({ scopeRoot: o.homeDir, scope: 'global', command: `package ${action}` });
|
|
892
|
+
try {
|
|
893
|
+
if (action === 'verify') code = verbVerify(o, ctx);
|
|
894
|
+
else {
|
|
895
|
+
const pkg = freshView(o).packages.find((p ) => p.id === id);
|
|
896
|
+
ctx.tierMax = pkg ? pkg.tierMax : null;
|
|
897
|
+
if (!pkg) code = unknownId(o);
|
|
898
|
+
else if (action === 'enable') code = verbEnable(o, pkg, ctx);
|
|
899
|
+
else if (action === 'disable') code = verbDisable(o, pkg, ctx);
|
|
900
|
+
else if (action === 'trust') code = verbTrust(o, pkg);
|
|
901
|
+
else if (action === 'exempt') code = verbExempt(o, pkg);
|
|
902
|
+
else if (action === 'remove') code = verbRemove(o, pkg, ctx);
|
|
903
|
+
else code = verbRevoke(o, pkg, ctx);
|
|
904
|
+
}
|
|
905
|
+
} finally {
|
|
906
|
+
hold.release();
|
|
907
|
+
}
|
|
908
|
+
} catch (error ) {
|
|
909
|
+
if (error?.name === 'RunLockTimeoutError' || error?.name === 'RunLockError') {
|
|
910
|
+
out(json ? JSON.stringify({ ok: false, error: 'run-lock', message: error.message }) : error.message);
|
|
911
|
+
code = finishRuntime(1);
|
|
912
|
+
} else {
|
|
913
|
+
captureCaught(error, 'package', 1);
|
|
914
|
+
console.error(`[ERROR] package: ${error?.message || String(error)}`);
|
|
915
|
+
code = finishRuntime(1);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
emitVerb(o, action, code, ctx.tierMax);
|
|
920
|
+
return code;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** One `package.verb` event (IC-024): closed enums only, never a package id or a path. Telemetry never changes the outcome. */
|
|
924
|
+
function emitVerb(o , action , code , tierMax ) {
|
|
925
|
+
try {
|
|
926
|
+
const writer = o.writer || new TelemetryWriter({ env: o.env, start: process.cwd() });
|
|
927
|
+
writer.write([{ kind: 'package.verb', action, result: code === 0 ? 'ok' : code === 2 ? 'usage' : 'refused', tier_max: tierMax }]);
|
|
928
|
+
} catch { /* a telemetry fault changes nothing */ }
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function handlePackageCommand({ action, path: pkgPath, id, source, index, packageVersion, plan, repair, points, dir, cwd = process.cwd(), fsImpl, json = false, repoRoot, homeDir = os.homedir(), env = process.env, projectRoot, tmpDir, settingsView, writer, now = Date.now(), hooks, registry, out = (line ) => console.log(line) } = {}) {
|
|
932
|
+
const o = { action, id, index, packageVersion, repair: repair === true, json, repoRoot, homeDir, env, projectRoot, tmpDir, settingsView, writer, now, hooks, registry, out };
|
|
933
|
+
if (action === 'new') {
|
|
934
|
+
const result = scaffoldPackage({ id, points, dir, cwd, homeDir, env, repoRoot, now, fsImpl, grantsPath: grantsPath(env) });
|
|
935
|
+
const code = result.ok ? 0 : result.usage ? 2 : 1;
|
|
936
|
+
emitVerb(o, 'new', code, result.ok ? result.tierMax : null);
|
|
937
|
+
if (json) {
|
|
938
|
+
const { usage: _usage, ...body } = result;
|
|
939
|
+
out(JSON.stringify(body));
|
|
940
|
+
} else if (result.ok) {
|
|
941
|
+
out(`created ${result.dir}`);
|
|
942
|
+
for (const file of result.files) out(path.join(result.dir, ...file.split('/')));
|
|
943
|
+
for (const f of result.findings) out(findingLine(f));
|
|
944
|
+
if (result.ignoredByGit === true) out("the project's own ignore rules hide this folder; a person may add !.doflow/package/ to the project's .gitignore");
|
|
945
|
+
for (const step of result.next) out(`next: ${step.step}: ${step.command ? `${step.command}; ` : ''}${step.note}`);
|
|
946
|
+
} else {
|
|
947
|
+
for (const f of result.findings) out(findingLine(f));
|
|
948
|
+
}
|
|
949
|
+
return finishRuntime(code);
|
|
950
|
+
}
|
|
951
|
+
if (VERB_ACTIONS.includes(action)) return handleVerb(o);
|
|
952
|
+
if (action === 'add' || action === 'update') {
|
|
953
|
+
const given = action === 'add' ? source : id;
|
|
954
|
+
if (typeof plan === 'string' && plan.length > 0) {
|
|
955
|
+
if (typeof given === 'string' || index !== undefined || packageVersion !== undefined || (action === 'add' && id !== undefined)) {
|
|
956
|
+
return usage(json, out, `${action} takes --plan alone, without source, index, id or version selection`);
|
|
957
|
+
}
|
|
958
|
+
return handleApply({ ...o, plan });
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
return handlePlan({ ...o, source });
|
|
962
|
+
} catch (error ) {
|
|
963
|
+
captureCaught(error, 'package', 1);
|
|
964
|
+
console.error(`[ERROR] package: ${error?.message || String(error)}`);
|
|
965
|
+
return finishRuntime(1);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (action === 'list' || action === 'show') {
|
|
969
|
+
try {
|
|
970
|
+
return handleView(o);
|
|
971
|
+
} catch (error ) {
|
|
972
|
+
captureCaught(error, 'package', 1);
|
|
973
|
+
console.error(`[ERROR] package: ${error?.message || String(error)}`);
|
|
974
|
+
return finishRuntime(1);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (action === 'check') {
|
|
978
|
+
try {
|
|
979
|
+
return handleCheck({ pkgPath, json, repoRoot, out });
|
|
980
|
+
} catch (error ) {
|
|
981
|
+
captureCaught(error, 'package', 1);
|
|
982
|
+
console.error(`[ERROR] package: ${error?.message || String(error)}`);
|
|
983
|
+
return finishRuntime(1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (action !== 'points') {
|
|
987
|
+
const message = 'action must be points, check, list, show, add, update, new, enable, disable, trust, exempt, revoke, verify or remove';
|
|
988
|
+
out(json ? JSON.stringify({ ok: false, error: 'usage', message }) : `doflow package: ${message}`);
|
|
989
|
+
return finishRuntime(2);
|
|
990
|
+
}
|
|
991
|
+
try {
|
|
992
|
+
const loaded = loadPoints(repoRoot ? { repoRoot } : {});
|
|
993
|
+
if (!loaded.ok) {
|
|
994
|
+
const message = loaded.errors.join('; ');
|
|
995
|
+
out(json ? JSON.stringify({ ok: false, error: 'points-invalid', message }) : `package: points-invalid: ${message}`);
|
|
996
|
+
return finishRuntime(1);
|
|
997
|
+
}
|
|
998
|
+
const rows = pointRows(loaded.registry);
|
|
999
|
+
if (json) out(JSON.stringify({ points: rows }));
|
|
1000
|
+
else for (const row of rows) out(pointLine(row));
|
|
1001
|
+
return finishRuntime(0);
|
|
1002
|
+
} catch (error ) {
|
|
1003
|
+
captureCaught(error, 'package', 1);
|
|
1004
|
+
console.error(`[ERROR] package: ${error?.message || String(error)}`);
|
|
1005
|
+
return finishRuntime(1);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
module.exports = { handlePackageCommand };
|