@khoavu882/doflow 2.0.0-beta.5 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/bin/doflow.js +22 -12
- package/core/.antigravity-plugin/plugin.json +1 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +26 -9
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +51 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -11
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- package/core/registry/contribution-points.json +35 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1445 -366
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +599 -996
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +272 -0
- package/core/registry/storage.json +687 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -6
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
- package/core/shared/scripts/doflow/bin/doflow-run +152 -34
- package/core/shared/skills/do/SKILL.md +14 -12
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +48 -148
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +23 -20
- package/core/shared/skills/do-diagnose/SKILL.md +18 -6
- package/core/shared/skills/do-document/SKILL.md +12 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +19 -7
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +10 -11
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +13 -14
- package/core/shared/skills/do-plan/SKILL.md +34 -20
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +143 -66
- package/src/adapters/claude/index.js +46 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +61 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +291 -46
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +22 -3
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +337 -32
- package/src/cli/runtime-commands.js +220 -60
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +4 -16
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +472 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +20 -11
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/pointer.js +67 -12
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +158 -79
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-ledger.js +52 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +33 -3
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +624 -18
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +281 -26
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +88 -10
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +38 -7
- package/src/runtime/lifecycle/overview.js +26 -2
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +2 -2
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +487 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +273 -0
- package/src/runtime/roadmap/loop.js +1719 -0
- package/src/runtime/roadmap/merge.js +390 -0
- package/src/runtime/roadmap/rows.js +1122 -0
- package/src/runtime/roadmap/seed.js +1063 -0
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +60 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +1 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +85 -14
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +373 -64
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verb-view.js +267 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -54
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +256 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +610 -67
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -251
- package/src/adapters/gemini/index.js +0 -429
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
// This module is the one seam both stores persist through:
|
|
14
14
|
//
|
|
15
15
|
// - a per-file lock (`<file>.lock` directory — mkdirSync is atomic on every platform) makes the
|
|
16
|
-
// read-merge-write below a critical section across processes on this machine;
|
|
16
|
+
// read-merge-write below a critical section across processes on this machine; the holder
|
|
17
|
+
// marks the directory with its own `owner-*` entry, so release and the pre-write check act on
|
|
18
|
+
// that holder's lock only, and a stale lock is reclaimed in place by one process (see the
|
|
19
|
+
// protocol above `attemptLock`);
|
|
17
20
|
// - inside the lock, the writer re-reads the file and MERGES what is on disk into its own view
|
|
18
21
|
// before writing, so an overlapping writer's items survive;
|
|
19
22
|
// - the payload carries a monotonically increasing `revision`, so an overlapped-and-merged
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
// the version named, instead of guessing at its shape.
|
|
25
28
|
|
|
26
29
|
const path = require('node:path');
|
|
30
|
+
const { randomBytes } = require('node:crypto');
|
|
27
31
|
const { isDeepStrictEqual } = require('node:util');
|
|
28
32
|
|
|
29
33
|
/** Three-way merge against the reader's snapshot. Unchanged local records never overwrite a
|
|
@@ -52,74 +56,339 @@ function sleep(ms ) {
|
|
|
52
56
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
// ── The lock protocol (117 Phase C; FU-8acds5, FU-vgkrvd) ───────────────────────────────────────────
|
|
60
|
+
//
|
|
61
|
+
// The lock is the `<file>.lock` directory, created with mkdirSync. A holder marks it with its own
|
|
62
|
+
// `owner-<pid>-<random>` entry. Before this protocol any process that found the lock older than
|
|
63
|
+
// LOCK_STALE_MS removed it by name and every release removed it by name, so two waiters that judged
|
|
64
|
+
// one stale lock could both take it and each one's release could delete the other's. Three facts
|
|
65
|
+
// carry the protocol (the safety argument, one interleaving at a time, is in the 117 feature records:
|
|
66
|
+
// research/phase-c-protocol.md):
|
|
67
|
+
//
|
|
68
|
+
// - Pinned: no DoFlow runtime renames a lock directory and rmdir removes only an empty one, so a
|
|
69
|
+
// directory with an entry in it stays at `<file>.lock` until it is empty. A process whose entry
|
|
70
|
+
// is in the lock knows the directory at that path is the one holding its entry.
|
|
71
|
+
// - Released: this runtime removes a lock directory only in its release, after removing its own
|
|
72
|
+
// entry from the lock it held; it reclaims a stale lock in place. Older runtimes remove one by
|
|
73
|
+
// name in their release, or after judging a lock stale.
|
|
74
|
+
// - Alone: a process holds the lock when its entry is the only entry in it. Two processes whose
|
|
75
|
+
// entries are in one directory cannot both have been alone in it.
|
|
76
|
+
//
|
|
77
|
+
// - Acquire: note the time, mkdir `<file>.lock`, mkdir its entry inside, read the time again. Under
|
|
78
|
+
// PUBLISH_BOUND_MS, no process can have judged the new directory stale before the entry landed,
|
|
79
|
+
// so the entry is in this process's own directory. Over it, the entry may have landed in a
|
|
80
|
+
// directory another process made after reclaiming this one (an older runtime's empty lock looks
|
|
81
|
+
// exactly like a new one), so the process removes its entry and starts over. Then it lists the
|
|
82
|
+
// lock: alone, it holds.
|
|
83
|
+
// - Reclaim (the lock is older than LOCK_STALE_MS): stat, list, stat again (the same directory,
|
|
84
|
+
// unchanged, so every entry listed has been there that long); mkdir its own entry; stat again:
|
|
85
|
+
// not the same directory means the judged one was removed and the entry landed in a newer lock,
|
|
86
|
+
// so it removes its entry and starts over. Then it removes the judged entries by name (names are
|
|
87
|
+
// unique, so a later holder's entry is never among them) and lists the lock: alone, it holds. The
|
|
88
|
+
// directory never empties on the way, so an older runtime's rmdir of a stale lock fails instead of
|
|
89
|
+
// removing it.
|
|
90
|
+
// - Contention: two entries mean two processes reached the lock. The one whose entry name sorts
|
|
91
|
+
// first waits for the other to leave (it holds next); the other removes its entry and waits. A
|
|
92
|
+
// process that removes its entry and finds the lock emptied enters it again (once, under a fresh
|
|
93
|
+
// name, after checking it is the same directory) instead of leaving an empty lock everyone would
|
|
94
|
+
// wait 10 s on.
|
|
95
|
+
// - Release notes the lock's identity while its entry is in it, removes the entry, then removes the
|
|
96
|
+
// directory only if it is still that one (an older runtime may have made a new, empty lock there
|
|
97
|
+
// if the release stalled). A holder whose entry is gone (its lock was reclaimed) removes nothing;
|
|
98
|
+
// a waiting process's entry keeps the directory, and that process holds next.
|
|
99
|
+
// - `updateTaskState` checks the holder's entry is still in the lock (`release.held()`) after
|
|
100
|
+
// writing its tmp file and before the rename: a holder that was paused past LOCK_STALE_MS and
|
|
101
|
+
// reclaimed writes nothing.
|
|
102
|
+
//
|
|
103
|
+
// Runtimes from before this protocol see a directory they did not create: they wait on a fresh one,
|
|
104
|
+
// and on a stale one their rmdir fails (it is not empty), so they fail closed ("Could not lock",
|
|
105
|
+
// `store-locked`) instead of taking it; their release of a lock reclaimed from them fails the same
|
|
106
|
+
// way. Whatever a process leaves when it dies is reclaimed once LOCK_STALE_MS old.
|
|
107
|
+
const OWNER_PREFIX = 'owner-';
|
|
108
|
+
// The longest a fresh acquire may take from its mkdir to its entry and still trust the directory is
|
|
109
|
+
// its own: half the stale age, so a process that judged it stale cannot have done so before the
|
|
110
|
+
// entry landed, with room for coarse file timestamps.
|
|
111
|
+
const PUBLISH_BOUND_MS = LOCK_STALE_MS / 2;
|
|
112
|
+
// How long tryAcquireLock waits for a process that reached the lock at the same moment to leave.
|
|
113
|
+
const CONTEND_MS = 40;
|
|
114
|
+
const CONTEND_WAIT_MS = 2;
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
55
118
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
119
|
+
* Optional lock settings (FU-31dyvs, for a store whose holders are identified by their entry): the
|
|
120
|
+
* defaults are the protocol above, unchanged.
|
|
121
|
+
* - `stale` replaces the age rule of a reclaim. It is asked after the listing re-check, with the
|
|
122
|
+
* entries listed and the lock's age, and the reclaim goes ahead only when it says true. It must
|
|
123
|
+
* treat an empty lock by age (an acquire in progress makes an empty lock first; see
|
|
124
|
+
* PUBLISH_BOUND_MS), and it may judge an entry by its owner: an entry whose owner is dead can be
|
|
125
|
+
* reclaimed at any age, because a dead process never acts on it again.
|
|
126
|
+
* - `entryName` makes this process's entry name (default `owner-<pid>-<random>`); every name it
|
|
127
|
+
* makes must be unique.
|
|
128
|
+
* - `removeEntry` removes one judged entry by name (default rmdir); it throws as rmdir would.
|
|
129
|
+
* - `retries` is acquireLock's attempt budget (default LOCK_RETRIES, each LOCK_WAIT_MS apart).
|
|
61
130
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/** Same directory: same device and inode, and the same birth time where the platform reports one. */
|
|
139
|
+
function sameDir(a , b ) {
|
|
140
|
+
return a.dev === b.dev && a.ino === b.ino && (!a.birthtimeMs || !b.birthtimeMs || a.birthtimeMs === b.birthtimeMs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** This attempt's entry name; `leave` gives it a fresh one when it re-enters an emptied lock. */
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/** Removes this process's entry; gone already is fine (a reclaimer removed it). */
|
|
147
|
+
function withdraw(fsImpl , lockDir , me ) {
|
|
148
|
+
try { fsImpl.rmdirSync(path.join(lockDir, me.name)); } catch { /* not there: nothing of ours is left */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* With this process's entry in the lock: `held` when it is the only entry; `queued` when another
|
|
153
|
+
* entry is there and ours sorts first (we hold once it leaves); otherwise we leave (`busy`, or `held`
|
|
154
|
+
* when leaving found everyone else gone). `again` when our entry is gone (the lock was reclaimed from
|
|
155
|
+
* us). With `rejoin` false (an entry that came back to an emptied lock) it never queues or comes back.
|
|
156
|
+
*/
|
|
157
|
+
function elect(fsImpl , lockDir , me , rejoin = true, options = {}) {
|
|
158
|
+
let entries ;
|
|
159
|
+
let pinned ;
|
|
65
160
|
try {
|
|
66
|
-
fsImpl.
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
if (
|
|
161
|
+
entries = fsImpl.readdirSync(lockDir);
|
|
162
|
+
if (!entries.includes(me.name)) return 'again';
|
|
163
|
+
if (entries.length === 1) return 'held';
|
|
164
|
+
if (rejoin && entries.every((name ) => name === me.name || me.name < name)) return 'queued';
|
|
165
|
+
pinned = pinnedDir(fsImpl, lockDir, me);
|
|
166
|
+
} catch { pinned = null; }
|
|
167
|
+
if (!pinned) { withdraw(fsImpl, lockDir, me); return 'again'; }
|
|
168
|
+
if (!rejoin) { withdraw(fsImpl, lockDir, me); return 'busy'; }
|
|
169
|
+
return leave(fsImpl, lockDir, me, pinned, options);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The stat of the lock while our entry is in it (so it is the directory holding our entry), or null. */
|
|
173
|
+
function pinnedDir(fsImpl , lockDir , me ) {
|
|
174
|
+
try {
|
|
175
|
+
const stat = fsImpl.statSync(lockDir);
|
|
176
|
+
return fsImpl.existsSync(path.join(lockDir, me.name)) ? stat : null;
|
|
177
|
+
} catch { return null; }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Removes our entry from the lock `pinned` (its stat taken while our entry was in it). If that left
|
|
182
|
+
* it empty, everyone else left after we looked, and an empty lock would block every process until it
|
|
183
|
+
* is stale: we enter it again, once, under a fresh name (a reclaimer that listed the old name removes
|
|
184
|
+
* names it listed), and hold it if we are alone. Never removes the directory.
|
|
185
|
+
*/
|
|
186
|
+
function leave(fsImpl , lockDir , me , pinned , options = {}) {
|
|
187
|
+
withdraw(fsImpl, lockDir, me);
|
|
188
|
+
try {
|
|
189
|
+
if (fsImpl.readdirSync(lockDir).length > 0) return 'busy';
|
|
190
|
+
me.name = (options.entryName || newToken)();
|
|
191
|
+
fsImpl.mkdirSync(path.join(lockDir, me.name));
|
|
192
|
+
} catch (error ) { return error.code === 'ENOENT' ? 'again' : 'busy'; }
|
|
193
|
+
let same = false;
|
|
194
|
+
try { same = sameDir(pinned, fsImpl.statSync(lockDir)); } catch { /* treat it as changed */ }
|
|
195
|
+
if (!same) { withdraw(fsImpl, lockDir, me); return 'again'; }
|
|
196
|
+
return elect(fsImpl, lockDir, me, false, options);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A queued attempt that stops waiting: leave the lock (or hold it, when the other left meanwhile). */
|
|
200
|
+
function giveUp(fsImpl , lockDir , me , options = {}) {
|
|
201
|
+
const step = elect(fsImpl, lockDir, me, true, options);
|
|
202
|
+
if (step !== 'queued') return step;
|
|
203
|
+
const pinned = pinnedDir(fsImpl, lockDir, me);
|
|
204
|
+
if (!pinned) { withdraw(fsImpl, lockDir, me); return 'again'; }
|
|
205
|
+
return leave(fsImpl, lockDir, me, pinned, options);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Reclaims the lock at `lockDir` in place when it is stale. `again` means look again now. */
|
|
209
|
+
function reclaimIfStale(fsImpl , lockDir , me , options = {}) {
|
|
210
|
+
const missing = (error ) => (error.code === 'ENOENT' ? 'again' : 'busy');
|
|
211
|
+
let judged ;
|
|
212
|
+
let entries ;
|
|
213
|
+
try {
|
|
214
|
+
judged = fsImpl.statSync(lockDir);
|
|
215
|
+
if (!options.stale && Date.now() - judged.mtimeMs <= LOCK_STALE_MS) return 'busy';
|
|
216
|
+
entries = fsImpl.readdirSync(lockDir);
|
|
217
|
+
const listed = fsImpl.statSync(lockDir);
|
|
218
|
+
if (!sameDir(judged, listed) || listed.mtimeMs !== judged.mtimeMs) return 'again';
|
|
219
|
+
} catch (error ) { return missing(error); }
|
|
220
|
+
if (options.stale && !options.stale({ lockDir, entries, ageMs: Date.now() - judged.mtimeMs })) return 'busy';
|
|
221
|
+
try { fsImpl.mkdirSync(path.join(lockDir, me.name)); } catch (error ) { return missing(error); }
|
|
222
|
+
// Our entry pins the directory it landed in; it must be the one judged stale.
|
|
223
|
+
let same = false;
|
|
224
|
+
try { same = sameDir(judged, fsImpl.statSync(lockDir)); } catch { /* treat it as changed */ }
|
|
225
|
+
if (!same) {
|
|
226
|
+
withdraw(fsImpl, lockDir, me);
|
|
227
|
+
return 'again';
|
|
70
228
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
229
|
+
for (const name of entries) {
|
|
230
|
+
try {
|
|
231
|
+
if (options.removeEntry) options.removeEntry(fsImpl, lockDir, name);
|
|
232
|
+
else fsImpl.rmdirSync(path.join(lockDir, name));
|
|
233
|
+
} catch (error ) {
|
|
234
|
+
if (error.code === 'ENOENT') continue; // another reclaimer, or its released holder, removed it
|
|
235
|
+
withdraw(fsImpl, lockDir, me); // not an entry this protocol makes: leave the lock alone
|
|
236
|
+
return 'busy';
|
|
77
237
|
}
|
|
78
238
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
239
|
+
return elect(fsImpl, lockDir, me, true, options);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A queued attempt's next look, for a lock with a `stale` rule (FU-31dyvs C-5): the other entries are
|
|
244
|
+
* judged as a reclaim judges them (listing re-check, `stale`, the directory still the one holding our
|
|
245
|
+
* entry), and when they are stale they are removed by name, so a queued process does not wait out its
|
|
246
|
+
* budget behind a holder that died. Without a `stale` rule this is `elect`, unchanged.
|
|
247
|
+
*/
|
|
248
|
+
function queuedStep(fsImpl , lockDir , me , options ) {
|
|
249
|
+
if (!options.stale) return elect(fsImpl, lockDir, me, true, options);
|
|
250
|
+
let judged ;
|
|
251
|
+
let others ;
|
|
252
|
+
try {
|
|
253
|
+
judged = fsImpl.statSync(lockDir);
|
|
254
|
+
const entries = fsImpl.readdirSync(lockDir);
|
|
255
|
+
const listed = fsImpl.statSync(lockDir);
|
|
256
|
+
if (!sameDir(judged, listed) || listed.mtimeMs !== judged.mtimeMs || !entries.includes(me.name)) return elect(fsImpl, lockDir, me, true, options);
|
|
257
|
+
others = entries.filter((name ) => name !== me.name);
|
|
258
|
+
} catch { return elect(fsImpl, lockDir, me, true, options); }
|
|
259
|
+
if (others.length === 0 || !options.stale({ lockDir, entries: others, ageMs: Date.now() - judged.mtimeMs })) return elect(fsImpl, lockDir, me, true, options);
|
|
260
|
+
const pinned = pinnedDir(fsImpl, lockDir, me);
|
|
261
|
+
if (!pinned || !sameDir(pinned, judged)) return elect(fsImpl, lockDir, me, true, options);
|
|
262
|
+
for (const name of others) {
|
|
263
|
+
try {
|
|
264
|
+
if (options.removeEntry) options.removeEntry(fsImpl, lockDir, name);
|
|
265
|
+
else fsImpl.rmdirSync(path.join(lockDir, name));
|
|
266
|
+
} catch (error ) {
|
|
267
|
+
if (error.code !== 'ENOENT') break; // not removable now: elect decides as before
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return elect(fsImpl, lockDir, me, true, options);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** One step towards holding the lock under `me`: held, queued (our entry waits in it), busy, or again. */
|
|
274
|
+
function attemptLock(fsImpl , lockDir , me , options = {}) {
|
|
275
|
+
const started = Date.now();
|
|
276
|
+
try {
|
|
277
|
+
fsImpl.mkdirSync(lockDir);
|
|
278
|
+
} catch (error ) {
|
|
279
|
+
// Windows reports EPERM/EACCES for a lock folder another process is deleting at this moment.
|
|
280
|
+
if (error.code !== 'EEXIST' && !(process.platform === 'win32' && (error.code === 'EPERM' || error.code === 'EACCES'))) throw error;
|
|
281
|
+
return reclaimIfStale(fsImpl, lockDir, me, options);
|
|
82
282
|
}
|
|
83
|
-
try {
|
|
84
|
-
|
|
283
|
+
try {
|
|
284
|
+
fsImpl.mkdirSync(path.join(lockDir, me.name));
|
|
285
|
+
} catch (error ) {
|
|
286
|
+
if (error.code === 'ENOENT') return 'again'; // removed before it was marked: an older runtime's release or stale break
|
|
85
287
|
throw error;
|
|
86
288
|
}
|
|
289
|
+
if (Date.now() - started >= PUBLISH_BOUND_MS) {
|
|
290
|
+
// Paused long enough for the new directory to be judged stale and replaced: the entry may sit in
|
|
291
|
+
// another process's lock. Leave it; the directory (ours or not) is left as found.
|
|
292
|
+
withdraw(fsImpl, lockDir, me);
|
|
293
|
+
return 'again';
|
|
294
|
+
}
|
|
295
|
+
return elect(fsImpl, lockDir, me, true, options);
|
|
87
296
|
}
|
|
88
297
|
|
|
298
|
+
/** The release function of a held lock; `release.held()` re-checks ownership before a write. */
|
|
299
|
+
function ownedRelease(fsImpl , lockDir , me ) {
|
|
300
|
+
const entry = path.join(lockDir, me.name);
|
|
301
|
+
let released = false;
|
|
302
|
+
const release = () => {
|
|
303
|
+
if (released) return;
|
|
304
|
+
released = true;
|
|
305
|
+
const mine = pinnedDir(fsImpl, lockDir, me); // the directory we hold, while our entry pins it
|
|
306
|
+
try { fsImpl.rmdirSync(entry); } catch { return; } // reclaimed: the lock is not ours to remove
|
|
307
|
+
// Removed only if it is still that directory: a release stalled here past LOCK_STALE_MS may find
|
|
308
|
+
// an older runtime's new (empty) lock at the path. What stays is the gap between this stat and the
|
|
309
|
+
// rmdir.
|
|
310
|
+
try {
|
|
311
|
+
if (mine && sameDir(mine, fsImpl.statSync(lockDir))) fsImpl.rmdirSync(lockDir);
|
|
312
|
+
} catch { /* a waiting process's entry is in it; that process holds next */ }
|
|
313
|
+
};
|
|
314
|
+
release.held = () => !released && fsImpl.existsSync(entry);
|
|
315
|
+
return release;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const newToken = () => `${OWNER_PREFIX}${process.pid}-${randomBytes(6).toString('hex')}`;
|
|
319
|
+
|
|
89
320
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
321
|
+
* Makes one attempt to take the cross-process lock for a state file, without waiting on a holder:
|
|
322
|
+
* a lock older than LOCK_STALE_MS is reclaimed, a live holder is never waited on. It waits only when
|
|
323
|
+
* another process reached the lock at the same moment, for at most CONTEND_MS (40 ms) for that
|
|
324
|
+
* process to leave. Returns a release function or null.
|
|
92
325
|
* @param {Object} fsImpl
|
|
93
326
|
* @param {string} file
|
|
327
|
+
* @param {LockOptions} [options] see LockOptions; the defaults are the protocol above
|
|
328
|
+
* @returns {(() => void)|null}
|
|
329
|
+
*/
|
|
330
|
+
function tryAcquireLock(fsImpl , file , options = {}) {
|
|
331
|
+
const lockDir = `${file}.lock`;
|
|
332
|
+
const deadline = Date.now() + CONTEND_MS;
|
|
333
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
334
|
+
const me = { name: (options.entryName || newToken)() };
|
|
335
|
+
let step = attemptLock(fsImpl, lockDir, me, options);
|
|
336
|
+
while (step === 'queued' && Date.now() < deadline) {
|
|
337
|
+
sleep(CONTEND_WAIT_MS);
|
|
338
|
+
step = queuedStep(fsImpl, lockDir, me, options);
|
|
339
|
+
}
|
|
340
|
+
if (step === 'queued') step = giveUp(fsImpl, lockDir, me, options);
|
|
341
|
+
if (step === 'held') return ownedRelease(fsImpl, lockDir, me);
|
|
342
|
+
if (step === 'busy') return null;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Takes the cross-process lock for one state file, waiting up to LOCK_RETRIES * LOCK_WAIT_MS (5 s).
|
|
349
|
+
* A lock older than LOCK_STALE_MS is reclaimed: its holder died or stalled, and the rename-based write
|
|
350
|
+
* keeps the file whole.
|
|
351
|
+
* @param {Object} fsImpl
|
|
352
|
+
* @param {string} file
|
|
353
|
+
* @param {LockOptions} [options] see LockOptions; the defaults are the protocol above
|
|
94
354
|
* @returns {() => void}
|
|
95
355
|
*/
|
|
96
|
-
function acquireLock(fsImpl , file ) {
|
|
356
|
+
function acquireLock(fsImpl , file , options = {}) {
|
|
97
357
|
const lockDir = `${file}.lock`;
|
|
358
|
+
const retries = options.retries ?? LOCK_RETRIES;
|
|
98
359
|
// The stale rule below is wall-clock, so the wait must be too: on a host where each attempt costs
|
|
99
|
-
// real time, counting attempts alone would outlive a live holder's staleness window
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (Date.now() - fsImpl.statSync(lockDir).mtimeMs > LOCK_STALE_MS) {
|
|
109
|
-
fsImpl.rmdirSync(lockDir);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
} catch { /* the holder released between our mkdir and stat — retry immediately */ }
|
|
113
|
-
sleep(LOCK_WAIT_MS);
|
|
360
|
+
// real time, counting attempts alone would outlive a live holder's staleness window.
|
|
361
|
+
const deadline = Date.now() + retries * LOCK_WAIT_MS;
|
|
362
|
+
let step = 'again';
|
|
363
|
+
let me = { name: '' };
|
|
364
|
+
for (let attempt = 0; attempt < retries && (attempt === 0 || Date.now() < deadline); attempt += 1) {
|
|
365
|
+
if (step === 'queued') step = queuedStep(fsImpl, lockDir, me, options);
|
|
366
|
+
else {
|
|
367
|
+
me = { name: (options.entryName || newToken)() }; // a fresh entry name per attempt: an earlier one's name never comes back
|
|
368
|
+
step = attemptLock(fsImpl, lockDir, me, options);
|
|
114
369
|
}
|
|
370
|
+
if (step === 'held') return ownedRelease(fsImpl, lockDir, me);
|
|
371
|
+
if (step === 'busy' || step === 'queued') sleep(LOCK_WAIT_MS);
|
|
115
372
|
}
|
|
116
|
-
|
|
117
|
-
|
|
373
|
+
if (step === 'queued' && giveUp(fsImpl, lockDir, me, options) === 'held') return ownedRelease(fsImpl, lockDir, me);
|
|
374
|
+
throw new Error(`Could not lock '${file}' after ${(retries * LOCK_WAIT_MS) / 1000}s — another writer holds '${lockDir}'. `
|
|
375
|
+
+ `If no other DoFlow process is running, the lock is left from a crash and is reclaimed once it is ${LOCK_STALE_MS / 1000}s old; retry then.`
|
|
376
|
+
+ strayEntries(fsImpl, lockDir));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Names a non-directory entry in the lock (no DoFlow runtime makes one; it blocks the lock until removed). */
|
|
380
|
+
function strayEntries(fsImpl , lockDir ) {
|
|
381
|
+
let stray = [];
|
|
382
|
+
try { stray = fsImpl.readdirSync(lockDir, { withFileTypes: true }).filter((d ) => !d.isDirectory()).map((d ) => d.name); } catch { return ''; }
|
|
383
|
+
if (stray.length === 0) return '';
|
|
384
|
+
return ` It holds ${stray.map((name) => `'${name}'`).join(', ')}, which no DoFlow runtime makes and which keeps it locked: `
|
|
385
|
+
+ `remove ${stray.length > 1 ? 'them' : 'it'} once no DoFlow process is running.`;
|
|
118
386
|
}
|
|
119
387
|
|
|
120
388
|
/**
|
|
121
389
|
* Takes the cross-process lock for one state file with a bounded wait.
|
|
122
|
-
* Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5)
|
|
390
|
+
* Makes 1 try plus `retries` retries (default 3) separated by `waitMs` (default 5); each try is one
|
|
391
|
+
* tryAcquireLock, which waits at most 40 ms, and only on a process that reached the lock at the same moment.
|
|
123
392
|
* Throws an Error('store-locked') after one try plus retries.
|
|
124
393
|
* @param {Object} fsImpl
|
|
125
394
|
* @param {string} file
|
|
@@ -166,7 +435,9 @@ function readTaskState(fsImpl , file ) {
|
|
|
166
435
|
try {
|
|
167
436
|
data = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
|
|
168
437
|
} catch (error ) {
|
|
169
|
-
|
|
438
|
+
// The parser's own message quotes the first bytes of the file; a state file can hold text the
|
|
439
|
+
// reader should not echo, so only the error's class is named.
|
|
440
|
+
const failure = new Error(`Failed to load state file '${file}': its content is not valid JSON (${error?.name || 'Error'}). The write path is atomic `
|
|
170
441
|
+ '(tmp + rename), so a torn file means outside interference; recover it from version control '
|
|
171
442
|
+ 'or move it aside and re-record the task\'s items.');
|
|
172
443
|
failure.code = 'store-unreadable';
|
|
@@ -181,6 +452,27 @@ function readTaskState(fsImpl , file ) {
|
|
|
181
452
|
return data;
|
|
182
453
|
}
|
|
183
454
|
|
|
455
|
+
/** How many times the folder is made again when it vanishes before the lock can be made in it. */
|
|
456
|
+
const FOLDER_RACE_RETRIES = 5;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Makes the state file's folder and takes the lock in it. The lock is a folder inside the state
|
|
460
|
+
* file's folder and is made without `recursive`, so a sweep that removes an empty namespace folder
|
|
461
|
+
* between the two steps makes the lock fail with ENOENT. The folder is then made again, a bounded
|
|
462
|
+
* number of times; the sweep never removes a folder that holds a lock, so a taken lock keeps it.
|
|
463
|
+
*/
|
|
464
|
+
function acquireInFolder(fsImpl , file , acquire ) {
|
|
465
|
+
for (let tries = 0; ; tries += 1) {
|
|
466
|
+
fsImpl.mkdirSync(path.dirname(file), { recursive: true });
|
|
467
|
+
try {
|
|
468
|
+
return acquire(fsImpl, file);
|
|
469
|
+
} catch (error ) {
|
|
470
|
+
if (error && error.code === 'ENOENT' && tries < FOLDER_RACE_RETRIES) continue;
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
184
476
|
/**
|
|
185
477
|
* The one durable-update path: lock, re-read, merge, bump revision, atomic write.
|
|
186
478
|
*
|
|
@@ -194,6 +486,9 @@ function readTaskState(fsImpl , file ) {
|
|
|
194
486
|
* that must inspect or set aside what is there under the same exclusion as the write
|
|
195
487
|
* @param {(fsImpl: Object, file: string) => () => void} [options.acquire] optional lock acquirer
|
|
196
488
|
* defaulting to acquireLock
|
|
489
|
+
* @param {number} [options.lostLockRetries=0] how many times to run the whole locked update again
|
|
490
|
+
* when the lock was reclaimed from this writer before its rename (nothing was written); only for a
|
|
491
|
+
* caller whose `prepare` and `build` have no effect outside the payload they return
|
|
197
492
|
* @returns {string|null} the file path written, or null when build declines a write
|
|
198
493
|
*/
|
|
199
494
|
function updateTaskState({
|
|
@@ -202,37 +497,51 @@ function updateTaskState({
|
|
|
202
497
|
build,
|
|
203
498
|
prepare,
|
|
204
499
|
acquire = acquireLock,
|
|
500
|
+
lostLockRetries = 0,
|
|
205
501
|
}
|
|
206
502
|
|
|
207
503
|
|
|
208
504
|
|
|
209
505
|
|
|
210
506
|
|
|
507
|
+
|
|
211
508
|
) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
509
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
510
|
+
const release = acquireInFolder(fsImpl, file, acquire);
|
|
511
|
+
try {
|
|
512
|
+
if (prepare) prepare();
|
|
513
|
+
const disk = readTaskState(fsImpl, file);
|
|
514
|
+
const built = build(disk);
|
|
515
|
+
if (built === null) return null;
|
|
516
|
+
const payload = {
|
|
517
|
+
...built,
|
|
518
|
+
version: SCHEMA_VERSION,
|
|
519
|
+
revision: (disk && Number.isInteger(disk.revision) ? disk.revision : 0) + 1,
|
|
520
|
+
};
|
|
521
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
522
|
+
fsImpl.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
523
|
+
// A writer paused past LOCK_STALE_MS may have been reclaimed; its read is then stale.
|
|
524
|
+
if (typeof release.held === 'function' && !release.held()) {
|
|
525
|
+
try { fsImpl.rmSync(tmp, { force: true }); } catch { /* stale-temp pruning removes it later */ }
|
|
526
|
+
if (attempt < lostLockRetries) continue;
|
|
527
|
+
const lost = new Error(`Lost the lock on '${file}' to another writer before writing (this writer stalled past `
|
|
528
|
+
+ `${LOCK_STALE_MS / 1000}s${lostLockRetries ? `, ${lostLockRetries + 1} times` : ''}). Nothing was written; run the command again.`);
|
|
529
|
+
lost.code = 'lock-lost';
|
|
530
|
+
throw lost;
|
|
531
|
+
}
|
|
532
|
+
fsImpl.renameSync(tmp, file);
|
|
533
|
+
return file;
|
|
534
|
+
} finally {
|
|
535
|
+
release();
|
|
536
|
+
}
|
|
230
537
|
}
|
|
231
538
|
}
|
|
232
539
|
|
|
233
540
|
module.exports = {
|
|
234
541
|
acquireLock,
|
|
235
542
|
tryAcquireLock,
|
|
543
|
+
LOCK_STALE_MS,
|
|
544
|
+
LOCK_WAIT_MS,
|
|
236
545
|
boundedAcquire,
|
|
237
546
|
updateTaskState,
|
|
238
547
|
readTaskState,
|