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