@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,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Optional pinned external provider. No imports, model loads or writes occur until invoked.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const zlib = require('node:zlib');
|
|
9
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
10
|
+
const { defaultIsAlive } = require('../../state/run-lock.js');
|
|
11
|
+
const SEMBLE_VERSION = '0.7.0';
|
|
12
|
+
const SEMBLE_LIMITS = Object.freeze({ files: 2000, fileBytes: 1048576, sourceBytes: 67108864,
|
|
13
|
+
chunks: 50000, artifactBytes: 268435456, modelBytes: 536870912, outputBytes: 2097152,
|
|
14
|
+
statsBytes: 65536, libraryBytes: 134217728, queryMs: 10000, lifecycleMs: 120000, changedFiles: 500, changedBytes: 16777216 });
|
|
15
|
+
const digest = (value ) => crypto.createHash('sha256').update(value).digest('hex');
|
|
16
|
+
function fault(reason , freshness = 'UNKNOWN', cause ) { return Object.assign(new Error(reason), { reason, freshness, ...(cause ? { cause } : {}) }); }
|
|
17
|
+
// Semble's own interpreter, found without a PATH wrapper: the `semble` launcher (PATH, then ~/.local/bin/semble) and the
|
|
18
|
+
// absolute interpreter its shebang names. The bridge then proves offline that this interpreter imports the pinned version.
|
|
19
|
+
// Ceiling: only a plain absolute shebang (uv/pipx layout); `env` indirection, arguments or a native launcher are unavailable.
|
|
20
|
+
function resolveSembleInterpreter({ env = process.env, home = os.homedir() } = {}) {
|
|
21
|
+
const dirs = String(env.PATH || '').split(path.delimiter).filter((dir ) => path.isAbsolute(dir));
|
|
22
|
+
const launcher = [...dirs.map((dir ) => path.join(dir, 'semble')), path.join(home, '.local', 'bin', 'semble')]
|
|
23
|
+
.find((file ) => { try { return fs.statSync(file).isFile(); } catch { return false; } });
|
|
24
|
+
if (!launcher) throw fault('provider-unavailable', 'UNKNOWN', 'no semble launcher on PATH or in ~/.local/bin');
|
|
25
|
+
const head = Buffer.alloc(1024); const fd = fs.openSync(launcher, 'r');
|
|
26
|
+
let count = 0; try { count = fs.readSync(fd, head, 0, head.length, 0); } finally { fs.closeSync(fd); }
|
|
27
|
+
const shebang = /^#!(\/\S+)[ \t]*\r?$/.exec(head.subarray(0, count).toString('utf8').split('\n')[0]);
|
|
28
|
+
if (!shebang || path.basename(shebang[1]) === 'env') throw fault('provider-unavailable', 'UNKNOWN', `${launcher} does not name an absolute interpreter`);
|
|
29
|
+
let usable = false; try { fs.accessSync(shebang[1], fs.constants.X_OK); usable = fs.statSync(shebang[1]).isFile(); } catch {}
|
|
30
|
+
if (!usable) throw fault('provider-unavailable', 'UNKNOWN', `interpreter ${shebang[1]} named by ${launcher} is not executable`);
|
|
31
|
+
return { python: shebang[1], launcher };
|
|
32
|
+
}
|
|
33
|
+
// Lifecycle preflight: the interpreter's installed Semble metadata must be the pin before any lock or mutation. No
|
|
34
|
+
// import, network or write happens here; the bridge re-checks the same pin under its audit policy on every call.
|
|
35
|
+
function checkSembleVersion(python ) {
|
|
36
|
+
const run = spawnSync(python, ['-c', 'import importlib.metadata as m, sys; sys.stdout.write(m.version("semble"))'], { cwd: path.dirname(python),
|
|
37
|
+
env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' }, encoding: 'utf8', timeout: 5000, maxBuffer: 4096 });
|
|
38
|
+
if (run.error || run.status !== 0) throw fault('provider-unavailable', 'UNKNOWN', `semble is not installed for ${python}`);
|
|
39
|
+
const found = String(run.stdout).trim().slice(0, 32);
|
|
40
|
+
if (found !== SEMBLE_VERSION) throw fault('provider-version-mismatch', 'UNKNOWN', `${python} has semble ${found}; the bridge pins ${SEMBLE_VERSION}`);
|
|
41
|
+
}
|
|
42
|
+
function semanticStatePaths(inventory ) {
|
|
43
|
+
const state = path.join(inventory.root, '.doflow', 'index', 'code', 'semble', digest(`${inventory.root}\n${inventory.scopeIgnoreDigest}`));
|
|
44
|
+
const mirror = path.join(state, 'mirror');
|
|
45
|
+
const cache = path.join(state, 'cache');
|
|
46
|
+
return { root: inventory.root, state, mirror, cache, index: path.join(cache, digest(mirror), 'index'),
|
|
47
|
+
manifest: path.join(state, 'manifest.json'), marker: path.join(state, 'in-progress.json'), lock: path.join(state, 'operation.lock') };
|
|
48
|
+
}
|
|
49
|
+
function inside(root , file ) { const rel = path.relative(root, file); return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel)); }
|
|
50
|
+
function safeDirectories(root , target , create = false) {
|
|
51
|
+
if (!inside(root, target)) throw fault('path-outside-state');
|
|
52
|
+
let cursor = root;
|
|
53
|
+
for (const segment of path.relative(root, target).split(path.sep).filter(Boolean)) {
|
|
54
|
+
cursor = path.join(cursor, segment);
|
|
55
|
+
try { const st = fs.lstatSync(cursor); if (!st.isDirectory() || st.isSymbolicLink()) throw fault('unsafe-state-directory'); }
|
|
56
|
+
catch (error ) { if (error.code !== 'ENOENT' || !create) throw error; fs.mkdirSync(cursor); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function regularBytes(file , max ) {
|
|
60
|
+
const before = fs.lstatSync(file);
|
|
61
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > max) throw fault('unsafe-or-oversized-file');
|
|
62
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
63
|
+
try {
|
|
64
|
+
const start = fs.fstatSync(fd);
|
|
65
|
+
if (start.ino !== before.ino || start.dev !== before.dev) throw fault('source-changed', 'STALE');
|
|
66
|
+
const buffer = Buffer.alloc(start.size + 1); const count = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
67
|
+
const after = fs.fstatSync(fd);
|
|
68
|
+
if (count !== start.size || after.size !== start.size || after.mtimeMs !== start.mtimeMs || after.ctimeMs !== start.ctimeMs) throw fault('source-changed', 'STALE');
|
|
69
|
+
return buffer.subarray(0, count);
|
|
70
|
+
} finally { fs.closeSync(fd); }
|
|
71
|
+
}
|
|
72
|
+
// `transient` (live provider sinks only) skips entries that vanish mid-walk; `linkRoots` (model only)
|
|
73
|
+
// admits a symlink whose realpath is a regular file inside one of those roots, counted at its target size.
|
|
74
|
+
function scanTree(root , maxBytes , maxFiles = 10000, { transient = false, linkRoots = [] } = {}) {
|
|
75
|
+
if (!fs.lstatSync(root).isDirectory() || fs.lstatSync(root).isSymbolicLink()) throw fault('unsafe-directory');
|
|
76
|
+
let bytes = 0; let entries = 0; const files = [];
|
|
77
|
+
const gone = (error ) => { if (!transient || error?.code !== 'ENOENT') throw error; };
|
|
78
|
+
const walk = (dir ) => {
|
|
79
|
+
let names = [];
|
|
80
|
+
try { names = fs.readdirSync(dir).sort(); } catch (error ) { gone(error); }
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
if (++entries > maxFiles * 4 + 32) throw fault('resource-limit');
|
|
83
|
+
const full = path.join(dir, name); let st ; let real ;
|
|
84
|
+
try { st = fs.lstatSync(full); } catch (error ) { gone(error); continue; }
|
|
85
|
+
if (st.isSymbolicLink()) {
|
|
86
|
+
if (!linkRoots.length) throw fault('unsafe-artifact');
|
|
87
|
+
try { real = fs.realpathSync(full); st = fs.lstatSync(real); } catch { throw fault('unsafe-artifact'); }
|
|
88
|
+
if (!st.isFile() || !linkRoots.some((r ) => real !== r && inside(r, real ))) throw fault('unsafe-artifact');
|
|
89
|
+
}
|
|
90
|
+
if (!st.isFile() && !st.isDirectory()) throw fault('unsafe-artifact');
|
|
91
|
+
if (st.isDirectory()) walk(full);
|
|
92
|
+
else { bytes += st.size; files.push({ path: path.relative(root, full).split(path.sep).join('/'), sizeBytes: st.size, ...(real ? { real } : {}) });
|
|
93
|
+
if (bytes > maxBytes || files.length > maxFiles) throw fault('resource-limit'); }
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
walk(root); return { bytes, files };
|
|
97
|
+
}
|
|
98
|
+
// Hugging Face snapshot layout only: <hub>/models--<id>/snapshots/<rev>/* link into content-addressed blobs.
|
|
99
|
+
function modelLinkRoots(modelPath ) {
|
|
100
|
+
const snapshots = path.dirname(modelPath); const repo = path.dirname(snapshots);
|
|
101
|
+
if (path.basename(snapshots) !== 'snapshots' || !/^models--[^/\\]+$/.test(path.basename(repo))) return [];
|
|
102
|
+
return [path.join(repo, 'blobs'), path.join(path.dirname(repo), 'blobs')].filter((dir ) => {
|
|
103
|
+
try { return fs.lstatSync(dir).isDirectory() && fs.realpathSync(dir) === dir; } catch { return false; }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function inspectModel(modelPath , limits = SEMBLE_LIMITS) {
|
|
107
|
+
if (typeof modelPath !== 'string' || !path.isAbsolute(modelPath) || fs.realpathSync(modelPath) !== modelPath) throw fault('local-model-required');
|
|
108
|
+
const inventory = scanTree(modelPath, limits.modelBytes, 10000, { linkRoots: modelLinkRoots(modelPath) });
|
|
109
|
+
for (const required of ['config.json', 'tokenizer.json', 'model.safetensors']) if (!inventory.files.some((f ) => f.path === required)) throw fault('local-model-incomplete');
|
|
110
|
+
const hashes = inventory.files.map((f ) => [f.path, digest(regularBytes(f.real || path.join(modelPath, f.path), limits.modelBytes))]);
|
|
111
|
+
return { path: modelPath, identity: digest(JSON.stringify(hashes)), bytes: inventory.bytes };
|
|
112
|
+
}
|
|
113
|
+
function validateInventory(inventory , limits ) {
|
|
114
|
+
if (!inventory?.complete || !path.isAbsolute(inventory.root) || fs.realpathSync(inventory.root) !== inventory.root || !inventory.scopeIgnoreDigest || !inventory.identity) throw fault('incomplete-corpus');
|
|
115
|
+
if (!Array.isArray(inventory.files) || inventory.files.length > limits.files) throw fault('corpus-limit');
|
|
116
|
+
const seen = new Set(); let bytes = 0;
|
|
117
|
+
for (const entry of inventory.files) {
|
|
118
|
+
if (typeof entry.path !== 'string' || entry.path.includes('\\') || entry.path.split('/').some((p ) => !p || p === '.' || p === '..') || path.isAbsolute(entry.path) || seen.has(entry.path)) throw fault('unsafe-corpus-path');
|
|
119
|
+
seen.add(entry.path);
|
|
120
|
+
if (!/^[0-9a-f]{64}$/.test(entry.fileHash) || entry.sizeBytes > limits.fileBytes || entry.sizeBytes < 0) throw fault('corpus-hash-or-size');
|
|
121
|
+
bytes += entry.sizeBytes;
|
|
122
|
+
}
|
|
123
|
+
if (bytes > limits.sourceBytes) throw fault('corpus-limit');
|
|
124
|
+
}
|
|
125
|
+
function readSource(inventory , entry , limits ) {
|
|
126
|
+
const absolute = path.join(inventory.root, entry.path);
|
|
127
|
+
if (entry.absolutePath !== absolute || fs.realpathSync(absolute) !== absolute) throw fault('unsafe-source-path');
|
|
128
|
+
const data = regularBytes(absolute, limits.fileBytes);
|
|
129
|
+
if (data.length !== entry.sizeBytes || digest(data) !== entry.fileHash) throw fault('source-changed', 'STALE');
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
function checkSources(inventory , limits ) {
|
|
133
|
+
for (const entry of inventory.files) readSource(inventory, entry, limits);
|
|
134
|
+
// Re-admission detects changed ignore files, scope policy and added/deleted members too.
|
|
135
|
+
const { fingerprintCorpus } = require('./code-corpus.js');
|
|
136
|
+
const next = fingerprintCorpus(inventory);
|
|
137
|
+
if (!next.complete || next.identity !== inventory.identity || next.scopeIgnoreDigest !== inventory.scopeIgnoreDigest) throw fault('corpus-changed', 'STALE');
|
|
138
|
+
}
|
|
139
|
+
// Semble 0.7.0 saves BM25 as `bm25_index/index.npz`: numpy's stored (uncompressed) zip of six 1-D .npy members.
|
|
140
|
+
// Parse it here without NumPy so a garbled, compressed, oversized or inconsistent archive is refused before the loader.
|
|
141
|
+
const BM25_MEMBERS = { 'doc_order.npy': '|u1', 'terms.npy': '|u1', 'offsets.npy': '<i8', 'docs.npy': '<i4', 'tfs.npy': '<i4', 'doc_lengths.npy': '<i4' };
|
|
142
|
+
function npzMembers(buffer ) {
|
|
143
|
+
const bad = () => fault('invalid-bm25-archive');
|
|
144
|
+
const end = buffer.length - 22; // numpy writes no archive comment, so the end record closes the file
|
|
145
|
+
if (end < 0 || buffer.readUInt32LE(end) !== 0x06054b50) throw bad();
|
|
146
|
+
const count = buffer.readUInt16LE(end + 10); const size = buffer.readUInt32LE(end + 12); const directory = buffer.readUInt32LE(end + 16); let cursor = directory;
|
|
147
|
+
if (buffer.readUInt16LE(end + 4) !== 0 || buffer.readUInt16LE(end + 8) !== count || count !== Object.keys(BM25_MEMBERS).length || directory + size !== end) throw bad();
|
|
148
|
+
const members = new Map (); let dataEnd = 0;
|
|
149
|
+
for (let i = 0; i < count; i++) {
|
|
150
|
+
if (cursor + 46 > end || buffer.readUInt32LE(cursor) !== 0x02014b50) throw bad();
|
|
151
|
+
const flags = buffer.readUInt16LE(cursor + 8); const method = buffer.readUInt16LE(cursor + 10); const crc = buffer.readUInt32LE(cursor + 16);
|
|
152
|
+
const stored = buffer.readUInt32LE(cursor + 20); const length = buffer.readUInt32LE(cursor + 24); const local = buffer.readUInt32LE(cursor + 42);
|
|
153
|
+
const nameLength = buffer.readUInt16LE(cursor + 28); const next = cursor + 46 + nameLength + buffer.readUInt16LE(cursor + 30) + buffer.readUInt16LE(cursor + 32);
|
|
154
|
+
const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString('latin1');
|
|
155
|
+
// Stored only: a member can never claim more bytes than the archive holds (no decompression bomb).
|
|
156
|
+
if (next > end || (flags & 1) || method !== 0 || stored !== length || !(name in BM25_MEMBERS) || members.has(name) || local < dataEnd || local + 30 + nameLength > directory) throw bad();
|
|
157
|
+
if (buffer.readUInt32LE(local) !== 0x04034b50 || buffer.readUInt16LE(local + 8) !== 0 || buffer.readUInt16LE(local + 26) !== nameLength
|
|
158
|
+
|| buffer.subarray(local + 30, local + 30 + nameLength).toString('latin1') !== name) throw bad();
|
|
159
|
+
const start = local + 30 + nameLength + buffer.readUInt16LE(local + 28); dataEnd = start + length;
|
|
160
|
+
if (dataEnd > directory) throw bad();
|
|
161
|
+
const data = buffer.subarray(start, dataEnd);
|
|
162
|
+
if (zlib.crc32(data) !== crc) throw bad();
|
|
163
|
+
members.set(name, data); cursor = next;
|
|
164
|
+
}
|
|
165
|
+
if (cursor !== end) throw bad();
|
|
166
|
+
return members;
|
|
167
|
+
}
|
|
168
|
+
function npyVector(data , descr ) {
|
|
169
|
+
const bad = () => fault('invalid-bm25-archive');
|
|
170
|
+
if (data.length < 10 || data.subarray(0, 6).toString('hex') !== '934e554d5059' || ![1, 2, 3].includes(data[6]) || data[7] !== 0) throw bad();
|
|
171
|
+
const start = data[6] === 1 ? 10 : 12; if (data.length < start) throw bad();
|
|
172
|
+
const headerLength = data[6] === 1 ? data.readUInt16LE(8) : data.readUInt32LE(8);
|
|
173
|
+
const header = data.subarray(start, start + headerLength).toString('latin1');
|
|
174
|
+
const shape = /['"]shape['"]\s*:\s*\(\s*(\d+)\s*,\s*\)/.exec(header); const width = +descr.slice(2);
|
|
175
|
+
if (start + headerLength > data.length || !header.includes(`'descr': '${descr}'`) || !/['"]fortran_order['"]\s*:\s*False/.test(header) || !shape
|
|
176
|
+
|| data.length !== start + headerLength + (+shape[1] * width)) throw bad();
|
|
177
|
+
const body = data.subarray(start + headerLength); const length = +shape[1];
|
|
178
|
+
return { length, body, at: (i ) => (width === 8 ? Number(body.readBigInt64LE(i * 8)) : width === 4 ? body.readInt32LE(i * 4) : body[i]) };
|
|
179
|
+
}
|
|
180
|
+
function unpackStrings(vector ) {
|
|
181
|
+
const text = vector.body.toString('utf8');
|
|
182
|
+
if (vector.length && !text.endsWith('\0')) throw fault('invalid-bm25-archive');
|
|
183
|
+
return vector.length ? text.slice(0, -1).split('\0') : [];
|
|
184
|
+
}
|
|
185
|
+
// Mirrors Semble 0.7.0 BM25 `_check_consistent` and adds the document count; returns doc_order for the chunk-id cross-check.
|
|
186
|
+
function inspectBm25(buffer ) {
|
|
187
|
+
const members = npzMembers(buffer);
|
|
188
|
+
const v = Object.fromEntries(Object.entries(BM25_MEMBERS).map(([name, descr]) => [name.slice(0, -4), npyVector(members.get(name) , descr)]));
|
|
189
|
+
const docOrder = unpackStrings(v.doc_order); const terms = unpackStrings(v.terms); const n = docOrder.length;
|
|
190
|
+
let ok = new Set(docOrder).size === n && v.doc_lengths.length === n && v.offsets.length === terms.length + 1 && v.offsets.at(0) === 0
|
|
191
|
+
&& v.offsets.at(v.offsets.length - 1) === v.docs.length && v.tfs.length === v.docs.length;
|
|
192
|
+
for (let i = 1; ok && i < v.offsets.length; i++) ok = v.offsets.at(i) >= v.offsets.at(i - 1);
|
|
193
|
+
for (let i = 0; ok && i < v.docs.length; i++) ok = v.docs.at(i) >= 0 && v.docs.at(i) < n;
|
|
194
|
+
if (!ok) throw fault('inconsistent-bm25-postings');
|
|
195
|
+
return docOrder;
|
|
196
|
+
}
|
|
197
|
+
function inspectArtifacts(index , inventory , paths , model , limits ) {
|
|
198
|
+
safeDirectories(paths.state, index); safeDirectories(paths.state, paths.mirror);
|
|
199
|
+
const tree = scanTree(index, limits.artifactBytes, 32);
|
|
200
|
+
const expected = ['bm25_index/index.npz', 'chunks.json', 'metadata.json', 'semantic_index/arguments.json', 'semantic_index/vectors.npy'];
|
|
201
|
+
if (JSON.stringify(tree.files.map((f ) => f.path).sort()) !== JSON.stringify(expected.sort())) throw fault('incomplete-index');
|
|
202
|
+
const json = (name ) => JSON.parse(regularBytes(path.join(index, name), limits.artifactBytes).toString('utf8'));
|
|
203
|
+
const metadata = json('metadata.json'); const chunks = json('chunks.json');
|
|
204
|
+
if (metadata.root_path !== paths.mirror || metadata.model_path !== model.path || metadata.cache_version !== 2 || metadata.chunk_size !== 750 || JSON.stringify(metadata.content_type) !== '["code"]') throw fault('incompatible-index');
|
|
205
|
+
if (!Array.isArray(chunks) || chunks.length > limits.chunks) throw fault('invalid-chunk-pool');
|
|
206
|
+
const docOrder = inspectBm25(regularBytes(path.join(index, 'bm25_index/index.npz'), limits.artifactBytes));
|
|
207
|
+
if (docOrder.length !== chunks.length) throw fault('invalid-chunk-pool');
|
|
208
|
+
// Check NPY header without loading NumPy; reject shape/size bombs before its allocator runs.
|
|
209
|
+
const vectors = regularBytes(path.join(index, 'semantic_index/vectors.npy'), limits.artifactBytes);
|
|
210
|
+
if (vectors.length < 10 || vectors.subarray(0, 6).toString('hex') !== '934e554d5059' || vectors[6] !== 1 || vectors[7] !== 0) throw fault('invalid-vector-header');
|
|
211
|
+
const headerLength = vectors.readUInt16LE(8); const header = vectors.subarray(10, 10 + headerLength).toString('ascii');
|
|
212
|
+
const shape = /['"]shape['"]\s*:\s*\(\s*(\d+)\s*,\s*(\d+)\s*,?\s*\)/.exec(header);
|
|
213
|
+
const dtype = /['"]descr['"]\s*:\s*['"]([<>=|][fiu])(\d+)['"]/.exec(header);
|
|
214
|
+
if (!shape || !dtype || +shape[1] !== chunks.length || +shape[2] < 1 || +shape[2] > 65536 || ![1, 2, 4, 8].includes(+dtype[2]) || vectors.length !== 10 + headerLength + (+shape[1] * +shape[2] * +dtype[2])) throw fault('inconsistent-vector-shape');
|
|
215
|
+
const admitted = new Map(inventory.files.map((e ) => [e.path, e]));
|
|
216
|
+
let offset = 0; const ids = [];
|
|
217
|
+
for (const [file, value] of Object.entries(metadata.files || {}) ) {
|
|
218
|
+
if (!admitted.has(file) || !Number.isSafeInteger(value.start) || !Number.isSafeInteger(value.count) || value.start !== offset || value.count < 0 || !Number.isInteger(value.mtime_ns)) throw fault('invalid-index-manifest');
|
|
219
|
+
for (let slot = 0; slot < value.count; slot++) {
|
|
220
|
+
const chunk = chunks[offset + slot];
|
|
221
|
+
if (!chunk || chunk.file_path !== file || !Number.isSafeInteger(chunk.start_line) || !Number.isSafeInteger(chunk.end_line) || chunk.start_line < 1 || chunk.end_line < chunk.start_line) throw fault('unsafe-chunk-path-or-range');
|
|
222
|
+
ids.push(`${file}:${slot}`);
|
|
223
|
+
}
|
|
224
|
+
offset += value.count;
|
|
225
|
+
}
|
|
226
|
+
if (offset !== chunks.length || JSON.stringify(ids) !== JSON.stringify(docOrder)) throw fault('inconsistent-artifacts');
|
|
227
|
+
// All metadata paths are admitted; constructor source reads can never escape this copied mirror.
|
|
228
|
+
const mirror = scanTree(paths.mirror, limits.sourceBytes, limits.files);
|
|
229
|
+
if (JSON.stringify(mirror.files.map((f ) => f.path).sort()) !== JSON.stringify([...admitted.keys()].sort())) throw fault('mirror-membership-mismatch');
|
|
230
|
+
for (const [file, entry] of admitted ) if (digest(regularBytes(path.join(paths.mirror, file), limits.fileBytes)) !== entry.fileHash) throw fault('mirror-hash-mismatch', 'STALE');
|
|
231
|
+
return { chunks: chunks.length, artifactBytes: tree.bytes, artifactIdentity: digest(JSON.stringify(tree.files.map((f ) => [f.path, digest(regularBytes(path.join(index, f.path), limits.artifactBytes))]))) };
|
|
232
|
+
}
|
|
233
|
+
// The semantic lock records {pid, hostname, token}. A lock whose owner pid is dead on this host (run-lock.ts's liveness
|
|
234
|
+
// rule: process.kill(pid, 0)) is reclaimed under a claim file named for its token, created `wx`: the claim winner re-reads
|
|
235
|
+
// the lock and removes it only while it still carries the judged token, so a live or newer lock is never removed. A lock
|
|
236
|
+
// from another host, from this process, or without a readable record is never reclaimed.
|
|
237
|
+
// Ceiling: a reclaimer killed between creating and removing its claim leaves `operation.lock.claim.<token>`, and that
|
|
238
|
+
// lock then waits for a manual removal, which the contention cause names.
|
|
239
|
+
const LOCK_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
240
|
+
function readLockOwner(lock ) {
|
|
241
|
+
try {
|
|
242
|
+
const owner = JSON.parse(regularBytes(lock, 4096).toString('utf8'));
|
|
243
|
+
return owner && Number.isInteger(owner.pid) && typeof owner.hostname === 'string' && LOCK_TOKEN.test(owner.token) ? owner : null;
|
|
244
|
+
} catch { return null; }
|
|
245
|
+
}
|
|
246
|
+
function reclaimStaleLock(lock , owner , hostname ) {
|
|
247
|
+
const claim = `${lock}.claim.${owner.token}`;
|
|
248
|
+
try { fs.writeFileSync(claim, `${JSON.stringify({ pid: process.pid, hostname })}\n`, { flag: 'wx', mode: 0o600 }); }
|
|
249
|
+
catch (error ) { if (error.code === 'EEXIST') return false; throw error; }
|
|
250
|
+
try {
|
|
251
|
+
if (readLockOwner(lock)?.token !== owner.token) return false;
|
|
252
|
+
fs.unlinkSync(lock); return true;
|
|
253
|
+
} finally { try { fs.unlinkSync(claim); } catch {} }
|
|
254
|
+
}
|
|
255
|
+
async function acquireSemanticLock(paths , { isAlive = defaultIsAlive, hostname = os.hostname(), waitMs = 100 } = {}) {
|
|
256
|
+
safeDirectories(paths.root, paths.state, true);
|
|
257
|
+
const token = crypto.randomUUID();
|
|
258
|
+
const deadline = Date.now() + waitMs;
|
|
259
|
+
while (true) {
|
|
260
|
+
try {
|
|
261
|
+
const fd = fs.openSync(paths.lock, 'wx', 0o600); fs.writeFileSync(fd, `${JSON.stringify({ pid: process.pid, hostname, token })}\n`);
|
|
262
|
+
return () => { fs.closeSync(fd); if (readLockOwner(paths.lock)?.token === token) fs.unlinkSync(paths.lock); };
|
|
263
|
+
} catch (error ) { if (error.code !== 'EEXIST') throw error; }
|
|
264
|
+
const owner = readLockOwner(paths.lock);
|
|
265
|
+
if (owner && owner.hostname === hostname && owner.pid !== process.pid && !isAlive(owner.pid) && reclaimStaleLock(paths.lock, owner, hostname)) continue;
|
|
266
|
+
if (Date.now() >= deadline) {
|
|
267
|
+
throw fault('lock-contention', 'UNKNOWN', `another semantic index operation holds ${paths.lock}${owner ? ` (pid ${owner.pid} on ${owner.hostname})` : ''}; `
|
|
268
|
+
+ 'run the command again when it finishes. A lock whose process has exited on this host is reclaimed automatically; '
|
|
269
|
+
+ `otherwise remove ${paths.lock} (and any ${path.basename(paths.lock)}.claim.* beside it) only after confirming no semantic build, refresh or query is running.`);
|
|
270
|
+
}
|
|
271
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function cleanupTemporary(paths ) {
|
|
275
|
+
for (const name of fs.readdirSync(paths.state)) {
|
|
276
|
+
if (!/^(query|staging)-[A-Za-z0-9-]+$/.test(name)) continue;
|
|
277
|
+
const dir = path.join(paths.state, name);
|
|
278
|
+
scanTree(dir, SEMBLE_LIMITS.artifactBytes, 10000); fs.rmSync(dir, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Per-invocation disposable sandbox: `stats/` is Semble's statistics sink (query SEMBLE_CACHE_LOCATION, 64 KiB);
|
|
282
|
+
// `library/` takes every redirected library cache/temp location. Semble 0.7.0 (semble-grammars 0.1.2) extracts sha-verified tree-sitter
|
|
283
|
+
// grammars from its own bundled archive there (77 libraries, 79.6 MB uncompressed on macos-arm64), so it is
|
|
284
|
+
// bounded by `libraryBytes` instead of the statistics cap.
|
|
285
|
+
function bridgeSandbox(sandbox ) {
|
|
286
|
+
const temp = path.join(sandbox, 'stats'); const library = path.join(sandbox, 'library');
|
|
287
|
+
for (const dir of [temp, library]) if (!fs.existsSync(dir)) fs.mkdirSync(dir);
|
|
288
|
+
return { temp, library };
|
|
289
|
+
}
|
|
290
|
+
function checkSandbox(sinks , limits , transient = false) {
|
|
291
|
+
scanTree(sinks.temp, limits.statsBytes, 128, { transient });
|
|
292
|
+
scanTree(sinks.library, limits.libraryBytes, 512, { transient });
|
|
293
|
+
}
|
|
294
|
+
function executeBridge(request , options , paths , sandbox ) {
|
|
295
|
+
const limits = options.limits; const deadline = request.action === 'query' ? limits.queryMs : limits.lifecycleMs;
|
|
296
|
+
const sinks = bridgeSandbox(sandbox); const { temp, library } = sinks;
|
|
297
|
+
return new Promise(resolve => {
|
|
298
|
+
const env = { ...process.env, PYTHONDONTWRITEBYTECODE: '1', PYTHONHASHSEED: '0', OMP_NUM_THREADS: '1', OPENBLAS_NUM_THREADS: '1', MKL_NUM_THREADS: '1', HF_HUB_OFFLINE: '1', TRANSFORMERS_OFFLINE: '1', HF_DATASETS_OFFLINE: '1',
|
|
299
|
+
SEMBLE_CACHE_LOCATION: request.action === 'query' ? temp : paths.cache, HF_HOME: library, HUGGINGFACE_HUB_CACHE: library,
|
|
300
|
+
TRANSFORMERS_CACHE: library, XDG_CACHE_HOME: library, SEMBLE_GRAMMARS_CACHE_DIR: path.join(library, 'grammars'), TORCH_HOME: library,
|
|
301
|
+
TMPDIR: library, TEMP: library, TMP: library, TOKENIZERS_PARALLELISM: 'false', SEMBLE_INDEX_WORKERS: '0' };
|
|
302
|
+
const child = spawn(options.python, [options.bridgePath || path.join(__dirname, 'semble-bridge.py')], { cwd: paths.state, env, stdio: ['pipe', 'pipe', 'pipe'], shell: false });
|
|
303
|
+
let stdout = ''; let outputBytes = 0; let failure = null; let done = false;
|
|
304
|
+
const stop = (reason ) => { if (!failure) failure = reason; child.kill('SIGKILL'); };
|
|
305
|
+
const timer = setTimeout(() => stop('provider-timeout'), deadline);
|
|
306
|
+
// A live child creates and removes files while we walk; only a real overrun is a resource limit.
|
|
307
|
+
const sandboxFault = (error ) => error?.reason === 'resource-limit' ? 'provider-resource-limit' : 'provider-unsafe-write';
|
|
308
|
+
const poll = setInterval(() => { try { checkSandbox(sinks, limits, true); if (request.savePath && fs.existsSync(request.savePath)) scanTree(request.savePath, limits.artifactBytes, 32, { transient: true }); } catch (error ) { stop(sandboxFault(error)); } }, 10);
|
|
309
|
+
const finish = (result ) => { if (done) return; done = true; clearTimeout(timer); clearInterval(poll); resolve(result); };
|
|
310
|
+
child.stdout.on('data', (chunk ) => { outputBytes += chunk.length; if (outputBytes > limits.outputBytes) stop('provider-output-limit'); else stdout += chunk.toString('utf8'); });
|
|
311
|
+
child.stderr.on('data', (chunk ) => { outputBytes += chunk.length; if (outputBytes > limits.outputBytes) stop('provider-output-limit'); });
|
|
312
|
+
child.on('error', () => finish({ ok: false, reason: 'provider-unavailable' }));
|
|
313
|
+
child.on('close', (code ) => {
|
|
314
|
+
if (failure) return finish({ ok: false, reason: failure });
|
|
315
|
+
try { checkSandbox(sinks, limits); } catch (error ) { return finish({ ok: false, reason: sandboxFault(error) }); }
|
|
316
|
+
if (code !== 0) return finish({ ok: false, reason: 'provider-error' });
|
|
317
|
+
try { const result = JSON.parse(stdout); if (result.version !== SEMBLE_VERSION || typeof result.ok !== 'boolean') throw Error(); finish(result); }
|
|
318
|
+
catch { finish({ ok: false, reason: 'invalid-provider-output' }); }
|
|
319
|
+
});
|
|
320
|
+
child.stdin.on('error', () => {}); child.stdin.end(JSON.stringify(request));
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function readCompleted(paths , inventory , model , limits , allowChanged = false) {
|
|
324
|
+
if (fs.existsSync(paths.marker)) throw fault('index-in-progress');
|
|
325
|
+
let manifest ;
|
|
326
|
+
try { manifest = JSON.parse(regularBytes(paths.manifest, 2097152).toString('utf8')); } catch { throw fault('index-missing-or-corrupt'); }
|
|
327
|
+
if (manifest.schemaVersion !== 1 || manifest.complete !== true || !manifest.generation || !manifest.artifactIdentity) throw fault('unknown-index-identity');
|
|
328
|
+
if (manifest.root !== inventory.root || manifest.scopeIgnoreDigest !== inventory.scopeIgnoreDigest || manifest.providerVersion !== SEMBLE_VERSION || manifest.model?.identity !== model.identity || manifest.model?.path !== model.path) throw fault('index-identity-mismatch', 'STALE');
|
|
329
|
+
if (!allowChanged && manifest.corpusIdentity !== inventory.identity) throw fault('index-source-mismatch', 'STALE');
|
|
330
|
+
return manifest;
|
|
331
|
+
}
|
|
332
|
+
async function runSembleQuery(options ) {
|
|
333
|
+
const limits = { ...SEMBLE_LIMITS, ...options.limits }; const inventory = options.inventory;
|
|
334
|
+
let release ; let temp ;
|
|
335
|
+
try {
|
|
336
|
+
validateInventory(inventory, limits); const model = inspectModel(options.modelPath, limits); const paths = semanticStatePaths(inventory);
|
|
337
|
+
const python = options.python || resolveSembleInterpreter().python;
|
|
338
|
+
release = await acquireSemanticLock(paths); cleanupTemporary(paths);
|
|
339
|
+
const manifest = readCompleted(paths, inventory, model, limits);
|
|
340
|
+
checkSources(inventory, limits);
|
|
341
|
+
const artifacts = inspectArtifacts(paths.index, inventory, paths, model, limits);
|
|
342
|
+
if (artifacts.artifactIdentity !== manifest.artifactIdentity) throw fault('index-artifacts-changed');
|
|
343
|
+
const queryTemp = fs.mkdtempSync(path.join(paths.state, 'query-')); temp = queryTemp;
|
|
344
|
+
const result = await executeBridge({ action: 'query', query: options.query, indexPath: paths.index, modelPath: model.path,
|
|
345
|
+
poolSize: artifacts.chunks, allowedWriteRoots: [queryTemp] }, { ...options, python, limits }, paths, queryTemp);
|
|
346
|
+
if (!result.ok) throw fault(result.reason || 'provider-error', 'UNKNOWN', typeof result.cause === 'string' ? result.cause : undefined);
|
|
347
|
+
if (!result.runtimeIdentity || JSON.stringify(result.runtimeIdentity) !== JSON.stringify(manifest.runtimeIdentity)) throw fault('provider-runtime-identity-mismatch', 'STALE');
|
|
348
|
+
if (!Array.isArray(result.candidates) || result.candidates.length > 200) throw fault('invalid-provider-output');
|
|
349
|
+
const admitted = new Map(inventory.files.map((f ) => [f.path, f]));
|
|
350
|
+
for (const candidate of result.candidates) {
|
|
351
|
+
const loc = candidate.locator;
|
|
352
|
+
if (!loc || !admitted.has(loc.file) || !Number.isSafeInteger(loc.line) || !Number.isSafeInteger(loc.endLine) || loc.line < 1 || loc.endLine < loc.line || !Number.isFinite(candidate.score)) throw fault('invalid-provider-candidate');
|
|
353
|
+
candidate.indexedSourceIdentity = (admitted.get(loc.file) ).fileHash;
|
|
354
|
+
}
|
|
355
|
+
checkSources(inventory, limits);
|
|
356
|
+
if (inspectModel(options.modelPath, limits).identity !== model.identity) throw fault('model-changed', 'STALE');
|
|
357
|
+
// sourceReadBytes is the admitted corpus size; Semble 0.7.0's loader only stats sources, so it over-counts provider reads.
|
|
358
|
+
return { ok: true, provider: 'semble.search', version: SEMBLE_VERSION, model, generation: manifest.generation, freshness: 'FRESH', candidates: result.candidates,
|
|
359
|
+
accounting: { indexedChunks: artifacts.chunks, sourceReadBytes: inventory.files.reduce((n , f ) => n + f.sizeBytes, 0), artifactBytes: artifacts.artifactBytes } };
|
|
360
|
+
} catch (error ) { return { ok: false, provider: 'semble.search', version: SEMBLE_VERSION, freshness: error.freshness || 'UNKNOWN', reason: error.reason || 'provider-unavailable',
|
|
361
|
+
...(typeof error.cause === 'string' ? { cause: error.cause } : {}), candidates: [] }; }
|
|
362
|
+
finally { try { if (temp) fs.rmSync(temp, { recursive: true, force: true }); } catch {} try { if (release) release(); } catch {} }
|
|
363
|
+
}
|
|
364
|
+
module.exports = { SEMBLE_VERSION, SEMBLE_LIMITS, runSembleQuery, resolveSembleInterpreter, checkSembleVersion, semanticStatePaths, digest, fault, inside, safeDirectories,
|
|
365
|
+
regularBytes, scanTree, inspectModel, inspectBm25, validateInventory, readSource, checkSources, inspectArtifacts, acquireSemanticLock, cleanupTemporary, executeBridge, readCompleted, modelLinkRoots };
|
|
@@ -15,7 +15,8 @@ const followup = require('./followup.js');
|
|
|
15
15
|
const { StoreUnsafeError, StoreLockedError } = require('./event-store.js');
|
|
16
16
|
const { prepareStore } = require('./store-upkeep.js');
|
|
17
17
|
const { printSafe } = require('../mask.js');
|
|
18
|
-
const { buildOverview, initFeature, featureStatus } = require('./overview.js');
|
|
18
|
+
const { buildOverview, initFeature, featureStatus, activeFeature } = require('./overview.js');
|
|
19
|
+
const { recordFeature, clearSlug } = require('../feature-pointer.js');
|
|
19
20
|
const { releaseFeatures, recordMerged } = require('./release.js');
|
|
20
21
|
const goal = require('./goal.js');
|
|
21
22
|
|
|
@@ -92,7 +93,9 @@ function releaseLines(r ) {
|
|
|
92
93
|
function lifecycleLines(result ) {
|
|
93
94
|
switch (result.action) {
|
|
94
95
|
case 'init': return [`${result.slug}: tracked ${result.tracked}${result.taken.length ? `, took ${result.taken.join(', ')}` : ''}${result.goal ? `, serves ${result.goal}` : ''}`, ...result.next];
|
|
95
|
-
case 'status':
|
|
96
|
+
case 'status':
|
|
97
|
+
if (result.active) return [result.slug && result.status ? `${result.slug}: ${result.status}${result.evidence ? ` (${result.evidence.kind}${result.evidence.ref ? ` ${result.evidence.ref}` : ''})` : ''}${result.release ? `, release ${result.release}` : ''}` : result.message, ...(result.active.state === 'agree' ? [`pointer agrees with branch ${result.active.branch}${result.active.artifactDir ? `, artifacts in ${result.active.artifactDir}` : ''}`] : [])];
|
|
98
|
+
return [`${result.slug}: ${result.status}${result.evidence ? ` (${result.evidence.kind}${result.evidence.ref ? ` ${result.evidence.ref}` : ''})` : ''}${result.release ? `, release ${result.release}` : ''}`, ...(result.note ? [`note: ${result.note}`] : [])];
|
|
96
99
|
case 'release': return releaseLines(result);
|
|
97
100
|
case 'merged': return [`${result.slug}: merge confirmed, now ${result.status}`];
|
|
98
101
|
default: return result.mode ? overviewLines(result) : [];
|
|
@@ -233,10 +236,17 @@ function handleLifecycleCommand({ action, cwd, global = false, slug = null, json
|
|
|
233
236
|
if (refusedStore) return emit(refusedStore, json, lifecycleLines);
|
|
234
237
|
let result;
|
|
235
238
|
if (act === 'overview') result = buildOverview({ root, maintain: Boolean(flags.maintain), since: flags.since });
|
|
236
|
-
else if (act === 'init')
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
else if (act === 'init') {
|
|
240
|
+
result = initFeature({ root, slug, take: flags.take, intent: flags.intent, goal: flags.goal });
|
|
241
|
+
if (result.ok) recordFeature({ storeRoot: root, cwd: cwd || process.cwd(), slug });
|
|
242
|
+
}
|
|
243
|
+
else if (act === 'release') {
|
|
244
|
+
result = releaseFeatures({ root, tag: flags.tag, confirm: Boolean(flags.confirm), features: flags.feature, exclude: flags.exclude, channel: flags.channel });
|
|
245
|
+
if (result.ok && result.recorded) for (const shipped of [...result.candidates.map((c ) => c.slug), ...result.added]) if (!result.excluded.includes(shipped)) clearSlug({ storeRoot: root, slug: shipped });
|
|
246
|
+
} else if (act === 'merged') {
|
|
247
|
+
result = recordMerged({ root, slug, reason: flags.reason, channel: flags.channel });
|
|
248
|
+
if (result.ok || result.finding === 'already-merged') clearSlug({ storeRoot: root, slug });
|
|
249
|
+
} else result = slug ? featureStatus({ root, slug }) : activeFeature({ root, cwd: cwd || process.cwd() });
|
|
240
250
|
return emit(result, json, lifecycleLines);
|
|
241
251
|
});
|
|
242
252
|
}
|
|
@@ -39,12 +39,12 @@ const MAX_EVENT_BYTES = 256 * 1024;
|
|
|
39
39
|
const MAX_JOURNAL_BYTES = 16 * 1024 * 1024;
|
|
40
40
|
const READ_CHUNK = 64 * 1024;
|
|
41
41
|
/** The decision register's channel vocabulary: who the caller says is acting. */
|
|
42
|
-
const CHANNEL_BY = { question: 'user', gate: 'user', prompt: 'user', default: 'agent' };
|
|
42
|
+
const CHANNEL_BY = { question: 'user', gate: 'user', prompt: 'user', default: 'agent', resolution: 'agent', policy: 'autopilot' };
|
|
43
43
|
|
|
44
44
|
function eventsDir(root ) { return path.join(root, EVENTS_REL); }
|
|
45
45
|
function lockTarget(root ) { return path.join(root, EVENTS_REL); }
|
|
46
46
|
|
|
47
|
-
/** @param {string} [channel] defaults to `default` @returns {'user'|'agent'|null} null for an unknown channel */
|
|
47
|
+
/** @param {string} [channel] defaults to `default` @returns {'user'|'agent'|'autopilot'|null} null for an unknown channel */
|
|
48
48
|
function byFromChannel(channel = 'default') { return CHANNEL_BY[channel] ?? null; }
|
|
49
49
|
|
|
50
50
|
/** `length` random characters of the lowercase Crockford base32 alphabet. */
|
|
@@ -235,13 +235,15 @@ function readEvents(root , { fsImpl = nodeFs, lockFallback = true, should
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
|
-
* Reads and folds the store.
|
|
238
|
+
* Reads and folds the store. With `DOFLOW_UPKEEP=off` in `env` the read never falls back to the
|
|
239
|
+
* store lock (it would make `events.lock`, a write the switch promises not to make): a journal that
|
|
240
|
+
* keeps moving during the read throws StoreLockedError instead.
|
|
239
241
|
* @param {string} root the IC-001 root
|
|
240
|
-
* @param {{hasBody?: Function, fsImpl?: Object}} [options]
|
|
242
|
+
* @param {{hasBody?: Function, fsImpl?: Object, now?: Date, env?: Object}} [options]
|
|
241
243
|
* @returns {Object} the fold result (see fold.js) plus `unreadable`
|
|
242
244
|
*/
|
|
243
|
-
function readFold(root , { fsImpl = nodeFs, hasBody, now = new Date() } = {}) {
|
|
244
|
-
const { events, unreadable, reasons } = readEvents(root, { fsImpl });
|
|
245
|
+
function readFold(root , { fsImpl = nodeFs, hasBody, now = new Date(), env = process.env } = {}) {
|
|
246
|
+
const { events, unreadable, reasons } = readEvents(root, { fsImpl, lockFallback: env.DOFLOW_UPKEEP !== 'off' });
|
|
245
247
|
return { ...finalize(foldInto(events, { now }), { hasBody }), unreadable, unreadableReasons: reasons };
|
|
246
248
|
}
|
|
247
249
|
|
|
@@ -73,7 +73,7 @@ function newFollowupId(taken ) {
|
|
|
73
73
|
|
|
74
74
|
function channelBy(channel ) {
|
|
75
75
|
const by = byFromChannel(channel || 'default');
|
|
76
|
-
if (!by) throw new FollowupUsageError(`unknown --channel '${channel}'. Valid: question, gate, prompt, default`);
|
|
76
|
+
if (!by) throw new FollowupUsageError(`unknown --channel '${channel}'. Valid: question, gate, prompt, resolution, default`);
|
|
77
77
|
return by;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -350,6 +350,11 @@ function takeFollowups({ root, ids, slug, channel, now = new Date(), fsImpl = no
|
|
|
350
350
|
if (!slug) throw new FollowupUsageError('--slug is required for --action take');
|
|
351
351
|
const bad = invalidSlugRefusal(slug);
|
|
352
352
|
if (bad) throw new FollowupUsageError(bad.message);
|
|
353
|
+
// `list` shows a taken item whose feature finished as `done`, but the fold still holds it `taken`: say what the user was shown.
|
|
354
|
+
const shown = loadFollowups(root, { fsImpl, now }).followups.find((item ) => item.derived && list.includes(item.id));
|
|
355
|
+
if (shown) {
|
|
356
|
+
return refusal('take', 'illegal-transition', `${shown.id} is done because feature ${shown.takenBy} finished (derived from git and the release records, not recorded on the item); only an open item can be taken. Nothing was written.`);
|
|
357
|
+
}
|
|
353
358
|
const out = appendEvents(root, [{ type: 'followup.taken', by: channelBy(channel), data: { ids: list, feature: slug } }], { now, fsImpl });
|
|
354
359
|
if (!out.ok) return refusalFrom('take', out);
|
|
355
360
|
return { ok: true, action: 'take', ids: list, feature: slug, events: out.written.map((w ) => w.file), next: [] };
|
|
@@ -16,6 +16,7 @@ const { withDerivedDone } = require('./fold.js');
|
|
|
16
16
|
const { appendEvents, readFold, readEvents } = require('./event-store.js');
|
|
17
17
|
const { deriveStatuses, bucketize, behindNote } = require('./status.js');
|
|
18
18
|
const { FollowupUsageError, parseIds } = require('./followup.js');
|
|
19
|
+
const { describeActive } = require('../feature-pointer.js');
|
|
19
20
|
|
|
20
21
|
/** The failure store, or null when its module cannot be loaded: the overview then reports no failures. */
|
|
21
22
|
function failureStoreOrNull() {
|
|
@@ -289,4 +290,26 @@ function featureStatus({ root, slug, now = new Date(), fsImpl = nodeFs } ) {
|
|
|
289
290
|
return result;
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
|
|
293
|
+
/**
|
|
294
|
+
* `status` with no `--slug` (row 135): the feature the runtime recorded for this checkout, against the
|
|
295
|
+
* branch's. A mismatch or an unreadable pointer is a refusal (exit 1) that names both sides and picks
|
|
296
|
+
* neither; an agreeing pointer also carries the tracked feature's derived status, as `--slug` does.
|
|
297
|
+
*/
|
|
298
|
+
function activeFeature({ root, cwd, now = new Date(), fsImpl = nodeFs } ) {
|
|
299
|
+
const active = describeActive({ storeRoot: root, cwd });
|
|
300
|
+
const here = `branch ${active.current.branch || 'unknown'}${active.current.slug ? `, branch feature ${active.current.slug}` : ''}`;
|
|
301
|
+
if (active.state === 'unreadable') {
|
|
302
|
+
return { ok: false, action: 'status', finding: 'feature-pointer-unreadable', message: `${active.file} cannot be read (${active.reason}); delete it to let the next feature start write it again.`, active };
|
|
303
|
+
}
|
|
304
|
+
if (active.state === 'mismatch') {
|
|
305
|
+
const p = active.pointer;
|
|
306
|
+
return { ok: false, action: 'status', finding: 'feature-mismatch', message: `the pointer names ${p.slug} on branch ${p.branch || 'unknown'}, but this checkout is on ${here}; neither is trusted, so check which feature you are on.`, active };
|
|
307
|
+
}
|
|
308
|
+
const result = { ok: true, action: 'status', active };
|
|
309
|
+
if (active.state === 'none') return { ...result, message: `no active feature is recorded for this checkout (${here}); pass --slug <slug> to ask about one.` };
|
|
310
|
+
// An untracked feature is refused by featureStatus; the pointer answer stands, named, without status fields.
|
|
311
|
+
const derived = featureStatus({ root, slug: active.slug, now, fsImpl });
|
|
312
|
+
return derived.ok ? { ...derived, active } : { ...result, slug: active.slug, message: `${active.slug} is tracked by its run only; \`lifecycle --action init\` to track it.` };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = { buildOverview, initFeature, featureStatus, activeFeature, goalView, goalConflicts, goalConflictNext, DISCOVERY_SHOWN, MAINTAIN_SHOWN, FEATURES_SHOWN };
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The machine-local report body (IC-005, DEC-014). A report's text can hold a crash trace, a log or
|
|
7
7
|
* pasted output, so it never enters the repository: the body is masked with the body profile (IC-012),
|
|
8
8
|
* cut to at most 1 MiB and written once under the same per-machine home the failure store uses,
|
|
9
|
-
* `<
|
|
9
|
+
* `<config home>/doflow/reports/<projectKey>/<FU-id>.txt`, folder 0700 and file
|
|
10
10
|
* 0600. The project event carries only a reference, the size and a masked 2 KB excerpt.
|
|
11
11
|
*
|
|
12
12
|
* `projectKey` is the first 12 hex characters of the SHA-256 of the IC-001 root's real path, so two
|
|
@@ -20,6 +20,7 @@ const nodeFs = require('node:fs');
|
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const crypto = require('node:crypto');
|
|
22
22
|
const mask = require('../mask.js');
|
|
23
|
+
const { configHome } = require('../config-home.js');
|
|
23
24
|
|
|
24
25
|
/** A `--file` or `--stdin` source is read for at most this many bytes; the rest could not be kept anyway. */
|
|
25
26
|
const MAX_READ_BYTES = 16 * 1024 * 1024;
|
|
@@ -38,17 +39,14 @@ const EXCERPT_BYTES = 2048;
|
|
|
38
39
|
class ReportInputError extends Error {}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* The folder holding every project's report bodies, or null when no home resolves. It
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* a build or an install where the failure modules cannot be loaded (IC-016).
|
|
42
|
+
* The folder holding every project's report bodies, or null when no config home resolves. It sits in
|
|
43
|
+
* the same config home as the failure home (`../config-home.js`), `doflow/reports`, and takes that
|
|
44
|
+
* leaf helper rather than the failure module, so the follow-up service still loads in a build or an
|
|
45
|
+
* install where the failure modules cannot be loaded (IC-016).
|
|
46
46
|
*/
|
|
47
47
|
function reportsHome(env = process.env) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const home = env.HOME;
|
|
51
|
-
return typeof home === 'string' && path.isAbsolute(home) ? path.join(home, '.config', 'doflow', 'reports') : null;
|
|
48
|
+
const { dir } = configHome(env);
|
|
49
|
+
return dir === null ? null : path.join(dir, 'reports');
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
function realProjectRoot(root , fsImpl = nodeFs) {
|