@khoavu882/doflow 2.0.0-beta.5 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/bin/doflow.js +22 -12
- package/core/.antigravity-plugin/plugin.json +1 -2
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/lib.sh +43 -24
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
- package/core/harnesses/claude/hooks/post-compact.sh +35 -32
- package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
- package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
- package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
- package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/claude/hooks/session-end.sh +23 -10
- package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
- package/core/harnesses/claude/settings/settings.json +26 -9
- package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
- package/core/harnesses/codex/agents/core-implementer.toml +1 -0
- package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
- package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
- package/core/harnesses/codex/agents/research-writer.toml +1 -1
- package/core/harnesses/codex/agents/reviewer.toml +10 -0
- package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
- package/core/harnesses/codex/agents/system-architect.toml +1 -1
- package/core/harnesses/codex/hooks/hooks.json +0 -10
- package/core/harnesses/codex/hooks/lib.sh +44 -25
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
- package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
- package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
- package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
- package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
- package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
- package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
- package/core/harnesses/copilot/hooks/doflow.json +51 -0
- package/core/harnesses/kiro/agents/doflow.json +1 -11
- package/core/harnesses/kiro/hooks/doflow.json +0 -10
- package/core/harnesses/kiro/hooks/lib.sh +26 -6
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
- package/core/harnesses/kiro/hooks/session-start.sh +1 -0
- package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
- package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
- package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
- package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
- package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
- package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
- package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
- package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
- package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
- package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
- package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
- package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
- package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
- package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
- package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
- package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
- package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
- package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
- package/core/harnesses/shared/locator/doflow-run +2 -3
- package/core/registry/advice.json +3 -3
- package/core/registry/assets.json +55 -82
- package/core/registry/capabilities.json +12 -0
- package/core/registry/contracts.json +58 -31
- package/core/registry/contribution-points.json +35 -0
- package/core/registry/gates.json +32 -0
- package/core/registry/guidance-caps.json +40 -0
- package/core/registry/harnesses.json +1445 -366
- package/core/registry/intake.json +53 -0
- package/core/registry/lifecycle.json +599 -996
- package/core/registry/mcp.json +5 -1
- package/core/registry/models.json +7 -1
- package/core/registry/package-templates/README.md +19 -0
- package/core/registry/package-templates/agent/files/agent.md +9 -0
- package/core/registry/package-templates/agent/template.json +4 -0
- package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
- package/core/registry/package-templates/capability-provider/template.json +11 -0
- package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
- package/core/registry/package-templates/debate-preset/template.json +4 -0
- package/core/registry/package-templates/intake-category/files/category.json +8 -0
- package/core/registry/package-templates/intake-category/template.json +4 -0
- package/core/registry/package-templates/review-preset/files/preset.json +30 -0
- package/core/registry/package-templates/review-preset/template.json +4 -0
- package/core/registry/package-templates/skill/files/SKILL.md +8 -0
- package/core/registry/package-templates/skill/template.json +4 -0
- package/core/registry/package-templates/worker/files/worker.json +92 -0
- package/core/registry/package-templates/worker/template.json +11 -0
- package/core/registry/packages/doflow/doflow-package.json +15 -0
- package/core/registry/packages/doflow/presets/debate-3.json +26 -0
- package/core/registry/protocols.json +61 -0
- package/core/registry/routes.json +17 -0
- package/core/registry/rules.json +431 -0
- package/core/registry/settings.json +272 -0
- package/core/registry/storage.json +687 -31
- package/core/registry/telemetry.json +236 -0
- package/core/registry/transcripts.json +142 -0
- package/core/registry/verification.json +76 -6
- package/core/registry/workflows.json +55 -0
- package/core/shared/agent-specs/core-implementer.md +3 -0
- package/core/shared/agent-specs/delivery-lead.md +68 -0
- package/core/shared/agent-specs/quality-guardian.md +5 -1
- package/core/shared/agent-specs/research-writer.md +10 -3
- package/core/shared/agent-specs/reviewer.md +53 -0
- package/core/shared/agent-specs/spec-analyst.md +5 -6
- package/core/shared/agent-specs/system-architect.md +5 -1
- package/core/shared/guidance/DOFLOW_CORE.md +1 -5
- package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
- package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
- package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
- package/core/shared/guidance/pointers/codex.md +1 -4
- package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
- package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
- package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
- package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
- package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
- package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
- package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
- package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
- package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
- package/core/shared/guidance/rules/RULES.md +45 -0
- package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
- package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
- package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
- package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
- package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
- package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
- package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
- package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
- package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
- package/core/shared/scripts/doflow/bin/doflow-run +152 -34
- package/core/shared/skills/do/SKILL.md +14 -12
- package/core/shared/skills/do/references/advice_calls.md +26 -0
- package/core/shared/skills/do/references/pm_routing.md +1 -2
- package/core/shared/skills/do/references/program_mode.md +31 -0
- package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
- package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
- package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
- package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
- package/core/shared/skills/do-code-review/SKILL.md +48 -148
- package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
- package/core/shared/skills/do-code-review/references/tools.md +90 -0
- package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
- package/core/shared/skills/do-design/SKILL.md +23 -20
- package/core/shared/skills/do-diagnose/SKILL.md +18 -6
- package/core/shared/skills/do-document/SKILL.md +12 -1
- package/core/shared/skills/do-document/references/deep_research.md +1 -2
- package/core/shared/skills/do-document/references/embedded-research.md +19 -7
- package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
- package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
- package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
- package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
- package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
- package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
- package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
- package/core/shared/skills/do-flow/SKILL.md +10 -11
- package/core/shared/skills/do-flow/references/task_classes.md +5 -5
- package/core/shared/skills/do-git/SKILL.md +4 -0
- package/core/shared/skills/do-implement/SKILL.md +13 -14
- package/core/shared/skills/do-plan/SKILL.md +34 -20
- package/core/shared/skills/do-test/SKILL.md +7 -4
- package/core/shared/templates/doflow/constitution-template.md +9 -3
- package/core/shared/templates/doflow/data-model-template.md +1 -1
- package/core/shared/templates/doflow/design-template.md +113 -16
- package/core/shared/templates/doflow/plan-template.md +11 -10
- package/core/shared/templates/doflow/question-log-template.md +14 -2
- package/core/shared/templates/doflow/requirement-template.md +1 -1
- package/core/shared/workflows/do-flow-chain.md +2 -0
- package/core/worker-evidence/antigravity/checks.json +14 -0
- package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
- package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
- package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
- package/core/worker-evidence/codex/checks.json +38 -0
- package/package.json +10 -6
- package/src/adapters/antigravity/index.js +143 -66
- package/src/adapters/claude/index.js +46 -5
- package/src/adapters/codex/agents.js +25 -15
- package/src/adapters/codex/hooks.js +39 -3
- package/src/adapters/codex/index.js +61 -13
- package/src/adapters/codex/native-projection.js +6 -2
- package/src/adapters/copilot/index.js +58 -23
- package/src/adapters/copy-tree.js +291 -46
- package/src/adapters/guidance-manifest.js +71 -0
- package/src/adapters/guidance-pointer.js +19 -0
- package/src/adapters/hook-commands.js +6 -10
- package/src/adapters/index.js +1 -0
- package/src/adapters/kiro/index.js +23 -12
- package/src/adapters/opencode/index.js +8 -8
- package/src/adapters/pi/hooks.js +2 -1
- package/src/adapters/pi/index.js +14 -7
- package/src/adapters/resolve-source.js +26 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/reconcile.js +53 -9
- package/src/cli/commands/remove.js +2 -2
- package/src/cli/commands/rollback.js +6 -3
- package/src/cli/commands/self-update.js +18 -9
- package/src/cli/commands/status.js +22 -3
- package/src/cli/commands/ui.js +99 -0
- package/src/cli/commands/update.js +2 -1
- package/src/cli/index.js +337 -32
- package/src/cli/runtime-commands.js +220 -60
- package/src/cli/shared.js +0 -2
- package/src/helper/git.js +1 -1
- package/src/helper/guardrails.js +1 -1
- package/src/helper/marker-merge.js +31 -13
- package/src/helper/package-ids.js +37 -0
- package/src/helper/semver.js +94 -0
- package/src/install/backup.js +105 -15
- package/src/install/context.js +1 -1
- package/src/install/targets.js +31 -16
- package/src/lifecycle/coverage.js +4 -16
- package/src/lifecycle/hook-tier.js +160 -0
- package/src/lifecycle/index.js +412 -78
- package/src/lifecycle/retired-harnesses.js +318 -0
- package/src/lifecycle/view.js +35 -15
- package/src/registry/index.js +551 -26
- package/src/registry/worker-ids.js +45 -0
- package/src/release/build-package.js +0 -3
- package/src/release/docs-blocks.js +472 -0
- package/src/release/preflight.js +425 -0
- package/src/release/rehearse.js +401 -0
- package/src/release/release-workflow.js +733 -0
- package/src/release/sync-plugin-versions.js +43 -7
- package/src/runtime/advice/bench-gate.js +25 -0
- package/src/runtime/advice/cli.js +59 -16
- package/src/runtime/advice/client.js +32 -9
- package/src/runtime/advice/config.js +98 -133
- package/src/runtime/advice/doctor.js +1 -1
- package/src/runtime/advice/elevation.js +97 -0
- package/src/runtime/advice/endpoint-guard.js +9 -3
- package/src/runtime/advice/mode.js +37 -0
- package/src/runtime/advice/record.js +18 -5
- package/src/runtime/advice/registry.js +4 -1
- package/src/runtime/advice/shadow-config.js +229 -0
- package/src/runtime/advice/shadow-dispatcher.js +276 -0
- package/src/runtime/brief-rules.js +81 -0
- package/src/runtime/brief-tools.js +111 -34
- package/src/runtime/candidate.js +20 -11
- package/src/runtime/capabilities/executor.js +450 -0
- package/src/runtime/capabilities/loopback.js +41 -0
- package/src/runtime/capabilities/sanitizer.js +88 -0
- package/src/runtime/capability-router.js +135 -3
- package/src/runtime/checkouts.js +73 -11
- package/src/runtime/claims.js +13 -7
- package/src/runtime/cli.js +196 -19
- package/src/runtime/config-home.js +42 -0
- package/src/runtime/context/arbitrate.js +767 -0
- package/src/runtime/context/delivery-store.js +4 -6
- package/src/runtime/context/emission-rules.js +118 -0
- package/src/runtime/context/identity.js +227 -0
- package/src/runtime/context/pointer.js +67 -12
- package/src/runtime/context/primary-command.js +117 -0
- package/src/runtime/context/primary.js +344 -0
- package/src/runtime/context/summary-store.js +655 -0
- package/src/runtime/context-pack.js +158 -79
- package/src/runtime/decision-register.js +13 -4
- package/src/runtime/dispatch/admission.js +392 -0
- package/src/runtime/dispatch/attempt-end.js +274 -0
- package/src/runtime/dispatch/command.js +628 -0
- package/src/runtime/dispatch/controls.js +904 -0
- package/src/runtime/dispatch/envelope.js +276 -0
- package/src/runtime/dispatch/grants.js +310 -0
- package/src/runtime/dispatch/integrate.js +617 -0
- package/src/runtime/dispatch/loop.js +1835 -0
- package/src/runtime/dispatch/store.js +352 -0
- package/src/runtime/dispatch/verify-run.js +201 -0
- package/src/runtime/evidence-ledger.js +52 -7
- package/src/runtime/failure/capture.js +3 -1
- package/src/runtime/failure/cli.js +25 -9
- package/src/runtime/failure/home.js +57 -30
- package/src/runtime/failure/store.js +7 -2
- package/src/runtime/feature-pointer.js +174 -0
- package/src/runtime/feature-resolve.js +13 -2
- package/src/runtime/fleet/command.js +77 -0
- package/src/runtime/fleet/projects.js +178 -0
- package/src/runtime/fleet/tree.js +383 -0
- package/src/runtime/freshness.js +33 -3
- package/src/runtime/git-memo.js +46 -0
- package/src/runtime/guidance/ablation.js +427 -0
- package/src/runtime/health.js +624 -18
- package/src/runtime/history-compactor.js +13 -3
- package/src/runtime/implementation-gate.js +281 -26
- package/src/runtime/intake/engine.js +677 -0
- package/src/runtime/intake/index.js +136 -0
- package/src/runtime/intake/manifest.js +140 -0
- package/src/runtime/intake/taxonomy.js +206 -0
- package/src/runtime/inventory/coverage.js +1 -1
- package/src/runtime/inventory/index.js +1 -1
- package/src/runtime/inventory/precedence.js +2 -2
- package/src/runtime/inventory/siblings.js +6 -4
- package/src/runtime/knowledge/code-corpus.js +277 -0
- package/src/runtime/knowledge/code-lexical.js +130 -0
- package/src/runtime/knowledge/code-retrieval.js +307 -0
- package/src/runtime/knowledge/code-validation.js +106 -0
- package/src/runtime/knowledge/gp3-gate.js +74 -0
- package/src/runtime/knowledge/graph-adapter.js +288 -0
- package/src/runtime/knowledge/graph-fallback.js +394 -0
- package/src/runtime/knowledge/hybrid.js +136 -8
- package/src/runtime/knowledge/retrieval.js +34 -1
- package/src/runtime/knowledge/rtk-compress.js +77 -0
- package/src/runtime/knowledge/semble-bridge.py +106 -0
- package/src/runtime/knowledge/semble-lifecycle.js +114 -0
- package/src/runtime/knowledge/semble-provider.js +365 -0
- package/src/runtime/lifecycle/cli.js +88 -10
- package/src/runtime/lifecycle/event-store.js +8 -6
- package/src/runtime/lifecycle/followup.js +38 -7
- package/src/runtime/lifecycle/overview.js +26 -2
- package/src/runtime/lifecycle/report-store.js +8 -10
- package/src/runtime/lifecycle/retention.js +95 -31
- package/src/runtime/lifecycle/root.js +9 -1
- package/src/runtime/lifecycle/status.js +34 -17
- package/src/runtime/lifecycle/store-upkeep.js +56 -7
- package/src/runtime/mask.js +134 -7
- package/src/runtime/mcp-consultations.js +40 -6
- package/src/runtime/model-router.js +79 -7
- package/src/runtime/models/catalogue.js +191 -0
- package/src/runtime/models/command.js +153 -0
- package/src/runtime/off-words.js +12 -0
- package/src/runtime/outcome.js +2 -2
- package/src/runtime/packages/command.js +1009 -0
- package/src/runtime/packages/contributions.js +116 -0
- package/src/runtime/packages/digest.js +132 -0
- package/src/runtime/packages/fetch.js +182 -0
- package/src/runtime/packages/grants.js +192 -0
- package/src/runtime/packages/index-file.js +134 -0
- package/src/runtime/packages/instruction.js +90 -0
- package/src/runtime/packages/manifest.js +331 -0
- package/src/runtime/packages/points.js +487 -0
- package/src/runtime/packages/programs.js +216 -0
- package/src/runtime/packages/scaffold.js +277 -0
- package/src/runtime/packages/source.js +90 -0
- package/src/runtime/packages/state.js +164 -0
- package/src/runtime/packages/store.js +449 -0
- package/src/runtime/packages/tar.js +106 -0
- package/src/runtime/packages/view.js +406 -0
- package/src/runtime/plan-check.js +59 -0
- package/src/runtime/plan-content.js +324 -0
- package/src/runtime/policy-init.js +118 -0
- package/src/runtime/profiles/doctor.js +96 -0
- package/src/runtime/profiles/resolve.js +244 -0
- package/src/runtime/profiles/rollback.js +254 -0
- package/src/runtime/profiles/sampler.js +41 -0
- package/src/runtime/readiness-record.js +16 -11
- package/src/runtime/readiness.js +46 -5
- package/src/runtime/research-request.js +50 -25
- package/src/runtime/retrieval-plan.js +10 -30
- package/src/runtime/retrieval-record.js +75 -5
- package/src/runtime/review/command.js +246 -0
- package/src/runtime/review/dominance.js +68 -0
- package/src/runtime/review/findings.js +434 -0
- package/src/runtime/review/identity.js +255 -0
- package/src/runtime/review/protocol-command.js +109 -0
- package/src/runtime/review/protocols.js +405 -0
- package/src/runtime/review/resolve-protocol.js +302 -0
- package/src/runtime/review/start.js +530 -0
- package/src/runtime/roadmap/attendance.js +137 -0
- package/src/runtime/roadmap/facts.js +479 -0
- package/src/runtime/roadmap/gates.js +273 -0
- package/src/runtime/roadmap/loop.js +1719 -0
- package/src/runtime/roadmap/merge.js +390 -0
- package/src/runtime/roadmap/rows.js +1122 -0
- package/src/runtime/roadmap/seed.js +1063 -0
- package/src/runtime/roadmap/worktree-cleanup.js +147 -0
- package/src/runtime/rules-report.js +336 -0
- package/src/runtime/scaffold/artifacts.js +60 -9
- package/src/runtime/scaffold/generate.js +21 -9
- package/src/runtime/settings/autopilot.js +106 -0
- package/src/runtime/settings/cli.js +280 -0
- package/src/runtime/settings/config-write.js +314 -0
- package/src/runtime/settings/files.js +271 -0
- package/src/runtime/settings/legacy.js +154 -0
- package/src/runtime/settings/lexical.js +56 -0
- package/src/runtime/settings/prepared-run.js +269 -0
- package/src/runtime/settings/project-layers.js +118 -0
- package/src/runtime/settings/refs.js +288 -0
- package/src/runtime/settings/registry.js +553 -0
- package/src/runtime/settings/resolve.js +713 -0
- package/src/runtime/settings/shared.js +37 -0
- package/src/runtime/settings/worker-billing.js +107 -0
- package/src/runtime/settings/write.js +269 -0
- package/src/runtime/slot-command.js +78 -0
- package/src/runtime/slots.js +671 -0
- package/src/runtime/source-version.js +1 -0
- package/src/runtime/state-root.js +217 -0
- package/src/runtime/storage/cli.js +12 -5
- package/src/runtime/storage/engine.js +9 -2
- package/src/runtime/storage/registry.js +14 -1
- package/src/runtime/storage/roots.js +66 -16
- package/src/runtime/storage/rules.js +82 -10
- package/src/runtime/storage/upkeep.js +10 -2
- package/src/runtime/storage/work-status.js +245 -26
- package/src/runtime/task-attempts.js +589 -69
- package/src/runtime/task-classifier.js +85 -14
- package/src/runtime/task-graph.js +373 -0
- package/src/runtime/task-run.js +778 -0
- package/src/runtime/task-scope.js +176 -55
- package/src/runtime/task-sources.js +147 -0
- package/src/runtime/task-state.js +373 -64
- package/src/runtime/team/admission.js +301 -0
- package/src/runtime/team/envelope.js +324 -0
- package/src/runtime/team/review-guard.js +156 -0
- package/src/runtime/trace/analyze.js +427 -0
- package/src/runtime/trace/baseline.js +770 -0
- package/src/runtime/trace/ledger.js +63 -140
- package/src/runtime/trace/otlp-export.js +568 -0
- package/src/runtime/trace/otlp-map.js +481 -0
- package/src/runtime/trace/spans.js +745 -0
- package/src/runtime/trace/telemetry.js +225 -0
- package/src/runtime/trace/views.js +1 -0
- package/src/runtime/transcripts/command.js +98 -0
- package/src/runtime/transcripts/cursor.js +326 -0
- package/src/runtime/transcripts/read.js +126 -0
- package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
- package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
- package/src/runtime/transcripts/readers/copilot-events.js +90 -0
- package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
- package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
- package/src/runtime/transcripts/registry.js +161 -0
- package/src/runtime/transcripts/stats.js +161 -0
- package/src/runtime/verb-view.js +267 -0
- package/src/runtime/verification/check-key.js +628 -0
- package/src/runtime/verification/contract-runner.js +546 -11
- package/src/runtime/verification/engine.js +499 -54
- package/src/runtime/verification/impact.js +617 -0
- package/src/runtime/verification/inflight.js +205 -0
- package/src/runtime/verification/registry.js +176 -0
- package/src/runtime/verification/reuse.js +184 -0
- package/src/runtime/verification/telemetry-emit.js +254 -0
- package/src/runtime/verification/test-selection.js +67 -7
- package/src/runtime/worker/active.js +90 -0
- package/src/runtime/worker/cancel.js +222 -0
- package/src/runtime/worker/catalogue.js +122 -0
- package/src/runtime/worker/command.js +677 -0
- package/src/runtime/worker/evidence.js +288 -0
- package/src/runtime/worker/launch.js +256 -0
- package/src/runtime/worker/models.js +137 -0
- package/src/runtime/worker/parsers.js +218 -0
- package/src/runtime/worker/probes/index.js +706 -0
- package/src/runtime/worker/process-tree.js +415 -0
- package/src/runtime/worker/quota.js +43 -0
- package/src/runtime/worker/readiness.js +713 -0
- package/src/runtime/worker/run-store.js +166 -0
- package/src/runtime/worker/supervisor.js +541 -0
- package/src/runtime/worker/worktree-check.js +252 -0
- package/src/runtime/workflow-engine.js +5 -1
- package/src/runtime/workflow-orchestrator.js +610 -67
- package/src/runtime/worktree.js +149 -22
- package/src/state/index.js +1 -1
- package/src/state/lockfile.js +64 -10
- package/src/state/run-lock.js +1 -1
- package/types/node-commonjs.d.ts +106 -7
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/hooks.json +0 -77
- package/core/harnesses/gemini/hooks/lib.sh +0 -302
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
- package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
- package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
- package/core/harnesses/gemini/hooks/session-end.sh +0 -5
- package/core/harnesses/gemini/hooks/session-start.sh +0 -7
- package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
- package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
- package/core/shared/guidance/FLAGS.md +0 -25
- package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
- package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
- package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
- package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
- package/src/adapters/gemini/hooks.js +0 -251
- package/src/adapters/gemini/index.js +0 -429
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Roadmap seed: validator, judgeDraft and handleSeedCommand (feature 113, IC-001..IC-008).
|
|
5
|
+
* Pure judgement of drafts against store rows; person-only refusals; lease-held writes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const { sha256Digest, gatherGitFacts } = require('./facts.js');
|
|
12
|
+
const {
|
|
13
|
+
seedFindings,
|
|
14
|
+
safeText,
|
|
15
|
+
validWriteSet,
|
|
16
|
+
SEED_RULES,
|
|
17
|
+
deriveStatus,
|
|
18
|
+
render,
|
|
19
|
+
renderRoadmapFile,
|
|
20
|
+
pathsOverlap,
|
|
21
|
+
writeSetsOverlap,
|
|
22
|
+
} = require('./rows.js');
|
|
23
|
+
const {
|
|
24
|
+
acquireLease,
|
|
25
|
+
releaseLease,
|
|
26
|
+
resolveMainRoot,
|
|
27
|
+
noPsAlive,
|
|
28
|
+
} = require('./loop.js');
|
|
29
|
+
const { readSeedSwitch } = require('../settings/autopilot.js');
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
function judgeDraft({
|
|
65
|
+
draftRaw,
|
|
66
|
+
seedRaw,
|
|
67
|
+
gitFacts = {},
|
|
68
|
+
op = 'check',
|
|
69
|
+
classes,
|
|
70
|
+
} ) {
|
|
71
|
+
let draftDoc ;
|
|
72
|
+
if (typeof draftRaw === 'string') {
|
|
73
|
+
try {
|
|
74
|
+
draftDoc = JSON.parse(draftRaw);
|
|
75
|
+
} catch (err ) {
|
|
76
|
+
return {
|
|
77
|
+
schema: 1,
|
|
78
|
+
action: 'seed',
|
|
79
|
+
op,
|
|
80
|
+
ok: false,
|
|
81
|
+
rows: [],
|
|
82
|
+
findings: [
|
|
83
|
+
{
|
|
84
|
+
rule: 'seed-invalid-json',
|
|
85
|
+
message: `Draft is not valid JSON: ${safeText(err.message)}`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
overlaps: [],
|
|
89
|
+
alone: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
draftDoc = draftRaw;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let seedDoc ;
|
|
97
|
+
if (typeof seedRaw === 'string') {
|
|
98
|
+
try {
|
|
99
|
+
seedDoc = JSON.parse(seedRaw);
|
|
100
|
+
} catch {
|
|
101
|
+
const storeFindings = seedFindings(seedRaw, { classes });
|
|
102
|
+
return {
|
|
103
|
+
schema: 1,
|
|
104
|
+
action: 'seed',
|
|
105
|
+
op,
|
|
106
|
+
ok: false,
|
|
107
|
+
rows: [],
|
|
108
|
+
findings: storeFindings,
|
|
109
|
+
overlaps: [],
|
|
110
|
+
alone: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
seedDoc = seedRaw;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!seedDoc || typeof seedDoc !== 'object' || Array.isArray(seedDoc)) {
|
|
118
|
+
const storeFindings = seedFindings(seedDoc, { classes });
|
|
119
|
+
return {
|
|
120
|
+
schema: 1,
|
|
121
|
+
action: 'seed',
|
|
122
|
+
op,
|
|
123
|
+
ok: false,
|
|
124
|
+
rows: [],
|
|
125
|
+
findings: storeFindings,
|
|
126
|
+
overlaps: [],
|
|
127
|
+
alone: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. Draft findings (draft mode checks shape only: keys, field shapes, slug/id, duplicate id, class)
|
|
132
|
+
const draftFindings = seedFindings(draftDoc, { draft: true, classes });
|
|
133
|
+
const storeDocumentFindings = seedFindings(seedDoc, { classes })
|
|
134
|
+
.filter((finding ) => finding.rowId === undefined && finding.rowIds === undefined);
|
|
135
|
+
const findingKey = (finding ) => `${finding.rule}|${finding.rowId ?? ''}|${(finding.rowIds || []).join(',')}|${finding.message}`;
|
|
136
|
+
const mergeUnique = (...lists ) => {
|
|
137
|
+
const seen = new Set ();
|
|
138
|
+
return lists.flat().filter((finding) => {
|
|
139
|
+
const key = findingKey(finding);
|
|
140
|
+
if (seen.has(key)) return false;
|
|
141
|
+
seen.add(key);
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!Array.isArray(seedDoc.rows)) {
|
|
147
|
+
const findings = mergeUnique(draftFindings, storeDocumentFindings)
|
|
148
|
+
.sort((a , b ) => SEED_RULES.indexOf(a.rule) - SEED_RULES.indexOf(b.rule));
|
|
149
|
+
return {
|
|
150
|
+
schema: 1,
|
|
151
|
+
action: 'seed',
|
|
152
|
+
op,
|
|
153
|
+
ok: false,
|
|
154
|
+
rows: [],
|
|
155
|
+
findings: findings.map((finding ) => ({ rule: finding.rule, message: finding.message })),
|
|
156
|
+
overlaps: [],
|
|
157
|
+
alone: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 3. Candidate document = store doc with draft amends replaced in place and draft adds appended
|
|
162
|
+
const storeRows = seedDoc.rows;
|
|
163
|
+
const candidateRows = [...storeRows];
|
|
164
|
+
const storeIndexMap = new Map ();
|
|
165
|
+
for (let i = 0; i < storeRows.length; i++) {
|
|
166
|
+
const r = storeRows[i];
|
|
167
|
+
if (r && typeof r === 'object' && typeof r.id === 'string') {
|
|
168
|
+
storeIndexMap.set(r.id, i);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const draftRows = (draftDoc && typeof draftDoc === 'object' && Array.isArray(draftDoc.rows))
|
|
173
|
+
? draftDoc.rows
|
|
174
|
+
: [];
|
|
175
|
+
|
|
176
|
+
for (const dRow of draftRows) {
|
|
177
|
+
if (dRow && typeof dRow === 'object' && typeof dRow.id === 'string') {
|
|
178
|
+
if (storeIndexMap.has(dRow.id)) {
|
|
179
|
+
candidateRows[storeIndexMap.get(dRow.id) ] = dRow;
|
|
180
|
+
} else {
|
|
181
|
+
candidateRows.push(dRow);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const candidate = {
|
|
187
|
+
...seedDoc,
|
|
188
|
+
rows: candidateRows,
|
|
189
|
+
};
|
|
190
|
+
const candidateFindings = seedFindings(candidate, { classes });
|
|
191
|
+
|
|
192
|
+
// Merge draft and candidate findings, dropping a candidate finding whose key equals one already present
|
|
193
|
+
const mergedFindings = mergeUnique(draftFindings, storeDocumentFindings, candidateFindings);
|
|
194
|
+
|
|
195
|
+
// 4. Per draft row (object with a string id), in draft order:
|
|
196
|
+
// change = amend when its id is a store row id, else add
|
|
197
|
+
// status for an amend row = status of STORE row from deriveStatus(storeRows, gitFacts).rowStatus[id]
|
|
198
|
+
const statusSafeRow = (r ) => r && typeof r === 'object' && !Array.isArray(r) &&
|
|
199
|
+
typeof r.id === 'string' && typeof r.slug === 'string' &&
|
|
200
|
+
(r.mergeCommit === undefined || typeof r.mergeCommit === 'string') &&
|
|
201
|
+
(r.hold === undefined || typeof r.hold === 'string');
|
|
202
|
+
const storeValidRows = storeRows.filter(statusSafeRow);
|
|
203
|
+
const { rowStatus: storeRowStatus } = deriveStatus(storeValidRows, gitFacts);
|
|
204
|
+
|
|
205
|
+
const resultRows = [];
|
|
206
|
+
const opFindings = [];
|
|
207
|
+
|
|
208
|
+
for (const dRow of draftRows) {
|
|
209
|
+
if (!dRow || typeof dRow !== 'object' || typeof dRow.id !== 'string') {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const id = dRow.id;
|
|
213
|
+
const isAmend = storeIndexMap.has(id);
|
|
214
|
+
const change = isAmend ? 'amend' : 'add';
|
|
215
|
+
|
|
216
|
+
if (isAmend) {
|
|
217
|
+
const status = storeRowStatus[id] || 'not-started';
|
|
218
|
+
resultRows.push({ id, change, status });
|
|
219
|
+
|
|
220
|
+
if (op === 'apply') {
|
|
221
|
+
opFindings.push({
|
|
222
|
+
rule: 'id-exists',
|
|
223
|
+
rowId: id,
|
|
224
|
+
message: `Row '${safeText(id)}' already exists in store; use --action amend to update`,
|
|
225
|
+
});
|
|
226
|
+
} else if (op === 'check' || op === 'amend') {
|
|
227
|
+
if (status === 'merged' || status === 'in-flight') {
|
|
228
|
+
opFindings.push({
|
|
229
|
+
rule: 'row-started',
|
|
230
|
+
rowId: id,
|
|
231
|
+
message: `Row '${safeText(id)}' has status '${safeText(status)}' and cannot be amended`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
resultRows.push({ id, change });
|
|
237
|
+
|
|
238
|
+
if (op === 'amend') {
|
|
239
|
+
opFindings.push({
|
|
240
|
+
rule: 'row-not-found',
|
|
241
|
+
rowId: id,
|
|
242
|
+
message: `Row '${safeText(id)}' not found in store; use --action apply to add`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 5. Sort ALL findings:
|
|
249
|
+
// document-level first; then by candidate row position of rowId (or first rowIds);
|
|
250
|
+
// then by SEED_RULES index; stable.
|
|
251
|
+
const candidateIdPos = new Map ();
|
|
252
|
+
for (let i = 0; i < candidateRows.length; i++) {
|
|
253
|
+
const r = candidateRows[i];
|
|
254
|
+
if (r && typeof r === 'object' && typeof r.id === 'string') {
|
|
255
|
+
if (!candidateIdPos.has(r.id)) {
|
|
256
|
+
candidateIdPos.set(r.id, i);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const allFindings = [...mergedFindings, ...opFindings].map((f, seq) => {
|
|
262
|
+
const targetId = f.rowId ?? (Array.isArray(f.rowIds) && f.rowIds.length > 0 ? f.rowIds[0] : undefined);
|
|
263
|
+
const isDocLevel = targetId === undefined;
|
|
264
|
+
const pos = isDocLevel ? -1 : (candidateIdPos.has(targetId) ? candidateIdPos.get(targetId) : Infinity);
|
|
265
|
+
const ruleIdx = (SEED_RULES ).indexOf(f.rule);
|
|
266
|
+
return { f, isDocLevel, pos, ruleIdx: ruleIdx >= 0 ? ruleIdx : Infinity, seq };
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
allFindings.sort((a, b) => {
|
|
270
|
+
if (a.isDocLevel !== b.isDocLevel) return a.isDocLevel ? -1 : 1;
|
|
271
|
+
if (a.pos !== b.pos) return a.pos - b.pos;
|
|
272
|
+
if (a.ruleIdx !== b.ruleIdx) return a.ruleIdx - b.ruleIdx;
|
|
273
|
+
return a.seq - b.seq;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const finalFindings = allFindings.map(({ f }) => {
|
|
277
|
+
const out = { rule: f.rule };
|
|
278
|
+
if (f.rowId !== undefined) out.rowId = f.rowId;
|
|
279
|
+
if (f.rowIds !== undefined) out.rowIds = f.rowIds;
|
|
280
|
+
out.message = f.message;
|
|
281
|
+
return out;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 7. Overlaps: statuses = deriveStatus(candidateValidRows, gitFacts)
|
|
285
|
+
// Pairs (i<j) of rows not merged, with at least one in draft id set, whose writeSets overlap
|
|
286
|
+
const candidateValidRows = candidateRows.filter(statusSafeRow);
|
|
287
|
+
const { rowStatus: candRowStatus } = deriveStatus(candidateValidRows, gitFacts);
|
|
288
|
+
|
|
289
|
+
const draftIdSet = new Set ();
|
|
290
|
+
for (const dRow of draftRows) {
|
|
291
|
+
if (dRow && typeof dRow === 'object' && typeof dRow.id === 'string') {
|
|
292
|
+
draftIdSet.add(dRow.id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const overlaps = [];
|
|
297
|
+
for (let i = 0; i < candidateValidRows.length; i++) {
|
|
298
|
+
const rI = candidateValidRows[i];
|
|
299
|
+
if (candRowStatus[rI.id] === 'merged') continue;
|
|
300
|
+
for (let j = i + 1; j < candidateValidRows.length; j++) {
|
|
301
|
+
const rJ = candidateValidRows[j];
|
|
302
|
+
if (candRowStatus[rJ.id] === 'merged') continue;
|
|
303
|
+
if (!draftIdSet.has(rI.id) && !draftIdSet.has(rJ.id)) continue;
|
|
304
|
+
const writeSetI = validWriteSet(rI.writeSet);
|
|
305
|
+
const writeSetJ = validWriteSet(rJ.writeSet);
|
|
306
|
+
if (!writeSetI || !writeSetJ || !writeSetsOverlap(writeSetI, writeSetJ)) continue;
|
|
307
|
+
|
|
308
|
+
const paths = [];
|
|
309
|
+
for (const pI of writeSetI) {
|
|
310
|
+
for (const pJ of writeSetJ) {
|
|
311
|
+
if (pathsOverlap(pI, pJ)) {
|
|
312
|
+
paths.push([pI, pJ]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
paths.sort(([aI, aJ], [bI, bJ]) => {
|
|
317
|
+
if (aI < bI) return -1;
|
|
318
|
+
if (aI > bI) return 1;
|
|
319
|
+
if (aJ < bJ) return -1;
|
|
320
|
+
if (aJ > bJ) return 1;
|
|
321
|
+
return 0;
|
|
322
|
+
});
|
|
323
|
+
overlaps.push({
|
|
324
|
+
rows: [rI.id, rJ.id],
|
|
325
|
+
paths,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 8. Alone: draft rows not merged that declare no writeSet or empty one
|
|
331
|
+
const alone = [];
|
|
332
|
+
const seenAlone = new Set ();
|
|
333
|
+
for (const dRow of draftRows) {
|
|
334
|
+
if (dRow && typeof dRow === 'object' && typeof dRow.id === 'string') {
|
|
335
|
+
const id = dRow.id;
|
|
336
|
+
if (seenAlone.has(id)) continue;
|
|
337
|
+
if (candRowStatus[id] !== 'merged') {
|
|
338
|
+
if (validWriteSet(dRow.writeSet) === null) {
|
|
339
|
+
seenAlone.add(id);
|
|
340
|
+
alone.push(id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
schema: 1,
|
|
348
|
+
action: 'seed',
|
|
349
|
+
op,
|
|
350
|
+
ok: finalFindings.length === 0,
|
|
351
|
+
rows: resultRows,
|
|
352
|
+
findings: finalFindings,
|
|
353
|
+
overlaps,
|
|
354
|
+
alone,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function printTextResult(
|
|
359
|
+
result ,
|
|
360
|
+
out ,
|
|
361
|
+
err ,
|
|
362
|
+
opts
|
|
363
|
+
) {
|
|
364
|
+
const op = safeText(result.op);
|
|
365
|
+
let statusPart ;
|
|
366
|
+
if (result.reason) {
|
|
367
|
+
statusPart = `refused: ${safeText(result.reason)}`;
|
|
368
|
+
} else if (result.findings.length === 0) {
|
|
369
|
+
statusPart = 'ok';
|
|
370
|
+
} else {
|
|
371
|
+
statusPart = `${result.findings.length} findings`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let addCount = 0;
|
|
375
|
+
let amendCount = 0;
|
|
376
|
+
for (const r of result.rows) {
|
|
377
|
+
if (r.change === 'add') addCount += 1;
|
|
378
|
+
if (r.change === 'amend') amendCount += 1;
|
|
379
|
+
}
|
|
380
|
+
const overlapCount = result.overlaps.length;
|
|
381
|
+
const aloneCount = result.alone.length;
|
|
382
|
+
|
|
383
|
+
let line1 = `roadmap seed ${op}: ${statusPart}, ${addCount} add, ${amendCount} amend, ${overlapCount} overlaps, ${aloneCount} alone`;
|
|
384
|
+
if ((op === 'apply' || op === 'amend') && result.ok) {
|
|
385
|
+
line1 += `, ${result.writes} files written`;
|
|
386
|
+
if (Array.isArray(result.proposed) && result.proposed.length > 0) {
|
|
387
|
+
line1 += `, ${result.proposed.length} proposed (no person verified, or autopilot is not verifiably off; a person runs in their terminal: doflow roadmap promote ${result.proposed.map(safeText).join(' ')})`;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (result.seed && result.seed.sha256 === null) {
|
|
391
|
+
line1 += ', store absent';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const detailLines = [];
|
|
395
|
+
|
|
396
|
+
// Findings
|
|
397
|
+
for (const f of result.findings) {
|
|
398
|
+
let idPart = '';
|
|
399
|
+
if (Array.isArray(f.rowIds) && f.rowIds.length > 0) {
|
|
400
|
+
idPart = f.rowIds.map(safeText).join(' ');
|
|
401
|
+
} else if (f.rowId) {
|
|
402
|
+
idPart = safeText(f.rowId);
|
|
403
|
+
}
|
|
404
|
+
const rule = safeText(f.rule);
|
|
405
|
+
// seedFindings escapes each raw value as it builds this message; this guards any other
|
|
406
|
+
// finding producer while preserving the full, untruncated prose of valid messages.
|
|
407
|
+
const message = typeof f.message === 'string'
|
|
408
|
+
? f.message.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
|
|
409
|
+
: safeText(f.message);
|
|
410
|
+
if (idPart) {
|
|
411
|
+
detailLines.push(` [${rule}] ${idPart}: ${message}`);
|
|
412
|
+
} else {
|
|
413
|
+
detailLines.push(` [${rule}] ${message}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Overlaps
|
|
418
|
+
for (const o of result.overlaps) {
|
|
419
|
+
const pair = o.paths && o.paths.length > 0 ? o.paths[0] : null;
|
|
420
|
+
let pathDesc = '';
|
|
421
|
+
if (pair) {
|
|
422
|
+
const [pA, pB] = pair;
|
|
423
|
+
pathDesc = pA === pB ? safeText(pA) : `${safeText(pA)} ~ ${safeText(pB)}`;
|
|
424
|
+
}
|
|
425
|
+
detailLines.push(` overlap ${safeText(o.rows[0])} ${safeText(o.rows[1])}: ${pathDesc}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Refusal single reason line
|
|
429
|
+
if (result.reason && opts?.refusalDetail) {
|
|
430
|
+
detailLines.push(` ${opts.refusalDetail}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const lines = [line1];
|
|
434
|
+
let maxDetails = 10;
|
|
435
|
+
if (typeof result.land === 'string') {
|
|
436
|
+
lines.push(` land (person only): ${result.land}`);
|
|
437
|
+
maxDetails = 9;
|
|
438
|
+
}
|
|
439
|
+
if (detailLines.length <= maxDetails) {
|
|
440
|
+
lines.push(...detailLines);
|
|
441
|
+
} else {
|
|
442
|
+
lines.push(...detailLines.slice(0, maxDetails));
|
|
443
|
+
const cut = detailLines.length - maxDetails;
|
|
444
|
+
lines.push(` ... ${cut} more (--json)`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const printFn = result.ok ? out : err;
|
|
448
|
+
for (const line of lines) {
|
|
449
|
+
printFn(line);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function quoteLandArgument(value ) {
|
|
454
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
455
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const LAND_CONTROL_CHARS = /[\u0000-\u001f\u007f\u2028\u2029]/;
|
|
459
|
+
|
|
460
|
+
function makeLandCommand(op , ok , rows , root , draftPath ) {
|
|
461
|
+
if (op !== 'check' || !ok || !Array.isArray(rows) || rows.length === 0) return null;
|
|
462
|
+
const change = rows[0]?.change;
|
|
463
|
+
if ((change !== 'add' && change !== 'amend') || !rows.every((row) => row?.change === change)) return null;
|
|
464
|
+
const resolvedRoot = path.resolve(root);
|
|
465
|
+
if (LAND_CONTROL_CHARS.test(resolvedRoot) || LAND_CONTROL_CHARS.test(draftPath)) return null;
|
|
466
|
+
const action = change === 'add' ? 'apply' : 'amend';
|
|
467
|
+
return `doflow roadmap seed ${quoteLandArgument(resolvedRoot)} --action ${action} --draft ${quoteLandArgument(draftPath)}`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function handleSeedCommand(options = {}) {
|
|
471
|
+
const {
|
|
472
|
+
action,
|
|
473
|
+
draft,
|
|
474
|
+
json = false,
|
|
475
|
+
cwd = process.cwd(),
|
|
476
|
+
root = resolveMainRoot(cwd),
|
|
477
|
+
fsImpl = fs,
|
|
478
|
+
gitFacts,
|
|
479
|
+
readSwitch,
|
|
480
|
+
alive,
|
|
481
|
+
env = process.env,
|
|
482
|
+
pid,
|
|
483
|
+
host,
|
|
484
|
+
now,
|
|
485
|
+
out = (s ) => console.log(s),
|
|
486
|
+
err = (s ) => console.error(s),
|
|
487
|
+
classes,
|
|
488
|
+
isTTY = process.stdin.isTTY === true,
|
|
489
|
+
confirm,
|
|
490
|
+
ttyClock,
|
|
491
|
+
ttySleep,
|
|
492
|
+
} = options;
|
|
493
|
+
|
|
494
|
+
const usage = (message ) => {
|
|
495
|
+
if (json) {
|
|
496
|
+
out(JSON.stringify({ error: 'usage', detail: message }));
|
|
497
|
+
} else {
|
|
498
|
+
err(`doflow roadmap: error: ${message}`);
|
|
499
|
+
}
|
|
500
|
+
return 2;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// 1. Usage check
|
|
504
|
+
if (!action || !['check', 'apply', 'amend'].includes(action)) {
|
|
505
|
+
return usage('--action must be one of check, apply, amend');
|
|
506
|
+
}
|
|
507
|
+
if (!draft) {
|
|
508
|
+
return usage('--draft <path> is required');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const absDraftPath = path.resolve(cwd, draft);
|
|
512
|
+
let draftRaw ;
|
|
513
|
+
try {
|
|
514
|
+
draftRaw = fsImpl.readFileSync(absDraftPath, 'utf8');
|
|
515
|
+
} catch (error ) {
|
|
516
|
+
return usage(`cannot read draft file '${absDraftPath}': ${error.message}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const storePath = path.join(root, 'agent-docs', 'roadmap', 'rows.json');
|
|
520
|
+
const tablePath = path.join(root, 'agent-docs', 'roadmap', 'ROADMAP.md');
|
|
521
|
+
const emptySeedRaw = JSON.stringify({ schema: 1, rows: [] });
|
|
522
|
+
|
|
523
|
+
let seedRaw ;
|
|
524
|
+
let storeAbsent = false;
|
|
525
|
+
try {
|
|
526
|
+
seedRaw = fsImpl.readFileSync(storePath, 'utf8');
|
|
527
|
+
} catch (error ) {
|
|
528
|
+
if (error?.code === 'ENOENT') {
|
|
529
|
+
seedRaw = emptySeedRaw;
|
|
530
|
+
storeAbsent = true;
|
|
531
|
+
} else {
|
|
532
|
+
return usage(`cannot read store file '${storePath}': ${error.message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 2. File digests
|
|
537
|
+
const draftSha = sha256Digest(draftRaw);
|
|
538
|
+
const seedSha = storeAbsent ? null : sha256Digest(seedRaw);
|
|
539
|
+
|
|
540
|
+
// 3. Check action
|
|
541
|
+
if (action === 'check') {
|
|
542
|
+
const facts = gitFacts || gatherGitFacts({ root });
|
|
543
|
+
if (facts.unreadable && facts.unreadable.length > 0) {
|
|
544
|
+
if (json) {
|
|
545
|
+
out(
|
|
546
|
+
JSON.stringify({
|
|
547
|
+
schema: 1,
|
|
548
|
+
action: 'seed',
|
|
549
|
+
op: action,
|
|
550
|
+
error: 'cannot-read-facts',
|
|
551
|
+
sources: facts.unreadable,
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
} else {
|
|
555
|
+
err(`doflow roadmap seed: cannot read facts: ${facts.unreadable.join(', ')}`);
|
|
556
|
+
}
|
|
557
|
+
return 1;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const judgement = judgeDraft({
|
|
561
|
+
draftRaw,
|
|
562
|
+
seedRaw,
|
|
563
|
+
gitFacts: facts,
|
|
564
|
+
op: action,
|
|
565
|
+
classes,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const result = {
|
|
569
|
+
schema: 1,
|
|
570
|
+
action: 'seed',
|
|
571
|
+
op: action,
|
|
572
|
+
ok: judgement.ok,
|
|
573
|
+
draft: { path: absDraftPath, sha256: draftSha },
|
|
574
|
+
seed: { path: storePath, sha256: seedSha },
|
|
575
|
+
rows: judgement.rows,
|
|
576
|
+
findings: judgement.findings,
|
|
577
|
+
overlaps: judgement.overlaps,
|
|
578
|
+
alone: judgement.alone,
|
|
579
|
+
reason: null,
|
|
580
|
+
switch: null,
|
|
581
|
+
writes: 0,
|
|
582
|
+
land: makeLandCommand(action, judgement.ok, judgement.rows, root, absDraftPath),
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
if (json) {
|
|
586
|
+
out(JSON.stringify(result));
|
|
587
|
+
} else {
|
|
588
|
+
printTextResult(result, out, err);
|
|
589
|
+
}
|
|
590
|
+
return result.ok ? 0 : 1;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 4. Apply / Amend action
|
|
594
|
+
// (a) maint-thin / AU C05: a seed write is allowed whatever the switch says. Rows land as drafted (ready) only when
|
|
595
|
+
// stdin is an interactive terminal AND either autopilot is verifiably off (the fail-closed seed reader) or a person
|
|
596
|
+
// answers yes to the same `/dev/tty` [y/N] that `roadmap promote` uses. A model with a shell controls the
|
|
597
|
+
// environment (an empty config home reads as off), so the switch alone cannot tell a person; and without the prompt
|
|
598
|
+
// a model with a pty could land ready rows while autopilot is on, which widens the queue. No terminal, or no/other
|
|
599
|
+
// answer, lands every row it adds or amends `proposed`, which autopilot never picks or advances until a person runs
|
|
600
|
+
// `roadmap promote`. The prompt comes before the roadmap lease (Ctrl-C would otherwise leave the lease behind) and
|
|
601
|
+
// only for a draft that judges clean against the store as read now. No flag or environment variable skips it.
|
|
602
|
+
const switchReader = readSwitch || readSeedSwitch;
|
|
603
|
+
let landProposed = isTTY !== true || switchReader(env, fsImpl).enabled !== false;
|
|
604
|
+
if (landProposed && isTTY === true) {
|
|
605
|
+
const preFacts = gitFacts || gatherGitFacts({ root });
|
|
606
|
+
const pre = preFacts.unreadable && preFacts.unreadable.length > 0 ? null
|
|
607
|
+
: judgeDraft({ draftRaw, seedRaw, gitFacts: preFacts, op: action, classes });
|
|
608
|
+
if (pre && pre.ok) {
|
|
609
|
+
const rowsList = (JSON.parse(draftRaw).rows || []).filter((r ) => r && typeof r === 'object');
|
|
610
|
+
const ids = rowsList.map((r ) => safeText(r.id));
|
|
611
|
+
const titles = rowsList.map((r ) => ` ${safeText(r.id)} ${safeText(r.title)}`);
|
|
612
|
+
const prompt = `${titles.join('\n')}\nLand ${ids.length} row(s) ready for autopilot (autopilot is not verifiably off): ${ids.join(' ')}? [y/N] `;
|
|
613
|
+
let answer = null;
|
|
614
|
+
try {
|
|
615
|
+
if (confirm) answer = confirm(prompt);
|
|
616
|
+
else {
|
|
617
|
+
const read = readTtyLine(prompt, { timeoutMs: SEED_PROMPT_TIMEOUT_MS, now: ttyClock, sleep: ttySleep });
|
|
618
|
+
answer = read.answer;
|
|
619
|
+
if (read.timedOut) err(`doflow roadmap seed: the [y/N] prompt timed out after ${Math.round(SEED_PROMPT_TIMEOUT_MS / 1000)} s with no answer; rows land proposed (run: doflow roadmap promote <id...>)`);
|
|
620
|
+
}
|
|
621
|
+
} catch { answer = null; }
|
|
622
|
+
if (typeof answer === 'string' && /^(y|yes)$/i.test(answer.trim())) landProposed = false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// (b) Acquire lease
|
|
627
|
+
const leaseDir = path.join(root, '.doflow', 'state', 'roadmap');
|
|
628
|
+
const nowFn = now ? now : () => new Date();
|
|
629
|
+
const nowDate = nowFn();
|
|
630
|
+
const me = {
|
|
631
|
+
schema: 1,
|
|
632
|
+
pid: pid ?? process.pid,
|
|
633
|
+
start: new Date(Date.now() - (process ).uptime() * 1000).toISOString(),
|
|
634
|
+
host: host ?? os.hostname(),
|
|
635
|
+
runId: null,
|
|
636
|
+
acquiredAt: nowDate.toISOString(),
|
|
637
|
+
};
|
|
638
|
+
const leaseAlive = alive || noPsAlive;
|
|
639
|
+
const lease = acquireLease(leaseDir, me, leaseAlive, fsImpl);
|
|
640
|
+
if (!lease.ok) {
|
|
641
|
+
const holder = lease.holder && typeof lease.holder === 'object' ? lease.holder : null;
|
|
642
|
+
const result = {
|
|
643
|
+
schema: 1,
|
|
644
|
+
action: 'seed',
|
|
645
|
+
op: action,
|
|
646
|
+
ok: false,
|
|
647
|
+
draft: { path: absDraftPath, sha256: draftSha },
|
|
648
|
+
seed: { path: storePath, sha256: seedSha },
|
|
649
|
+
rows: [],
|
|
650
|
+
findings: [],
|
|
651
|
+
overlaps: [],
|
|
652
|
+
alone: [],
|
|
653
|
+
reason: 'lease-held',
|
|
654
|
+
switch: null,
|
|
655
|
+
writes: 0,
|
|
656
|
+
land: null,
|
|
657
|
+
};
|
|
658
|
+
const holderDesc = holder
|
|
659
|
+
? `pid ${safeText(holder.pid ?? 'unknown')} on ${safeText(holder.host ?? 'unknown')}`
|
|
660
|
+
: 'unknown holder';
|
|
661
|
+
|
|
662
|
+
if (json) {
|
|
663
|
+
out(JSON.stringify(result));
|
|
664
|
+
} else {
|
|
665
|
+
printTextResult(result, out, err, {
|
|
666
|
+
refusalDetail: `lease held by ${holderDesc}`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return 1;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// (c) Under lease
|
|
673
|
+
try {
|
|
674
|
+
let freshSeedRaw ;
|
|
675
|
+
let freshStoreAbsent = false;
|
|
676
|
+
try {
|
|
677
|
+
freshSeedRaw = fsImpl.readFileSync(storePath, 'utf8');
|
|
678
|
+
} catch (error ) {
|
|
679
|
+
if (error?.code === 'ENOENT') {
|
|
680
|
+
freshSeedRaw = emptySeedRaw;
|
|
681
|
+
freshStoreAbsent = true;
|
|
682
|
+
} else {
|
|
683
|
+
return usage(`cannot read store file '${storePath}': ${error.message}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const freshSeedSha = freshStoreAbsent ? null : sha256Digest(freshSeedRaw);
|
|
687
|
+
|
|
688
|
+
const facts = gitFacts || gatherGitFacts({ root });
|
|
689
|
+
if (facts.unreadable && facts.unreadable.length > 0) {
|
|
690
|
+
if (json) {
|
|
691
|
+
out(
|
|
692
|
+
JSON.stringify({
|
|
693
|
+
schema: 1,
|
|
694
|
+
action: 'seed',
|
|
695
|
+
op: action,
|
|
696
|
+
error: 'cannot-read-facts',
|
|
697
|
+
sources: facts.unreadable,
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
} else {
|
|
701
|
+
err(`doflow roadmap seed: cannot read facts: ${facts.unreadable.join(', ')}`);
|
|
702
|
+
}
|
|
703
|
+
return 1;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const judgement = judgeDraft({
|
|
707
|
+
draftRaw,
|
|
708
|
+
seedRaw: freshSeedRaw,
|
|
709
|
+
gitFacts: facts,
|
|
710
|
+
op: action,
|
|
711
|
+
classes,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
if (!judgement.ok) {
|
|
715
|
+
const result = {
|
|
716
|
+
schema: 1,
|
|
717
|
+
action: 'seed',
|
|
718
|
+
op: action,
|
|
719
|
+
ok: false,
|
|
720
|
+
draft: { path: absDraftPath, sha256: draftSha },
|
|
721
|
+
seed: { path: storePath, sha256: freshSeedSha },
|
|
722
|
+
rows: judgement.rows,
|
|
723
|
+
findings: judgement.findings,
|
|
724
|
+
overlaps: judgement.overlaps,
|
|
725
|
+
alone: judgement.alone,
|
|
726
|
+
reason: null,
|
|
727
|
+
switch: null,
|
|
728
|
+
writes: 0,
|
|
729
|
+
land: null,
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
if (json) {
|
|
733
|
+
out(JSON.stringify(result));
|
|
734
|
+
} else {
|
|
735
|
+
printTextResult(result, out, err);
|
|
736
|
+
}
|
|
737
|
+
return 1;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// (d) No finding: build new store document and write
|
|
741
|
+
let parsedStore ;
|
|
742
|
+
try {
|
|
743
|
+
parsedStore = JSON.parse(freshSeedRaw);
|
|
744
|
+
} catch {
|
|
745
|
+
throw new Error('Seed invariant failed after clean judgement: store JSON could not be parsed');
|
|
746
|
+
}
|
|
747
|
+
if (!parsedStore || typeof parsedStore !== 'object' || Array.isArray(parsedStore) || !Array.isArray(parsedStore.rows)) {
|
|
748
|
+
throw new Error('Seed invariant failed after clean judgement: store rows must be an array');
|
|
749
|
+
}
|
|
750
|
+
const storeRows = parsedStore.rows;
|
|
751
|
+
const proposedIds = [];
|
|
752
|
+
const proposedIf = (row ) => {
|
|
753
|
+
if (!landProposed) return row;
|
|
754
|
+
proposedIds.push(row.id);
|
|
755
|
+
return { ...row, proposed: true };
|
|
756
|
+
};
|
|
757
|
+
let newRows ;
|
|
758
|
+
if (action === 'apply') {
|
|
759
|
+
const draftAddRows = [];
|
|
760
|
+
const draftDoc = JSON.parse(draftRaw);
|
|
761
|
+
for (const r of draftDoc.rows || []) {
|
|
762
|
+
if (r && typeof r === 'object' && typeof r.id === 'string') {
|
|
763
|
+
draftAddRows.push(r);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
newRows = [...storeRows, ...draftAddRows.map(proposedIf)];
|
|
767
|
+
} else {
|
|
768
|
+
// amend
|
|
769
|
+
const draftMap = new Map ();
|
|
770
|
+
const draftDoc = JSON.parse(draftRaw);
|
|
771
|
+
for (const r of draftDoc.rows || []) {
|
|
772
|
+
if (r && typeof r === 'object' && typeof r.id === 'string') {
|
|
773
|
+
draftMap.set(r.id, r);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
newRows = storeRows.map((r ) => {
|
|
777
|
+
if (r && typeof r === 'object' && draftMap.has(r.id)) {
|
|
778
|
+
// An amend never promotes: a live proposed row stays proposed whatever this call would otherwise land.
|
|
779
|
+
if (r.proposed === true) {
|
|
780
|
+
proposedIds.push(r.id);
|
|
781
|
+
return { ...draftMap.get(r.id), proposed: true };
|
|
782
|
+
}
|
|
783
|
+
return proposedIf(draftMap.get(r.id));
|
|
784
|
+
}
|
|
785
|
+
return r;
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const newStoreDoc = { ...parsedStore, rows: newRows };
|
|
790
|
+
const newStoreRaw = JSON.stringify(newStoreDoc, null, 2) + '\n';
|
|
791
|
+
|
|
792
|
+
let writes = 0;
|
|
793
|
+
if (newStoreRaw !== freshSeedRaw) {
|
|
794
|
+
const storeDir = path.dirname(storePath);
|
|
795
|
+
fsImpl.mkdirSync(storeDir, { recursive: true });
|
|
796
|
+
const tmpStorePath = path.join(storeDir, `.tmp-rows-${pid ?? process.pid}-${Date.now()}`);
|
|
797
|
+
try {
|
|
798
|
+
fsImpl.writeFileSync(tmpStorePath, newStoreRaw, 'utf8');
|
|
799
|
+
fsImpl.renameSync(tmpStorePath, storePath);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
try { fsImpl.unlinkSync(tmpStorePath); } catch { /* ignore temp cleanup failure */ }
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
writes += 1;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const { statuses } = deriveStatus(newRows, facts);
|
|
808
|
+
const newTableContent = render(newRows, statuses);
|
|
809
|
+
const tableResult = renderRoadmapFile({
|
|
810
|
+
roadmapPath: tablePath,
|
|
811
|
+
content: newTableContent,
|
|
812
|
+
fsImpl,
|
|
813
|
+
});
|
|
814
|
+
if (tableResult.written) {
|
|
815
|
+
writes += 1;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const result = {
|
|
819
|
+
schema: 1,
|
|
820
|
+
action: 'seed',
|
|
821
|
+
op: action,
|
|
822
|
+
ok: true,
|
|
823
|
+
draft: { path: absDraftPath, sha256: draftSha },
|
|
824
|
+
seed: { path: storePath, sha256: freshSeedSha },
|
|
825
|
+
rows: judgement.rows,
|
|
826
|
+
findings: [],
|
|
827
|
+
overlaps: judgement.overlaps,
|
|
828
|
+
alone: judgement.alone,
|
|
829
|
+
reason: null,
|
|
830
|
+
switch: null,
|
|
831
|
+
writes,
|
|
832
|
+
land: null,
|
|
833
|
+
proposed: proposedIds,
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
if (json) {
|
|
837
|
+
out(JSON.stringify(result));
|
|
838
|
+
} else {
|
|
839
|
+
printTextResult(result, out, err);
|
|
840
|
+
}
|
|
841
|
+
return 0;
|
|
842
|
+
} finally {
|
|
843
|
+
releaseLease(leaseDir, me, fsImpl);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
/** How long the seed prompt waits for a person before it counts as "no" (promote waits without limit). */
|
|
865
|
+
const SEED_PROMPT_TIMEOUT_MS = 60000;
|
|
866
|
+
const TTY_POLL_MS = 25;
|
|
867
|
+
|
|
868
|
+
function sleepMs(ms ) {
|
|
869
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Reads one answer line from the controlling terminal (`/dev/tty`), never from stdin, an argument or the environment.
|
|
881
|
+
* `answer` is null when the terminal cannot be opened or read, the line is too long, or the optional timeout passes
|
|
882
|
+
* (`timedOut`). A timed read opens the terminal non-blocking and polls, so a pty that stays open without an answer
|
|
883
|
+
* cannot hold the caller for ever.
|
|
884
|
+
*/
|
|
885
|
+
function readTtyLine(prompt , options = {}) {
|
|
886
|
+
const { timeoutMs = 0, now = Date.now, sleep = sleepMs } = options;
|
|
887
|
+
const timed = timeoutMs > 0;
|
|
888
|
+
let fd ;
|
|
889
|
+
try { fd = timed ? fs.openSync('/dev/tty', fs.constants.O_RDWR | fs.constants.O_NONBLOCK) : fs.openSync('/dev/tty', 'r+'); } catch { return { answer: null, timedOut: false }; }
|
|
890
|
+
const deadline = timed ? now() + timeoutMs : Infinity;
|
|
891
|
+
let timedOut = false;
|
|
892
|
+
// one byte; 0 at end of input, -1 when the timeout passed
|
|
893
|
+
const readByte = (buf ) => {
|
|
894
|
+
for (;;) {
|
|
895
|
+
try { return fs.readSync(fd, buf, 0, 1, null); }
|
|
896
|
+
catch (error ) {
|
|
897
|
+
if (!timed || (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK')) throw error;
|
|
898
|
+
if (now() >= deadline) { timedOut = true; return -1; }
|
|
899
|
+
sleep(TTY_POLL_MS);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
try {
|
|
904
|
+
fs.writeSync(fd, prompt);
|
|
905
|
+
const buf = Buffer.alloc(1);
|
|
906
|
+
let line = '';
|
|
907
|
+
let ended = false;
|
|
908
|
+
for (let i = 0; i < 64 && !ended; i += 1) {
|
|
909
|
+
const n = readByte(buf);
|
|
910
|
+
if (n < 0) return { answer: null, timedOut: true };
|
|
911
|
+
if (n === 0 || buf[0] === 10) ended = true;
|
|
912
|
+
else line += String.fromCharCode(buf[0]);
|
|
913
|
+
}
|
|
914
|
+
// drain what is left of the typed line (bounded), so it is not left in the terminal queue
|
|
915
|
+
const tooLong = !ended;
|
|
916
|
+
for (let i = 0; i < 4096 && !ended; i += 1) {
|
|
917
|
+
const n = readByte(buf);
|
|
918
|
+
if (n < 0) return { answer: null, timedOut: true };
|
|
919
|
+
if (n === 0 || buf[0] === 10) ended = true;
|
|
920
|
+
}
|
|
921
|
+
return { answer: tooLong ? null : line, timedOut: false };
|
|
922
|
+
} catch {
|
|
923
|
+
return { answer: null, timedOut };
|
|
924
|
+
} finally {
|
|
925
|
+
try { fs.closeSync(fd); } catch { /* ignore close failure */ }
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** The untimed reader `roadmap promote` uses: a person runs that verb and may take as long as they need. */
|
|
930
|
+
function readTtyAnswer(prompt ) {
|
|
931
|
+
return readTtyLine(prompt).answer;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* maint-thin: `roadmap promote <id...>`, person only. A model with a shell controls the environment, so neither an
|
|
936
|
+
* environment check nor the autopilot switch can tell a person. The verb requires an interactive terminal on stdin
|
|
937
|
+
* (refused `needs-person-tty`, nothing written), validates the named rows, then prints their ids and titles and asks
|
|
938
|
+
* `[y/N]` on the controlling terminal (`/dev/tty`): no `/dev/tty`, or an answer other than y/yes, is refused
|
|
939
|
+
* `needs-person-confirm` and nothing is written. No flag or environment variable skips the prompt, and the autopilot
|
|
940
|
+
* switch is not read. The prompt comes before the roadmap lease (Ctrl-C would otherwise leave the lease behind); under
|
|
941
|
+
* the lease the store is read again and must equal what was shown, then the verb drops the `proposed` mark of every
|
|
942
|
+
* named row, so autopilot may pick it. All or nothing: an unknown id, a row that is not proposed, or a duplicate id is a finding (reported before any
|
|
943
|
+
* prompt) and nothing is written. Exit 0 promoted, 1 refused or findings, 2 usage.
|
|
944
|
+
*/
|
|
945
|
+
function handlePromoteCommand(options = {}) {
|
|
946
|
+
const {
|
|
947
|
+
ids = [],
|
|
948
|
+
json = false,
|
|
949
|
+
root = resolveMainRoot(process.cwd()),
|
|
950
|
+
fsImpl = fs,
|
|
951
|
+
gitFacts,
|
|
952
|
+
alive,
|
|
953
|
+
pid,
|
|
954
|
+
host,
|
|
955
|
+
now,
|
|
956
|
+
out = (s ) => console.log(s),
|
|
957
|
+
err = (s ) => console.error(s),
|
|
958
|
+
isTTY = process.stdin.isTTY === true,
|
|
959
|
+
confirm = readTtyAnswer,
|
|
960
|
+
} = options;
|
|
961
|
+
const emit = (result , code , detail ) => {
|
|
962
|
+
if (json) out(JSON.stringify(result));
|
|
963
|
+
else {
|
|
964
|
+
const head = result.ok ? `roadmap promote: ok, ${result.promoted.length} promoted${result.promoted.length ? `: ${result.promoted.map(safeText).join(' ')}` : ''}`
|
|
965
|
+
: result.reason ? `roadmap promote: refused: ${safeText(result.reason)}` : `roadmap promote: ${result.findings.length} findings`;
|
|
966
|
+
(result.ok ? out : err)(head);
|
|
967
|
+
for (const f of result.findings || []) err(` [${safeText(f.rule)}] ${safeText(f.rowId)}: ${f.message}`);
|
|
968
|
+
if (detail) err(` ${detail}`);
|
|
969
|
+
}
|
|
970
|
+
return code;
|
|
971
|
+
};
|
|
972
|
+
if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === 'string' && /^[0-9]{3}([a-z]|-[A-Z])?$/.test(id))) {
|
|
973
|
+
if (json) out(JSON.stringify({ error: 'usage', detail: 'roadmap promote takes one or more row ids' }));
|
|
974
|
+
else err('doflow roadmap: error: roadmap promote takes one or more row ids (e.g. doflow roadmap promote 130 131)');
|
|
975
|
+
return 2;
|
|
976
|
+
}
|
|
977
|
+
const storePath = path.join(root, 'agent-docs', 'roadmap', 'rows.json');
|
|
978
|
+
const tablePath = path.join(root, 'agent-docs', 'roadmap', 'ROADMAP.md');
|
|
979
|
+
const base = { schema: 1, action: 'promote', ok: false, ids, promoted: [] , findings: [] , reason: null , writes: 0 };
|
|
980
|
+
|
|
981
|
+
if (isTTY !== true) {
|
|
982
|
+
return emit({ ...base, reason: 'needs-person-tty' }, 1,
|
|
983
|
+
'promote runs only from an interactive terminal: a person runs doflow roadmap promote in their own shell');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Read, validate and ask BEFORE taking the lease: Ctrl-C kills the process inside the terminal read, so a lease held
|
|
987
|
+
// across the prompt would be left behind. The lease is taken after the answer, and the store is read again under it.
|
|
988
|
+
let raw ;
|
|
989
|
+
try { raw = fsImpl.readFileSync(storePath, 'utf8'); }
|
|
990
|
+
catch (error ) { return emit({ ...base, findings: ids.map((id) => ({ rule: 'row-not-found', rowId: id, message: `no store at ${storePath}: ${safeText(error.message)}` })) }, 1); }
|
|
991
|
+
const storeFindings = seedFindings(raw);
|
|
992
|
+
if (storeFindings.length > 0) return emit({ ...base, findings: storeFindings }, 1);
|
|
993
|
+
const doc = JSON.parse(raw);
|
|
994
|
+
const findings = [];
|
|
995
|
+
const seen = new Set ();
|
|
996
|
+
for (const id of ids) {
|
|
997
|
+
const row = doc.rows.find((r ) => r.id === id);
|
|
998
|
+
if (seen.has(id)) findings.push({ rule: 'duplicate-id', rowId: id, message: `Row '${safeText(id)}' is named twice` });
|
|
999
|
+
else if (!row) findings.push({ rule: 'row-not-found', rowId: id, message: `Row '${safeText(id)}' not found in store` });
|
|
1000
|
+
else if (row.proposed !== true) findings.push({ rule: 'row-not-proposed', rowId: id, message: `Row '${safeText(id)}' is not proposed` });
|
|
1001
|
+
seen.add(id);
|
|
1002
|
+
}
|
|
1003
|
+
if (findings.length > 0) return emit({ ...base, findings }, 1);
|
|
1004
|
+
const titles = ids.map((id) => ` ${safeText(id)} ${safeText(doc.rows.find((r ) => r.id === id).title)}`);
|
|
1005
|
+
const prompt = `${titles.join('\n')}\nPromote ${ids.length} row(s): ${ids.map(safeText).join(' ')}? [y/N] `;
|
|
1006
|
+
let answer = null;
|
|
1007
|
+
try { answer = confirm(prompt); } catch { answer = null; }
|
|
1008
|
+
if (typeof answer !== 'string' || !/^(y|yes)$/i.test(answer.trim())) {
|
|
1009
|
+
return emit({ ...base, reason: 'needs-person-confirm' }, 1,
|
|
1010
|
+
'promote needs a person to answer y on the controlling terminal (/dev/tty); there is no flag or variable that skips the prompt');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const leaseDir = path.join(root, '.doflow', 'state', 'roadmap');
|
|
1014
|
+
const nowDate = (now ? now : () => new Date())();
|
|
1015
|
+
const me = {
|
|
1016
|
+
schema: 1,
|
|
1017
|
+
pid: pid ?? process.pid,
|
|
1018
|
+
start: new Date(Date.now() - (process ).uptime() * 1000).toISOString(),
|
|
1019
|
+
host: host ?? os.hostname(),
|
|
1020
|
+
runId: null,
|
|
1021
|
+
acquiredAt: nowDate.toISOString(),
|
|
1022
|
+
};
|
|
1023
|
+
const lease = acquireLease(leaseDir, me, alive || noPsAlive, fsImpl);
|
|
1024
|
+
if (!lease.ok) return emit({ ...base, reason: 'lease-held' }, 1);
|
|
1025
|
+
try {
|
|
1026
|
+
let current = null;
|
|
1027
|
+
try { current = fsImpl.readFileSync(storePath, 'utf8'); } catch { current = null; }
|
|
1028
|
+
if (current !== raw) {
|
|
1029
|
+
return emit({ ...base, reason: 'needs-person-confirm' }, 1,
|
|
1030
|
+
'the roadmap store changed after the rows were shown; nothing was written, run doflow roadmap promote again');
|
|
1031
|
+
}
|
|
1032
|
+
const rows = doc.rows.map((r ) => {
|
|
1033
|
+
if (!ids.includes(r.id)) return r;
|
|
1034
|
+
const { proposed, ...ready } = r;
|
|
1035
|
+
return ready;
|
|
1036
|
+
});
|
|
1037
|
+
const newRaw = JSON.stringify({ ...doc, rows }, null, 2) + '\n';
|
|
1038
|
+
let writes = 0;
|
|
1039
|
+
const tmp = path.join(path.dirname(storePath), `.tmp-rows-${pid ?? process.pid}-${Date.now()}`);
|
|
1040
|
+
try {
|
|
1041
|
+
fsImpl.writeFileSync(tmp, newRaw, 'utf8');
|
|
1042
|
+
fsImpl.renameSync(tmp, storePath);
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
try { fsImpl.unlinkSync(tmp); } catch { /* ignore temp cleanup failure */ }
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
writes += 1;
|
|
1048
|
+
const facts = gitFacts || gatherGitFacts({ root });
|
|
1049
|
+
if (!facts.unreadable || facts.unreadable.length === 0) {
|
|
1050
|
+
const { statuses } = deriveStatus(rows, facts);
|
|
1051
|
+
if (renderRoadmapFile({ roadmapPath: tablePath, content: render(rows, statuses), fsImpl }).written) writes += 1;
|
|
1052
|
+
}
|
|
1053
|
+
return emit({ ...base, ok: true, promoted: [...ids], writes }, 0);
|
|
1054
|
+
} finally {
|
|
1055
|
+
releaseLease(leaseDir, me, fsImpl);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
module.exports = {
|
|
1060
|
+
judgeDraft,
|
|
1061
|
+
handleSeedCommand,
|
|
1062
|
+
handlePromoteCommand,
|
|
1063
|
+
};
|