session-orchestrator 3.16.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/.claude-plugin/marketplace.json +29 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/agents/explorer.toml +14 -0
- package/.codex-plugin/agents/session-reviewer.toml +23 -0
- package/.codex-plugin/agents/wave-worker.toml +15 -0
- package/.codex-plugin/config.toml +20 -0
- package/.codex-plugin/plugin.json +37 -0
- package/.cursor/rules/000-session-orchestrator.mdc +73 -0
- package/.cursor/rules/010-session-workflow.mdc +170 -0
- package/.cursor/rules/020-quality-gates.mdc +128 -0
- package/.cursor/rules/030-wave-execution.mdc +216 -0
- package/.cursor/rules/040-discovery.mdc +242 -0
- package/.cursor/rules/050-plan.mdc +235 -0
- package/.cursor/rules/060-evolve.mdc +232 -0
- package/.cursor/rules/070-gitlab-ops.mdc +246 -0
- package/.cursor/rules/080-ecosystem-health.mdc +145 -0
- package/.mcp.json +8 -0
- package/CHANGELOG.md +1544 -0
- package/LICENSE +21 -0
- package/NOTICE +64 -0
- package/README.md +242 -0
- package/SECURITY.md +90 -0
- package/agents/AGENTS.md +136 -0
- package/agents/analyst.md +99 -0
- package/agents/architect-reviewer.md +93 -0
- package/agents/code-implementer.md +106 -0
- package/agents/db-specialist.md +104 -0
- package/agents/dialectic-deriver.md +139 -0
- package/agents/docs-writer.md +113 -0
- package/agents/eval-judge.md +146 -0
- package/agents/memory-proposal-collector.md +297 -0
- package/agents/qa-strategist.md +102 -0
- package/agents/schemas/analyst.schema.json +46 -0
- package/agents/schemas/architect-reviewer.schema.json +50 -0
- package/agents/schemas/code-implementer.schema.json +61 -0
- package/agents/schemas/db-specialist.schema.json +80 -0
- package/agents/schemas/docs-writer.schema.json +56 -0
- package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
- package/agents/schemas/qa-strategist.schema.json +46 -0
- package/agents/schemas/security-reviewer.schema.json +86 -0
- package/agents/schemas/session-reviewer.schema.json +69 -0
- package/agents/schemas/test-writer.schema.json +69 -0
- package/agents/schemas/ui-developer.schema.json +90 -0
- package/agents/schemas/ux-evaluator.schema.json +51 -0
- package/agents/security-reviewer.md +236 -0
- package/agents/session-reviewer.md +201 -0
- package/agents/skill-applied-judge.md +122 -0
- package/agents/test-writer.md +123 -0
- package/agents/ui-developer.md +109 -0
- package/agents/ux-evaluator.md +161 -0
- package/assets/icon.svg +11 -0
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +47 -0
- package/commands/autopilot-multi.md +74 -0
- package/commands/autopilot.md +80 -0
- package/commands/bootstrap.md +56 -0
- package/commands/brainstorm.md +48 -0
- package/commands/close.md +24 -0
- package/commands/debug.md +36 -0
- package/commands/discovery.md +32 -0
- package/commands/dispatcher.md +59 -0
- package/commands/eval.md +28 -0
- package/commands/evolve.md +10 -0
- package/commands/go.md +41 -0
- package/commands/grill.md +45 -0
- package/commands/harness-audit.md +26 -0
- package/commands/memory-cleanup.md +25 -0
- package/commands/persona-panel.md +121 -0
- package/commands/plan.md +15 -0
- package/commands/portfolio.md +97 -0
- package/commands/reconcile.md +23 -0
- package/commands/repo-audit.md +24 -0
- package/commands/session.md +30 -0
- package/commands/spinout.md +15 -0
- package/commands/sunset-review.md +27 -0
- package/commands/templates-ack.md +96 -0
- package/commands/test.md +97 -0
- package/docs/README.md +105 -0
- package/docs/USER-GUIDE.md +1403 -0
- package/docs/ci-setup.md +81 -0
- package/docs/codex-setup.md +142 -0
- package/docs/components.md +74 -0
- package/docs/cursor-setup.md +104 -0
- package/docs/events-schema.md +81 -0
- package/docs/migration-v3.md +148 -0
- package/docs/owner-config-schema.md +154 -0
- package/docs/persona-panel.md +433 -0
- package/docs/pi-setup.md +115 -0
- package/docs/plugin-architecture-v3.md +296 -0
- package/docs/pm-skills-marketplace.md +114 -0
- package/docs/policy-cache-validation-2026-04-28.md +118 -0
- package/docs/rule-authoring.md +316 -0
- package/docs/session-config-reference.md +1439 -0
- package/docs/session-config-template.md +961 -0
- package/docs/vault-docs-architecture.md +297 -0
- package/hooks/_lib/lock-bootstrap.mjs +272 -0
- package/hooks/_lib/lock-reconcile.mjs +93 -0
- package/hooks/_lib/profile-gate.mjs +95 -0
- package/hooks/_lib/transcript-history.mjs +211 -0
- package/hooks/agent-teams-h3-test.sh +362 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/cwd-change-restore.mjs +131 -0
- package/hooks/enforce-commands.mjs +179 -0
- package/hooks/enforce-scope.mjs +273 -0
- package/hooks/hooks-codex.json +60 -0
- package/hooks/hooks-cursor.json +15 -0
- package/hooks/hooks-pi.json +115 -0
- package/hooks/hooks.json +215 -0
- package/hooks/loop-guard.mjs +260 -0
- package/hooks/on-session-end.mjs +217 -0
- package/hooks/on-session-start.mjs +660 -0
- package/hooks/on-stop.mjs +294 -0
- package/hooks/operator-steer.mjs +64 -0
- package/hooks/post-edit-validate.mjs +225 -0
- package/hooks/post-subagent-discovery-validator.mjs +398 -0
- package/hooks/post-tool-batch-wave-signal.mjs +328 -0
- package/hooks/post-tool-failure-corrective-context.mjs +248 -0
- package/hooks/post-tooluse-frontend-slop.mjs +184 -0
- package/hooks/pre-bash-destructive-guard.mjs +515 -0
- package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
- package/hooks/pre-bash-staging-fence.mjs +223 -0
- package/hooks/pre-bash-templates-first.mjs +404 -0
- package/hooks/run-node.sh +72 -0
- package/hooks/skill-invocation-telemetry.mjs +99 -0
- package/hooks/subagent-telemetry.mjs +249 -0
- package/hooks/wave-scope-commit-guard.mjs +191 -0
- package/monitors/monitors.json +14 -0
- package/output-styles/finding-report.md +48 -0
- package/output-styles/session-report.md +53 -0
- package/output-styles/wave-summary.md +38 -0
- package/package.json +94 -0
- package/pi/extensions/session-orchestrator.ts +25 -0
- package/pi/prompts/autopilot-multi.md +12 -0
- package/pi/prompts/autopilot.md +12 -0
- package/pi/prompts/bootstrap.md +12 -0
- package/pi/prompts/brainstorm.md +12 -0
- package/pi/prompts/close.md +11 -0
- package/pi/prompts/debug.md +12 -0
- package/pi/prompts/discovery.md +12 -0
- package/pi/prompts/dispatcher.md +12 -0
- package/pi/prompts/eval.md +12 -0
- package/pi/prompts/evolve.md +12 -0
- package/pi/prompts/go.md +12 -0
- package/pi/prompts/grill.md +12 -0
- package/pi/prompts/harness-audit.md +12 -0
- package/pi/prompts/memory-cleanup.md +12 -0
- package/pi/prompts/persona-panel.md +12 -0
- package/pi/prompts/plan.md +12 -0
- package/pi/prompts/portfolio.md +12 -0
- package/pi/prompts/reconcile.md +12 -0
- package/pi/prompts/repo-audit.md +12 -0
- package/pi/prompts/session.md +12 -0
- package/pi/prompts/spinout.md +12 -0
- package/pi/prompts/sunset-review.md +12 -0
- package/pi/prompts/templates-ack.md +12 -0
- package/pi/prompts/test.md +12 -0
- package/rules/_index.md +51 -0
- package/rules/always-on/commit-discipline.md +26 -0
- package/rules/always-on/npm-quality-gates.md +26 -0
- package/rules/always-on/parallel-sessions.md +43 -0
- package/rules/opt-in-domain/prompt-caching.md +270 -0
- package/rules/opt-in-stack/backend-data.md +188 -0
- package/rules/opt-in-stack/backend.md +390 -0
- package/rules/opt-in-stack/frontend.md +98 -0
- package/rules/opt-in-stack/security-web.md +194 -0
- package/rules/opt-in-stack/swift.md +65 -0
- package/scripts/archive-closed-prds.mjs +416 -0
- package/scripts/autopilot-multi.mjs +802 -0
- package/scripts/autopilot.mjs +383 -0
- package/scripts/backfill-abandoned-sessions.mjs +265 -0
- package/scripts/backfill-learnings-expires.mjs +196 -0
- package/scripts/backfill-learnings.mjs +203 -0
- package/scripts/backfill-sessions.mjs +282 -0
- package/scripts/check-doc-consistency.sh +279 -0
- package/scripts/check-package-manager.mjs +445 -0
- package/scripts/ci/assert-vitest-green.mjs +267 -0
- package/scripts/codex-install.mjs +435 -0
- package/scripts/compute-grounding-injection.sh +186 -0
- package/scripts/cursor-install.mjs +113 -0
- package/scripts/dialectic-deriver.mjs +573 -0
- package/scripts/emit-event.mjs +160 -0
- package/scripts/emit-session.mjs +212 -0
- package/scripts/eval-session.mjs +262 -0
- package/scripts/export-hw-learnings.mjs +437 -0
- package/scripts/gc-stale-worktrees.mjs +666 -0
- package/scripts/generate-pi-prompts.mjs +127 -0
- package/scripts/harness-audit.mjs +287 -0
- package/scripts/lib/agent-frontmatter.mjs +266 -0
- package/scripts/lib/agent-output-schema.mjs +166 -0
- package/scripts/lib/agent-status.mjs +303 -0
- package/scripts/lib/ajv-loader.mjs +34 -0
- package/scripts/lib/auto-dialectic.mjs +382 -0
- package/scripts/lib/auto-dream.mjs +471 -0
- package/scripts/lib/autonomy/suitability.mjs +212 -0
- package/scripts/lib/autopilot/dep-graph.mjs +417 -0
- package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
- package/scripts/lib/autopilot/flags.mjs +104 -0
- package/scripts/lib/autopilot/kill-switches.mjs +174 -0
- package/scripts/lib/autopilot/loop.mjs +320 -0
- package/scripts/lib/autopilot/mr-draft.mjs +520 -0
- package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
- package/scripts/lib/autopilot/recent-runs.mjs +106 -0
- package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
- package/scripts/lib/autopilot/telemetry.mjs +224 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
- package/scripts/lib/autopilot-telemetry.mjs +11 -0
- package/scripts/lib/autopilot.mjs +39 -0
- package/scripts/lib/backlog-scan.mjs +179 -0
- package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
- package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
- package/scripts/lib/build-live-signals.mjs +150 -0
- package/scripts/lib/ci-status-banner.mjs +425 -0
- package/scripts/lib/claude-md-budget-lint.mjs +246 -0
- package/scripts/lib/cli-flags.mjs +158 -0
- package/scripts/lib/codex/plugin-contract.mjs +610 -0
- package/scripts/lib/cold-start-detector.mjs +240 -0
- package/scripts/lib/command-blocker.mjs +458 -0
- package/scripts/lib/common.mjs +333 -0
- package/scripts/lib/config/auto-dream.mjs +77 -0
- package/scripts/lib/config/block-header.mjs +94 -0
- package/scripts/lib/config/broken-window.mjs +114 -0
- package/scripts/lib/config/coercers.mjs +248 -0
- package/scripts/lib/config/cold-start.mjs +92 -0
- package/scripts/lib/config/config-protection.mjs +120 -0
- package/scripts/lib/config/cross-repo.mjs +104 -0
- package/scripts/lib/config/custom-phases.mjs +213 -0
- package/scripts/lib/config/dialectic.mjs +92 -0
- package/scripts/lib/config/discovery-validator.mjs +75 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
- package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
- package/scripts/lib/config/docs-orchestrator.mjs +90 -0
- package/scripts/lib/config/docs-staleness.mjs +96 -0
- package/scripts/lib/config/drift-check.mjs +155 -0
- package/scripts/lib/config/eval.mjs +130 -0
- package/scripts/lib/config/events-rotation.mjs +74 -0
- package/scripts/lib/config/evolve.mjs +308 -0
- package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
- package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
- package/scripts/lib/config/handover-gate.mjs +106 -0
- package/scripts/lib/config/host-paths.mjs +76 -0
- package/scripts/lib/config/io.mjs +54 -0
- package/scripts/lib/config/loop-guard.mjs +117 -0
- package/scripts/lib/config/memory.mjs +150 -0
- package/scripts/lib/config/persona-gate-wave.mjs +258 -0
- package/scripts/lib/config/reconcile.mjs +205 -0
- package/scripts/lib/config/section-extractor.mjs +100 -0
- package/scripts/lib/config/skill-evolution.mjs +112 -0
- package/scripts/lib/config/slopcheck.mjs +99 -0
- package/scripts/lib/config/state-md-lock.mjs +83 -0
- package/scripts/lib/config/templates-first.mjs +94 -0
- package/scripts/lib/config/test.mjs +113 -0
- package/scripts/lib/config/vault-integration.mjs +201 -0
- package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
- package/scripts/lib/config/vault-staleness.mjs +84 -0
- package/scripts/lib/config/vault-sync.mjs +96 -0
- package/scripts/lib/config/verification-auto-fix.mjs +84 -0
- package/scripts/lib/config/wave-reviewers.mjs +133 -0
- package/scripts/lib/config-schema.mjs +345 -0
- package/scripts/lib/config.mjs +474 -0
- package/scripts/lib/convergence-monitor.mjs +389 -0
- package/scripts/lib/coordinator-snapshot.mjs +371 -0
- package/scripts/lib/crypto-digest-utils.mjs +91 -0
- package/scripts/lib/discovery/helpers.mjs +127 -0
- package/scripts/lib/discovery/triage-state.mjs +279 -0
- package/scripts/lib/dispatcher/cli.mjs +257 -0
- package/scripts/lib/dispatcher/enumerate.mjs +243 -0
- package/scripts/lib/dispatcher/rank.mjs +363 -0
- package/scripts/lib/ecosystem-health.mjs +224 -0
- package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
- package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
- package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
- package/scripts/lib/ecosystem-wizard.mjs +48 -0
- package/scripts/lib/env-check.mjs +89 -0
- package/scripts/lib/eval/engine.mjs +605 -0
- package/scripts/lib/eval/judge.mjs +433 -0
- package/scripts/lib/eval/report.mjs +367 -0
- package/scripts/lib/eval/schema.mjs +618 -0
- package/scripts/lib/eval/session-resolve.mjs +137 -0
- package/scripts/lib/eval/sink.mjs +77 -0
- package/scripts/lib/events-rotation.mjs +86 -0
- package/scripts/lib/events-schema.mjs +81 -0
- package/scripts/lib/events.mjs +80 -0
- package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
- package/scripts/lib/exclusivity-matrix.mjs +68 -0
- package/scripts/lib/fetch-baseline.mjs +311 -0
- package/scripts/lib/file-lock.mjs +512 -0
- package/scripts/lib/frontend-detect/detect.mjs +138 -0
- package/scripts/lib/frontend-detect/rules.mjs +295 -0
- package/scripts/lib/frontmatter-guard.mjs +241 -0
- package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
- package/scripts/lib/gates/gate-baseline.mjs +42 -0
- package/scripts/lib/gates/gate-full.mjs +85 -0
- package/scripts/lib/gates/gate-helpers.mjs +231 -0
- package/scripts/lib/gates/gate-incremental.mjs +76 -0
- package/scripts/lib/gates/gate-per-file.mjs +55 -0
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
- package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
- package/scripts/lib/handover-gate.mjs +222 -0
- package/scripts/lib/hardening.mjs +43 -0
- package/scripts/lib/hardware-pattern-detector.mjs +238 -0
- package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
- package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
- package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
- package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
- package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
- package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
- package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
- package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
- package/scripts/lib/harness-audit/categories.mjs +19 -0
- package/scripts/lib/historical-guard.mjs +15 -0
- package/scripts/lib/host-identity.mjs +262 -0
- package/scripts/lib/instruction-budget-guard.mjs +332 -0
- package/scripts/lib/io.mjs +304 -0
- package/scripts/lib/issue-close-strip-labels.mjs +161 -0
- package/scripts/lib/language-mappers/README.md +57 -0
- package/scripts/lib/language-mappers/index.mjs +165 -0
- package/scripts/lib/language-mappers/markdown.mjs +149 -0
- package/scripts/lib/language-mappers/python.mjs +249 -0
- package/scripts/lib/language-mappers/swift.mjs +201 -0
- package/scripts/lib/language-mappers/typescript.mjs +433 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
- package/scripts/lib/learnings/filters.mjs +43 -0
- package/scripts/lib/learnings/io.mjs +255 -0
- package/scripts/lib/learnings/schema.mjs +518 -0
- package/scripts/lib/learnings/surface.mjs +207 -0
- package/scripts/lib/learnings.mjs +42 -0
- package/scripts/lib/lock-reaper.mjs +648 -0
- package/scripts/lib/locks/index.mjs +31 -0
- package/scripts/lib/locks/lock-body.mjs +62 -0
- package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
- package/scripts/lib/locks/state-md-lock.mjs +351 -0
- package/scripts/lib/loop-readiness-banner.mjs +144 -0
- package/scripts/lib/memory-banner.mjs +478 -0
- package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
- package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
- package/scripts/lib/memory-paths.mjs +31 -0
- package/scripts/lib/memory-proposals/collector.mjs +334 -0
- package/scripts/lib/memory-proposals/schema.mjs +289 -0
- package/scripts/lib/memory-proposals/sink.mjs +507 -0
- package/scripts/lib/memory-proposals/store.mjs +441 -0
- package/scripts/lib/mission-status-schema.mjs +114 -0
- package/scripts/lib/mode-selector/alternatives.mjs +64 -0
- package/scripts/lib/mode-selector/constants.mjs +29 -0
- package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
- package/scripts/lib/mode-selector/rationale.mjs +55 -0
- package/scripts/lib/mode-selector/scoring.mjs +221 -0
- package/scripts/lib/mode-selector-accuracy.mjs +121 -0
- package/scripts/lib/mode-selector.mjs +160 -0
- package/scripts/lib/multi-provider-build/providers.mjs +64 -0
- package/scripts/lib/multi-provider-build/templating.mjs +130 -0
- package/scripts/lib/named-baseline-resolver.mjs +233 -0
- package/scripts/lib/named-vault-resolver.mjs +433 -0
- package/scripts/lib/owner-config/coerce.mjs +29 -0
- package/scripts/lib/owner-config/constants.mjs +21 -0
- package/scripts/lib/owner-config/defaults.mjs +50 -0
- package/scripts/lib/owner-config/error.mjs +19 -0
- package/scripts/lib/owner-config/index.mjs +13 -0
- package/scripts/lib/owner-config/merge.mjs +52 -0
- package/scripts/lib/owner-config/validate.mjs +259 -0
- package/scripts/lib/owner-config-banner.mjs +126 -0
- package/scripts/lib/owner-config-loader.mjs +159 -0
- package/scripts/lib/owner-config.example.yaml +72 -0
- package/scripts/lib/owner-config.mjs +28 -0
- package/scripts/lib/owner-interview.mjs +243 -0
- package/scripts/lib/owner-yaml.mjs +571 -0
- package/scripts/lib/package-manager.mjs +160 -0
- package/scripts/lib/path-utils.mjs +217 -0
- package/scripts/lib/peer-cards/merger.mjs +310 -0
- package/scripts/lib/peer-cards/reader.mjs +125 -0
- package/scripts/lib/peer-cards/schema.mjs +230 -0
- package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
- package/scripts/lib/peer-cards/writer.mjs +138 -0
- package/scripts/lib/peer-discovery.mjs +200 -0
- package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
- package/scripts/lib/persona-panel/consolidator.mjs +370 -0
- package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
- package/scripts/lib/persona-panel/threshold.mjs +130 -0
- package/scripts/lib/pi-hook-bridge.mjs +328 -0
- package/scripts/lib/platform.mjs +266 -0
- package/scripts/lib/playwright-driver/runner.mjs +297 -0
- package/scripts/lib/plugin-root.mjs +210 -0
- package/scripts/lib/pre-dispatch-check.mjs +126 -0
- package/scripts/lib/product-repo-detect.mjs +121 -0
- package/scripts/lib/profiles/registry.mjs +176 -0
- package/scripts/lib/profiles/schema.mjs +209 -0
- package/scripts/lib/qg-command-drift-banner.mjs +88 -0
- package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
- package/scripts/lib/quality-gate.mjs +536 -0
- package/scripts/lib/quality-gates-cache.mjs +228 -0
- package/scripts/lib/quality-gates-policy.mjs +95 -0
- package/scripts/lib/recommendations-v0.mjs +156 -0
- package/scripts/lib/reconcile/eligibility.mjs +203 -0
- package/scripts/lib/reconcile/emitter.mjs +244 -0
- package/scripts/lib/reconcile/engine.mjs +412 -0
- package/scripts/lib/reconcile/idempotency.mjs +239 -0
- package/scripts/lib/reconcile/renderer.mjs +211 -0
- package/scripts/lib/reconcile/writer.mjs +293 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
- package/scripts/lib/resource-probe/evaluate.mjs +190 -0
- package/scripts/lib/resource-probe/parsers.mjs +181 -0
- package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
- package/scripts/lib/resource-probe.mjs +95 -0
- package/scripts/lib/rule-loader.mjs +552 -0
- package/scripts/lib/rules-sync.mjs +439 -0
- package/scripts/lib/scope-gate.mjs +496 -0
- package/scripts/lib/session-close-backfill.mjs +539 -0
- package/scripts/lib/session-discovery.mjs +256 -0
- package/scripts/lib/session-end/phase-skip.mjs +357 -0
- package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
- package/scripts/lib/session-id.mjs +362 -0
- package/scripts/lib/session-lock.mjs +703 -0
- package/scripts/lib/session-registry.mjs +355 -0
- package/scripts/lib/session-schema/aliases.mjs +71 -0
- package/scripts/lib/session-schema/constants.mjs +115 -0
- package/scripts/lib/session-schema/normalizer.mjs +66 -0
- package/scripts/lib/session-schema/timestamps.mjs +64 -0
- package/scripts/lib/session-schema/validator.mjs +453 -0
- package/scripts/lib/session-schema.mjs +71 -0
- package/scripts/lib/session-token-rollup.mjs +137 -0
- package/scripts/lib/sessions-staleness-banner.mjs +247 -0
- package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
- package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
- package/scripts/lib/skill-evolution/engine.mjs +719 -0
- package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
- package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
- package/scripts/lib/skill-health/join.mjs +181 -0
- package/scripts/lib/skill-health/score.mjs +123 -0
- package/scripts/lib/skill-invocations-schema.mjs +214 -0
- package/scripts/lib/skill-judge.mjs +348 -0
- package/scripts/lib/skill-judgments-schema.mjs +264 -0
- package/scripts/lib/slopcheck.mjs +501 -0
- package/scripts/lib/soul-resolve.mjs +118 -0
- package/scripts/lib/spiral-carryover.mjs +495 -0
- package/scripts/lib/state-md/body-sections.mjs +851 -0
- package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
- package/scripts/lib/state-md/mission-status.mjs +247 -0
- package/scripts/lib/state-md/recommendations.mjs +57 -0
- package/scripts/lib/state-md/yaml-parser.mjs +234 -0
- package/scripts/lib/state-md-peer-guard.mjs +232 -0
- package/scripts/lib/state-md.mjs +53 -0
- package/scripts/lib/subagents-schema.mjs +309 -0
- package/scripts/lib/sunset/walker.mjs +1192 -0
- package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
- package/scripts/lib/test-runner/fingerprint.mjs +33 -0
- package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
- package/scripts/lib/tmux-layout/layouts.mjs +224 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
- package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
- package/scripts/lib/validate/check-agents.mjs +457 -0
- package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
- package/scripts/lib/validate/check-commands.mjs +148 -0
- package/scripts/lib/validate/check-component-paths.mjs +112 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
- package/scripts/lib/validate/check-json-files.mjs +116 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
- package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
- package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
- package/scripts/lib/validate/check-pi-package.mjs +110 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
- package/scripts/lib/validate/check-plugin-json.mjs +96 -0
- package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
- package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
- package/scripts/lib/validate/check-rules.mjs +143 -0
- package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
- package/scripts/lib/validate/confidential-names.mjs +169 -0
- package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
- package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
- package/scripts/lib/validate/tier-inference.mjs +100 -0
- package/scripts/lib/validate-vendored-rules.mjs +519 -0
- package/scripts/lib/vault-archive.mjs +404 -0
- package/scripts/lib/vault-backfill/glab.mjs +164 -0
- package/scripts/lib/vault-backfill/manifest.mjs +75 -0
- package/scripts/lib/vault-backfill/template.mjs +130 -0
- package/scripts/lib/vault-consolidate-fs.mjs +331 -0
- package/scripts/lib/vault-migration-rules.mjs +155 -0
- package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
- package/scripts/lib/vault-mirror/namespace.mjs +152 -0
- package/scripts/lib/vault-mirror/process.mjs +567 -0
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
- package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
- package/scripts/lib/vault-mirror/render.mjs +8 -0
- package/scripts/lib/vault-mirror/utils.mjs +217 -0
- package/scripts/lib/vault-relocation-rules.mjs +555 -0
- package/scripts/lib/vault-repo-backfill.mjs +235 -0
- package/scripts/lib/vault-staleness-banner.mjs +142 -0
- package/scripts/lib/vault-status/board-writer.mjs +769 -0
- package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
- package/scripts/lib/vault-sync-baseline.mjs +152 -0
- package/scripts/lib/wave-context.mjs +29 -0
- package/scripts/lib/wave-executor/pool.mjs +248 -0
- package/scripts/lib/wave-resource-gate.mjs +204 -0
- package/scripts/lib/wave-sizing.mjs +75 -0
- package/scripts/lib/webhook-url.mjs +105 -0
- package/scripts/lib/workspace.mjs +198 -0
- package/scripts/lib/worktree/constants.mjs +35 -0
- package/scripts/lib/worktree/index.mjs +17 -0
- package/scripts/lib/worktree/lifecycle.mjs +287 -0
- package/scripts/lib/worktree/listing.mjs +118 -0
- package/scripts/lib/worktree/meta.mjs +64 -0
- package/scripts/lib/worktree-freshness.mjs +313 -0
- package/scripts/lib/worktree.mjs +15 -0
- package/scripts/lifecycle-sim-v6.mjs +347 -0
- package/scripts/lock-reaper.mjs +185 -0
- package/scripts/mcp-server.sh +241 -0
- package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
- package/scripts/memory-propose.mjs +464 -0
- package/scripts/migrate-cold-start-seed.mjs +404 -0
- package/scripts/migrate-learnings-jsonl.mjs +189 -0
- package/scripts/migrate-legacy-learnings.sh +61 -0
- package/scripts/migrate-sessions-jsonl.mjs +448 -0
- package/scripts/migrate-subagents-jsonl.mjs +196 -0
- package/scripts/migrate-vault-paths.mjs +796 -0
- package/scripts/parse-config.mjs +149 -0
- package/scripts/pi-install.mjs +117 -0
- package/scripts/print-applicable-rules.mjs +247 -0
- package/scripts/promote-vault-strict.mjs +496 -0
- package/scripts/relocate-vault-corpus.mjs +1178 -0
- package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
- package/scripts/run-quality-gate.mjs +216 -0
- package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
- package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
- package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
- package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
- package/scripts/sweep-expired-learnings.mjs +135 -0
- package/scripts/sync-vault-schema.mjs +376 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
- package/scripts/tmux-layout.mjs +245 -0
- package/scripts/token-audit.sh +191 -0
- package/scripts/typecheck.mjs +42 -0
- package/scripts/upload-social-preview.mjs +316 -0
- package/scripts/validate-config.mjs +46 -0
- package/scripts/validate-plugin-manifests.mjs +163 -0
- package/scripts/validate-plugin.mjs +264 -0
- package/scripts/validate-wave-scope.mjs +289 -0
- package/scripts/vault-backfill.mjs +404 -0
- package/scripts/vault-consolidate.mjs +596 -0
- package/scripts/vault-integration-watcher.mjs +394 -0
- package/scripts/vault-mirror.mjs +430 -0
- package/skills/_shared/bootstrap-gate.md +111 -0
- package/skills/_shared/config-reading.md +226 -0
- package/skills/_shared/instruction-file-resolution.md +79 -0
- package/skills/_shared/model-selection.md +64 -0
- package/skills/_shared/monitor-patterns.md +300 -0
- package/skills/_shared/parallel-aware-auq.md +121 -0
- package/skills/_shared/parallel-aware-preamble.md +185 -0
- package/skills/_shared/platform-tools.md +96 -0
- package/skills/_shared/state-ownership.md +221 -0
- package/skills/architecture/DEEPENING.md +37 -0
- package/skills/architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/architecture/LANGUAGE.md +53 -0
- package/skills/architecture/SKILL.md +92 -0
- package/skills/autopilot/SKILL.md +419 -0
- package/skills/bootstrap/SKILL.md +592 -0
- package/skills/bootstrap/STATE.md.template +24 -0
- package/skills/bootstrap/_shared-template.md +243 -0
- package/skills/bootstrap/deep-template.md +659 -0
- package/skills/bootstrap/fast-template.md +251 -0
- package/skills/bootstrap/intensity-heuristic.md +80 -0
- package/skills/bootstrap/public-fallback.md +342 -0
- package/skills/bootstrap/standard-template.md +736 -0
- package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
- package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
- package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
- package/skills/brainstorm/SKILL.md +268 -0
- package/skills/brainstorm/soul.md +49 -0
- package/skills/claude-md-drift-check/SKILL.md +186 -0
- package/skills/claude-md-drift-check/checker.mjs +1380 -0
- package/skills/claude-md-drift-check/checker.sh +37 -0
- package/skills/claude-md-drift-check/package.json +16 -0
- package/skills/convergence-monitoring/README.md +39 -0
- package/skills/convergence-monitoring/SIGNALS.md +246 -0
- package/skills/convergence-monitoring/SKILL.md +285 -0
- package/skills/daily/SKILL.md +222 -0
- package/skills/daily/generate.sh +92 -0
- package/skills/daily/templates/daily.md.tpl +36 -0
- package/skills/debug/SKILL.md +188 -0
- package/skills/debug/soul.md +35 -0
- package/skills/discovery/SKILL.md +567 -0
- package/skills/discovery/issue-templates.md +237 -0
- package/skills/discovery/probes/docs-staleness.mjs +195 -0
- package/skills/discovery/probes/frontend-slop.mjs +186 -0
- package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
- package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
- package/skills/discovery/probes/vault-staleness.mjs +272 -0
- package/skills/discovery/probes-arch.md +252 -0
- package/skills/discovery/probes-audit.md +95 -0
- package/skills/discovery/probes-code.md +329 -0
- package/skills/discovery/probes-docs.md +76 -0
- package/skills/discovery/probes-feature.md +150 -0
- package/skills/discovery/probes-infra.md +138 -0
- package/skills/discovery/probes-intro.md +25 -0
- package/skills/discovery/probes-session.md +495 -0
- package/skills/discovery/probes-supply-chain.md +94 -0
- package/skills/discovery/probes-ui.md +147 -0
- package/skills/discovery/probes-vault.md +64 -0
- package/skills/discovery/slop-patterns.md +115 -0
- package/skills/dispatcher/SKILL.md +173 -0
- package/skills/docs-orchestrator/SKILL.md +362 -0
- package/skills/docs-orchestrator/audience-mapping.md +140 -0
- package/skills/domain-model/ADR-FORMAT.md +47 -0
- package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
- package/skills/domain-model/SKILL.md +85 -0
- package/skills/ecosystem-health/SKILL.md +119 -0
- package/skills/ecosystem-health/wizard.md +193 -0
- package/skills/eval/SKILL.md +293 -0
- package/skills/eval/rubric-v1.md +218 -0
- package/skills/evolve/SKILL.md +546 -0
- package/skills/frontmatter-guard/SKILL.md +126 -0
- package/skills/gitlab-ops/SKILL.md +368 -0
- package/skills/gitlab-portfolio/SKILL.md +196 -0
- package/skills/grill/SKILL.md +185 -0
- package/skills/grill/soul.md +55 -0
- package/skills/hook-development/SKILL.md +413 -0
- package/skills/mcp-builder/SKILL.md +260 -0
- package/skills/memory-cleanup/SKILL.md +310 -0
- package/skills/mode-selector/SKILL.md +226 -0
- package/skills/peekaboo-driver/SKILL.md +237 -0
- package/skills/peekaboo-driver/soul.md +32 -0
- package/skills/persona-panel/SKILL.md +365 -0
- package/skills/persona-panel/persona-format.md +205 -0
- package/skills/persona-panel/presets/designer-lens.md +87 -0
- package/skills/persona-panel/presets/engineer-lens.md +88 -0
- package/skills/persona-panel/presets/pm-lens.md +86 -0
- package/skills/plan/SKILL.md +496 -0
- package/skills/plan/mode-feature.md +141 -0
- package/skills/plan/mode-new.md +297 -0
- package/skills/plan/mode-retro.md +271 -0
- package/skills/plan/prd-feature-template.md +132 -0
- package/skills/plan/prd-full-template.md +151 -0
- package/skills/plan/prd-reviewer-prompt.md +103 -0
- package/skills/plan/retro-template.md +75 -0
- package/skills/plan/soul.md +62 -0
- package/skills/playwright-driver/SKILL.md +226 -0
- package/skills/playwright-driver/soul.md +30 -0
- package/skills/quality-gates/SKILL.md +212 -0
- package/skills/reconcile/SKILL.md +324 -0
- package/skills/repo-audit/SKILL.md +272 -0
- package/skills/session-end/SKILL.md +1044 -0
- package/skills/session-end/discovery-scan.md +37 -0
- package/skills/session-end/drift-operations.md +97 -0
- package/skills/session-end/learning-patterns.md +78 -0
- package/skills/session-end/metrics-collection.md +175 -0
- package/skills/session-end/phase-3-2-docs-verification.md +148 -0
- package/skills/session-end/phase-3-6-tail.md +344 -0
- package/skills/session-end/phase-3-7a-recommendations.md +86 -0
- package/skills/session-end/plan-verification.md +288 -0
- package/skills/session-end/session-metrics-write.md +223 -0
- package/skills/session-end/vault-operations.md +50 -0
- package/skills/session-end/verification-checklist.md +20 -0
- package/skills/session-plan/SKILL.md +554 -0
- package/skills/session-plan/wave-template.md +37 -0
- package/skills/session-start/SKILL.md +1043 -0
- package/skills/session-start/phase-2-5-docs-planning.md +119 -0
- package/skills/session-start/phase-4-5-resource-health.md +49 -0
- package/skills/session-start/phase-7-1-premise-check.md +47 -0
- package/skills/session-start/phase-7-5-mode-selector.md +237 -0
- package/skills/session-start/phase-8-5-express-path.md +61 -0
- package/skills/session-start/presentation-format.md +81 -0
- package/skills/session-start/soul.md +57 -0
- package/skills/skill-creator/SKILL.md +168 -0
- package/skills/spinout/SKILL.md +76 -0
- package/skills/sunset-review/SKILL.md +96 -0
- package/skills/test-runner/SKILL.md +362 -0
- package/skills/test-runner/rubric-v1.md +388 -0
- package/skills/test-runner/soul.md +46 -0
- package/skills/tmux-layout/SKILL.md +104 -0
- package/skills/ubiquitous-language/SKILL.md +97 -0
- package/skills/using-orchestrator/SKILL.md +144 -0
- package/skills/vault-mirror/SKILL.md +234 -0
- package/skills/vault-sync/SKILL.md +319 -0
- package/skills/vault-sync/package-lock.json +40 -0
- package/skills/vault-sync/package.json +11 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
- package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
- package/skills/vault-sync/validator.mjs +658 -0
- package/skills/vault-sync/validator.sh +55 -0
- package/skills/wave-executor/SKILL.md +496 -0
- package/skills/wave-executor/circuit-breaker.md +169 -0
- package/skills/wave-executor/wave-loop.md +1043 -0
- package/skills/write-executable-plan/SKILL.md +237 -0
- package/skills/write-executable-plan/plan-template.md +154 -0
- package/templates/_minimal/CLAUDE.md.tmpl +41 -0
- package/templates/_minimal/README.md.tmpl +15 -0
- package/templates/_minimal/gitignore.tmpl +47 -0
- package/templates/_shared/harte-regeln.md +16 -0
- package/templates/_shared/loop.md +90 -0
- package/templates/_shared/rules/parallel-sessions.md +77 -0
- package/templates/nextjs-minimal/README.md +30 -0
- package/templates/nextjs-minimal/app/layout.tsx +18 -0
- package/templates/nextjs-minimal/app/page.tsx +7 -0
- package/templates/nextjs-minimal/eslint.config.mjs +16 -0
- package/templates/nextjs-minimal/next.config.mjs +4 -0
- package/templates/nextjs-minimal/package.json +27 -0
- package/templates/nextjs-minimal/tsconfig.json +23 -0
- package/templates/node-minimal/README.md +33 -0
- package/templates/node-minimal/eslint.config.mjs +10 -0
- package/templates/node-minimal/package.json +21 -0
- package/templates/node-minimal/src/index.ts +1 -0
- package/templates/node-minimal/tests/sanity.test.ts +5 -0
- package/templates/node-minimal/tsconfig.json +17 -0
- package/templates/personas/README.md +150 -0
- package/templates/personas/accounting-compliance.v1.md +120 -0
- package/templates/personas/accounting-tax-advisor.v1.md +116 -0
- package/templates/personas/buyer-p1-cto.v1.md +125 -0
- package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
- package/templates/personas/buyer-p3-build.v1.md +130 -0
- package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
- package/templates/personas/buyer-p5-solo.v1.md +132 -0
- package/templates/personas/buyer-p6-ld.v1.md +130 -0
- package/templates/personas/klima-ai-expert.v1.md +114 -0
- package/templates/personas/klima-physicist.v1.md +117 -0
- package/templates/python-uv/README.md +28 -0
- package/templates/python-uv/pyproject.toml +32 -0
- package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
- package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
- package/templates/python-uv/tests/test_sanity.py +2 -0
- package/templates/static-html/README.md +19 -0
- package/templates/static-html/index.html +15 -0
- package/templates/static-html/script.js +1 -0
- package/templates/static-html/styles.css +26 -0
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-writer.mjs — Render + idempotently write the vault live-status board.
|
|
3
|
+
*
|
|
4
|
+
* Epic #673 Phase 1 (issue #674). Writes a host-local board at
|
|
5
|
+
* `<vault-dir>/01-projects/_active-sessions.md` listing, per repo, one row:
|
|
6
|
+
* status ∈ {frei, in-progress, closed, force-closed}
|
|
7
|
+
* semantic-session-id · branch · mode · last-heartbeat
|
|
8
|
+
*
|
|
9
|
+
* Source of truth: the per-repo `session.lock` v2 lease (heartbeat-based
|
|
10
|
+
* liveness via {@link isLockLive}) plus the host-wide session registry (the
|
|
11
|
+
* registry entry is the ONLY source of `branch` — the lock lacks that field).
|
|
12
|
+
*
|
|
13
|
+
* Exports:
|
|
14
|
+
* GENERATOR_MARKER — frontmatter sentinel that identifies generator-owned files
|
|
15
|
+
* resolveBoardPath — vaultDir → `<vaultDir>/01-projects/_active-sessions.md`
|
|
16
|
+
* collectRows — per-repo status derivation (readLock + readRegistry)
|
|
17
|
+
* renderBoard — pure render: rows[] → full markdown (frontmatter + table)
|
|
18
|
+
* normalizeUpdated — stabilise the `updated:` line for byte-equality noop compare
|
|
19
|
+
* writeBoard — idempotent write with skip-handwritten / skip-noop / dry-run
|
|
20
|
+
* mirrorBoard — thin convenience: config-read + resolve + write (no-ops when vault off)
|
|
21
|
+
* buildSweepRepos — pure helper: Candidate[] → sweep repo descriptors (busy ∪ thisRepo)
|
|
22
|
+
* sweepBoard — host-wide sweep: enumerateCandidates + mirrorBoard (issue #716)
|
|
23
|
+
*
|
|
24
|
+
* Idempotent merge: writeBoard's caller passes a fully-rendered board, but
|
|
25
|
+
* {@link collectRows} preserves rows for repos NOT in the current update by
|
|
26
|
+
* reading the EXISTING generator-owned board first, so repeated writes are stable.
|
|
27
|
+
* PRESERVED `in-progress` rows are additionally re-derived for TTL staleness
|
|
28
|
+
* (issue #829 Finding 2) inside {@link mirrorBoard} — a preserved row whose
|
|
29
|
+
* heartbeat has aged past {@link DEFAULT_TTL_HOURS} flips to `force-closed`
|
|
30
|
+
* instead of being copied forward unboundedly.
|
|
31
|
+
*
|
|
32
|
+
* CRITICAL SAFETY (Epic #673 #1 risk — never clobber hand-authored vault notes):
|
|
33
|
+
* 1. The `_generator` marker guard refuses any file we did not author.
|
|
34
|
+
* 2. Defense-in-depth: writeBoard hard-refuses to touch `_overview.md`
|
|
35
|
+
* (sven-owned, must NEVER be written by this generator).
|
|
36
|
+
*
|
|
37
|
+
* No console noise — library code. Plain Node ESM. No external deps.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
41
|
+
import os from 'node:os';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
|
|
44
|
+
import { isLockLive, readLock, DEFAULT_TTL_HOURS } from '../session-lock.mjs';
|
|
45
|
+
import { readRegistry, repoPathHash, isRegistryEntryFresh } from '../session-registry.mjs';
|
|
46
|
+
import { parseFrontmatter } from '../vault-mirror/utils.mjs';
|
|
47
|
+
import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
48
|
+
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
49
|
+
import { enumerateCandidates } from '../dispatcher/enumerate.mjs';
|
|
50
|
+
|
|
51
|
+
/** Frontmatter sentinel that identifies generator-owned board files. */
|
|
52
|
+
export const GENERATOR_MARKER = 'session-orchestrator-active-sessions@1';
|
|
53
|
+
|
|
54
|
+
/** Placeholder used for noop comparison (replaces the live `updated:` value). */
|
|
55
|
+
const UPDATED_PLACEHOLDER = '__UPDATED_PLACEHOLDER__';
|
|
56
|
+
|
|
57
|
+
/** The four board statuses, in display priority order. */
|
|
58
|
+
const STATUS_IN_PROGRESS = 'in-progress';
|
|
59
|
+
const STATUS_FORCE_CLOSED = 'force-closed';
|
|
60
|
+
const STATUS_CLOSED = 'closed';
|
|
61
|
+
const STATUS_FREI = 'frei';
|
|
62
|
+
|
|
63
|
+
// ── Key normalization ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fold a repo-name key to a case-insensitive form for merge/compare purposes
|
|
67
|
+
* (issue #719). On case-insensitive-preserving filesystems (APFS, the default
|
|
68
|
+
* for macOS Home volumes), `some-repo` and `Some-Repo` are the SAME
|
|
69
|
+
* physical directory — every site that keys rows by `repoName` (prior-status
|
|
70
|
+
* lookup, preserved-row map, merge upsert) must fold through this helper so
|
|
71
|
+
* the two casings collapse to one board row instead of rendering as
|
|
72
|
+
* duplicates. Row OBJECTS are left untouched — {@link renderBoard} still
|
|
73
|
+
* displays the row's original `repo` string (true on-disk casing); only the
|
|
74
|
+
* MAP KEY is folded.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} s
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
const foldKey = (s) => String(s ?? '').toLowerCase();
|
|
80
|
+
|
|
81
|
+
// ── Path helpers ────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Expand a leading `~` to the current user's home directory. Inlined here on
|
|
85
|
+
* purpose — the shared helper is private elsewhere, and a shared
|
|
86
|
+
* `vault-write-guard.mjs` extraction is deferred to a later epic (W2 forbids a
|
|
87
|
+
* new shared file in this slice).
|
|
88
|
+
*
|
|
89
|
+
* @param {string} p
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function expandHome(p) {
|
|
93
|
+
if (typeof p !== 'string' || p.length === 0) return p;
|
|
94
|
+
if (p === '~') return os.homedir();
|
|
95
|
+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
|
96
|
+
return p;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the board file path from a vault directory.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} vaultDir — absolute or `~`-prefixed vault root
|
|
103
|
+
* @returns {string} `<vaultDir>/01-projects/_active-sessions.md`
|
|
104
|
+
*/
|
|
105
|
+
export function resolveBoardPath(vaultDir) {
|
|
106
|
+
return path.join(expandHome(vaultDir), '01-projects', '_active-sessions.md');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Formatting helpers ───────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format a Date as ISO 8601 (for frontmatter fields).
|
|
113
|
+
* @param {Date} date
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function toIso(date) {
|
|
117
|
+
return date.toISOString();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Format a last-heartbeat ISO string for table display. Returns '—' if absent.
|
|
122
|
+
* Kept verbatim (the ISO string) so operators can diff against lock files.
|
|
123
|
+
*
|
|
124
|
+
* @param {string|null|undefined} iso
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function fmtHeartbeat(iso) {
|
|
128
|
+
if (!iso) return '—';
|
|
129
|
+
return String(iso);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render a cell value, substituting '—' for empty/absent values and escaping
|
|
134
|
+
* the pipe character so a stray `|` cannot break the markdown table.
|
|
135
|
+
*
|
|
136
|
+
* @param {string|null|undefined} value
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
function cell(value) {
|
|
140
|
+
if (value === null || value === undefined || value === '') return '—';
|
|
141
|
+
return String(value).replace(/\|/g, '\\|');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Row collection (status derivation) ───────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build the rows array from each repo's session.lock + the host-wide registry.
|
|
148
|
+
*
|
|
149
|
+
* Status derivation (this module derives ALL four states itself — no existing
|
|
150
|
+
* code does):
|
|
151
|
+
* - in-progress — readLock non-null AND isLockLive(lock) === true.
|
|
152
|
+
* - force-closed — readLock non-null AND isLockLive(lock) === false (dead
|
|
153
|
+
* lease: heartbeat older than ttl). Fields are read straight
|
|
154
|
+
* off the raw lock — the dead lock is never silently dropped.
|
|
155
|
+
* - closed — explicit per-repo `status: 'closed'` override (session-end
|
|
156
|
+
* passes this for the current repo), OR a prior generator-owned
|
|
157
|
+
* board row was in-progress/force-closed and there is now no lock.
|
|
158
|
+
* - frei — no lock AND no fresh registry entry AND not previously in-progress.
|
|
159
|
+
*
|
|
160
|
+
* branch/mode/semantic-session-id: prefer the lock's, then the matching registry
|
|
161
|
+
* entry. `branch` ONLY exists on the registry entry (the lock has no branch).
|
|
162
|
+
* semantic id = `semantic_session_id ?? session_id`.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} opts
|
|
165
|
+
* @param {Array<{ repoRoot: string, repoName?: string, status?: string }>} opts.repos
|
|
166
|
+
* Repos to compute rows for. `repoRoot` is an absolute path; `repoName`
|
|
167
|
+
* defaults to its basename; optional `status: 'closed'` forces a closed row.
|
|
168
|
+
* @param {Date} [opts.now] — clock seam (defaults to new Date()).
|
|
169
|
+
* @param {Array<object>} [opts.registry] — pre-read registry (test seam); defaults
|
|
170
|
+
* to a fresh {@link readRegistry} call.
|
|
171
|
+
* @param {Map<string, string>} [opts.priorStatusByRepo] — {@link foldKey}-folded
|
|
172
|
+
* (case-insensitive) repoName → prior board status, used to derive `closed`
|
|
173
|
+
* when a once-active repo now has no lock. Callers MUST fold the key with
|
|
174
|
+
* {@link foldKey} before inserting (issue #719) — this function folds its
|
|
175
|
+
* own lookup key to match.
|
|
176
|
+
* @returns {Promise<Array<{ repo: string, status: string, session: string|null,
|
|
177
|
+
* branch: string|null, mode: string|null, heartbeat: string|null }>>}
|
|
178
|
+
*/
|
|
179
|
+
export async function collectRows({ repos, now = new Date(), registry, priorStatusByRepo } = {}) {
|
|
180
|
+
if (!Array.isArray(repos)) {
|
|
181
|
+
throw new TypeError('collectRows: opts.repos must be an array');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const nowMs = now instanceof Date ? now.getTime() : Date.now();
|
|
185
|
+
const registryEntries = Array.isArray(registry) ? registry : await readRegistry();
|
|
186
|
+
const priorStatus = priorStatusByRepo instanceof Map ? priorStatusByRepo : new Map();
|
|
187
|
+
|
|
188
|
+
const rows = [];
|
|
189
|
+
|
|
190
|
+
for (const repo of repos) {
|
|
191
|
+
if (!repo || typeof repo.repoRoot !== 'string' || repo.repoRoot.length === 0) {
|
|
192
|
+
// Skip malformed repo descriptors rather than throwing — one bad entry
|
|
193
|
+
// must not abort the whole board render.
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const repoName = typeof repo.repoName === 'string' && repo.repoName.length > 0
|
|
198
|
+
? repo.repoName
|
|
199
|
+
: path.basename(path.resolve(repo.repoRoot));
|
|
200
|
+
|
|
201
|
+
const lock = readLock({ repoRoot: repo.repoRoot });
|
|
202
|
+
|
|
203
|
+
// Match the registry entry for this repo by path hash (branch lives here only).
|
|
204
|
+
let registryEntry;
|
|
205
|
+
try {
|
|
206
|
+
const hash = repoPathHash(repo.repoRoot);
|
|
207
|
+
registryEntry = registryEntries.find((e) => e && e.repo_path_hash === hash) ?? null;
|
|
208
|
+
} catch {
|
|
209
|
+
registryEntry = null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Field resolution: prefer the lock, then the registry entry.
|
|
213
|
+
const semanticFromLock = lock
|
|
214
|
+
? (lock.semantic_session_id ?? lock.session_id ?? null)
|
|
215
|
+
: null;
|
|
216
|
+
const semanticFromRegistry = registryEntry
|
|
217
|
+
? (registryEntry.semantic_session_id ?? registryEntry.session_id ?? null)
|
|
218
|
+
: null;
|
|
219
|
+
const session = semanticFromLock ?? semanticFromRegistry ?? null;
|
|
220
|
+
|
|
221
|
+
// branch ONLY exists on the registry entry — the lock has no branch field.
|
|
222
|
+
const branch = (registryEntry && registryEntry.branch) ? registryEntry.branch : null;
|
|
223
|
+
|
|
224
|
+
const mode = (lock && lock.mode)
|
|
225
|
+
? lock.mode
|
|
226
|
+
: (registryEntry && registryEntry.mode ? registryEntry.mode : null);
|
|
227
|
+
|
|
228
|
+
const heartbeat = lock
|
|
229
|
+
? (lock.last_heartbeat ?? lock.started_at ?? null)
|
|
230
|
+
: (registryEntry ? (registryEntry.last_heartbeat ?? null) : null);
|
|
231
|
+
|
|
232
|
+
// ── Status derivation ──
|
|
233
|
+
let status;
|
|
234
|
+
if (repo.status === STATUS_CLOSED) {
|
|
235
|
+
// Explicit per-repo override (session-end passes 'closed' for current repo).
|
|
236
|
+
status = STATUS_CLOSED;
|
|
237
|
+
} else if (lock && isLockLive(lock, nowMs)) {
|
|
238
|
+
status = STATUS_IN_PROGRESS;
|
|
239
|
+
} else if (lock) {
|
|
240
|
+
// Lock present but dead lease (heartbeat older than ttl).
|
|
241
|
+
status = STATUS_FORCE_CLOSED;
|
|
242
|
+
} else {
|
|
243
|
+
// No live lock. Derive status from the prior board state + registry freshness.
|
|
244
|
+
// Key is folded (issue #719) so `Some-Repo` and `some-repo` resolve
|
|
245
|
+
// to the same prior-status entry on case-insensitive-preserving filesystems.
|
|
246
|
+
const prior = priorStatus.get(foldKey(repoName));
|
|
247
|
+
if (prior === STATUS_CLOSED || prior === STATUS_FORCE_CLOSED) {
|
|
248
|
+
// Terminal prior state is STICKY absent a live lock. A still-fresh registry
|
|
249
|
+
// entry must NOT resurrect a cleanly-closed (or force-closed) repo to
|
|
250
|
+
// in-progress — only a real lock (handled above) re-asserts in-progress.
|
|
251
|
+
// Without this, a second bare refresh within the registry freshness window
|
|
252
|
+
// (default 15min) of a clean close would wrongly flip `closed` → `in-progress`
|
|
253
|
+
// (#674 review finding).
|
|
254
|
+
status = prior;
|
|
255
|
+
} else if (prior === STATUS_IN_PROGRESS) {
|
|
256
|
+
// Was live, lock now gone → the session ended → closed.
|
|
257
|
+
status = STATUS_CLOSED;
|
|
258
|
+
} else if (registryEntry && isRegistryEntryFresh(registryEntry, { now: nowMs })) {
|
|
259
|
+
// Never-seen repo with a fresh registry heartbeat but the lock momentarily
|
|
260
|
+
// absent — treat as in-progress (the session is heartbeating the registry).
|
|
261
|
+
status = STATUS_IN_PROGRESS;
|
|
262
|
+
} else {
|
|
263
|
+
status = STATUS_FREI;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
rows.push({
|
|
268
|
+
repo: repoName,
|
|
269
|
+
status,
|
|
270
|
+
session: status === STATUS_FREI ? null : session,
|
|
271
|
+
branch: status === STATUS_FREI ? null : branch,
|
|
272
|
+
mode: status === STATUS_FREI ? null : mode,
|
|
273
|
+
heartbeat: status === STATUS_FREI ? null : heartbeat,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return rows;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Render (pure) ────────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Render the board markdown from the rows array.
|
|
284
|
+
*
|
|
285
|
+
* Rows are sorted alphabetically by repo name for stable, diff-friendly output.
|
|
286
|
+
*
|
|
287
|
+
* @param {Array<{ repo: string, status: string, session?: string|null,
|
|
288
|
+
* branch?: string|null, mode?: string|null, heartbeat?: string|null }>} rows
|
|
289
|
+
* @param {{ now: Date, createdIso?: string, updatedPlaceholder?: string }} opts
|
|
290
|
+
* @returns {string} full markdown (frontmatter + table)
|
|
291
|
+
*/
|
|
292
|
+
export function renderBoard(rows, opts = {}) {
|
|
293
|
+
const { now, createdIso, updatedPlaceholder } = opts;
|
|
294
|
+
const nowDate = now instanceof Date ? now : new Date();
|
|
295
|
+
|
|
296
|
+
const nowIso = toIso(nowDate);
|
|
297
|
+
const updatedValue = updatedPlaceholder ?? nowIso;
|
|
298
|
+
const createdValue = createdIso ?? nowIso;
|
|
299
|
+
|
|
300
|
+
const sortedRows = [...(Array.isArray(rows) ? rows : [])].sort((a, b) =>
|
|
301
|
+
String(a?.repo ?? '').localeCompare(String(b?.repo ?? '')),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const lines = [];
|
|
305
|
+
|
|
306
|
+
// Frontmatter
|
|
307
|
+
lines.push('---');
|
|
308
|
+
lines.push(`_generator: ${GENERATOR_MARKER}`);
|
|
309
|
+
lines.push('id: active-sessions');
|
|
310
|
+
lines.push('type: board');
|
|
311
|
+
lines.push(`created: ${createdValue}`);
|
|
312
|
+
lines.push(`updated: ${updatedValue}`);
|
|
313
|
+
lines.push('---');
|
|
314
|
+
lines.push('');
|
|
315
|
+
|
|
316
|
+
// Title + preamble
|
|
317
|
+
lines.push('# Active Sessions');
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('> Live session-status board. Generator-owned — do not hand-edit.');
|
|
320
|
+
lines.push('');
|
|
321
|
+
|
|
322
|
+
// Board table
|
|
323
|
+
lines.push('| Repo | Status | Session | Branch | Mode | Last heartbeat |');
|
|
324
|
+
lines.push('|---|---|---|---|---|---|');
|
|
325
|
+
for (const row of sortedRows) {
|
|
326
|
+
lines.push(
|
|
327
|
+
`| ${cell(row?.repo)} | ${cell(row?.status)} | ${cell(row?.session)} | ` +
|
|
328
|
+
`${cell(row?.branch)} | ${cell(row?.mode)} | ${cell(fmtHeartbeat(row?.heartbeat))} |`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
lines.push('');
|
|
332
|
+
|
|
333
|
+
return lines.join('\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Write (idempotent) ───────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Normalize a markdown string by replacing the `updated:` frontmatter line with
|
|
340
|
+
* a stable placeholder, enabling byte-for-byte noop comparison.
|
|
341
|
+
*
|
|
342
|
+
* @param {string} content
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
export function normalizeUpdated(content) {
|
|
346
|
+
return String(content ?? '').replace(/^(updated:\s*)(.+)$/m, `$1${UPDATED_PLACEHOLDER}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Parse a generator-owned board's table back into row objects. Used by the
|
|
351
|
+
* idempotent-merge path in {@link mirrorBoard} to recover prior per-repo rows
|
|
352
|
+
* (status carry-over + row preservation for repos not in the current update).
|
|
353
|
+
*
|
|
354
|
+
* Tolerant by design: skips the header + separator rows, ignores any line that
|
|
355
|
+
* is not a 6-column table row, and maps the literal '—' placeholder back to
|
|
356
|
+
* null. Unescapes the `\|` pipe-escaping applied by {@link renderBoard}.
|
|
357
|
+
*
|
|
358
|
+
* @param {string} content — full board markdown
|
|
359
|
+
* @returns {Array<{ repo: string, status: string, session: string|null,
|
|
360
|
+
* branch: string|null, mode: string|null, heartbeat: string|null }>}
|
|
361
|
+
*/
|
|
362
|
+
export function parseBoardRows(content) {
|
|
363
|
+
const rows = [];
|
|
364
|
+
const text = String(content ?? '');
|
|
365
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
366
|
+
const line = rawLine.trim();
|
|
367
|
+
if (!line.startsWith('|')) continue;
|
|
368
|
+
// Split on unescaped pipes, drop the leading/trailing empty fields.
|
|
369
|
+
const cells = line
|
|
370
|
+
.split(/(?<!\\)\|/)
|
|
371
|
+
.slice(1, -1)
|
|
372
|
+
.map((c) => c.trim());
|
|
373
|
+
if (cells.length !== 6) continue;
|
|
374
|
+
// Skip the header row and the |---|---| separator row.
|
|
375
|
+
if (cells[0] === 'Repo' || /^-+$/.test(cells[0])) continue;
|
|
376
|
+
const unesc = (v) => (v === '—' ? null : v.replace(/\\\|/g, '|'));
|
|
377
|
+
const repo = unesc(cells[0]);
|
|
378
|
+
if (repo === null) continue;
|
|
379
|
+
rows.push({
|
|
380
|
+
repo,
|
|
381
|
+
status: cells[1],
|
|
382
|
+
session: unesc(cells[2]),
|
|
383
|
+
branch: unesc(cells[3]),
|
|
384
|
+
mode: unesc(cells[4]),
|
|
385
|
+
heartbeat: unesc(cells[5]),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return rows;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Write the board file with idempotency + safety guards.
|
|
393
|
+
*
|
|
394
|
+
* Guard order:
|
|
395
|
+
* 1. dryRun → { action: 'dry-run' } (never touches disk).
|
|
396
|
+
* 2. SAFETY (Epic #673 #1 risk): basename === '_overview.md' →
|
|
397
|
+
* { action: 'skipped-handwritten' }. `_overview.md` is sven-owned and must
|
|
398
|
+
* NEVER be written by this generator, regardless of any marker.
|
|
399
|
+
* 3. file exists → parseFrontmatter:
|
|
400
|
+
* - !fm || !fm._generator → skipped-handwritten
|
|
401
|
+
* - fm._generator !== GENERATOR_MARKER → skipped-handwritten
|
|
402
|
+
* - normalizeUpdated(existing) === new → skipped-noop
|
|
403
|
+
* 4. else → mkdirSync(recursive) + writeFileSync → written.
|
|
404
|
+
*
|
|
405
|
+
* @param {{
|
|
406
|
+
* outputPath: string,
|
|
407
|
+
* content: string,
|
|
408
|
+
* dryRun?: boolean,
|
|
409
|
+
* fs?: { readFileSync?: Function, writeFileSync?: Function, mkdirSync?: Function, existsSync?: Function },
|
|
410
|
+
* }} opts
|
|
411
|
+
* @returns {{ action: 'written'|'skipped-handwritten'|'skipped-noop'|'dry-run', path: string }}
|
|
412
|
+
*/
|
|
413
|
+
export function writeBoard(opts) {
|
|
414
|
+
const { outputPath, content, dryRun = false, fs: injectedFs } = opts;
|
|
415
|
+
|
|
416
|
+
const fsReadFile = injectedFs?.readFileSync ?? readFileSync;
|
|
417
|
+
const fsWriteFile = injectedFs?.writeFileSync ?? writeFileSync;
|
|
418
|
+
const fsMkdir = injectedFs?.mkdirSync ?? mkdirSync;
|
|
419
|
+
const fsExists = injectedFs?.existsSync ?? existsSync;
|
|
420
|
+
|
|
421
|
+
// 1. Dry-run: never write.
|
|
422
|
+
if (dryRun) {
|
|
423
|
+
return { action: 'dry-run', path: outputPath };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 2. SAFETY (Epic #673 #1 risk): _overview.md is sven-owned — NEVER write it.
|
|
427
|
+
if (path.basename(outputPath) === '_overview.md') {
|
|
428
|
+
return { action: 'skipped-handwritten', path: outputPath };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 3. Existing-file guards.
|
|
432
|
+
if (fsExists(outputPath)) {
|
|
433
|
+
let existingContent;
|
|
434
|
+
try {
|
|
435
|
+
existingContent = fsReadFile(outputPath, 'utf8');
|
|
436
|
+
} catch {
|
|
437
|
+
existingContent = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (existingContent !== null && existingContent !== undefined) {
|
|
441
|
+
const fm = parseFrontmatter(existingContent);
|
|
442
|
+
|
|
443
|
+
// Skip-on-manual-edit: no _generator, or a different generator.
|
|
444
|
+
if (!fm || !fm['_generator']) {
|
|
445
|
+
return { action: 'skipped-handwritten', path: outputPath };
|
|
446
|
+
}
|
|
447
|
+
if (fm['_generator'] !== GENERATOR_MARKER) {
|
|
448
|
+
return { action: 'skipped-handwritten', path: outputPath };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Skip-noop: identical modulo the live `updated:` timestamp.
|
|
452
|
+
if (normalizeUpdated(existingContent) === normalizeUpdated(content)) {
|
|
453
|
+
return { action: 'skipped-noop', path: outputPath };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 4. Write.
|
|
459
|
+
fsMkdir(path.dirname(outputPath), { recursive: true });
|
|
460
|
+
fsWriteFile(outputPath, content, 'utf8');
|
|
461
|
+
return { action: 'written', path: outputPath };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── Convenience: config-read + resolve + write ───────────────────────────────────
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Thin convenience that reads Session Config, resolves the vault board path,
|
|
468
|
+
* collects rows, renders, and writes. Silently no-ops when vault-integration is
|
|
469
|
+
* disabled or vault-dir is absent — returns `{ action: 'skipped-vault-disabled' }`.
|
|
470
|
+
*
|
|
471
|
+
* The caller (session-start / session-end) typically owns the higher-level
|
|
472
|
+
* decision and may call {@link collectRows} + {@link renderBoard} + {@link writeBoard}
|
|
473
|
+
* directly; this helper exists for the common single-repo update path.
|
|
474
|
+
*
|
|
475
|
+
* Vault path assertion (Epic #673 safety): the resolved vault dir MUST live
|
|
476
|
+
* under $HOME — a vault outside the home tree is refused as `skipped-vault-disabled`
|
|
477
|
+
* so a misconfigured path can never drive a write into an arbitrary location.
|
|
478
|
+
*
|
|
479
|
+
* @param {object} opts
|
|
480
|
+
* @param {string} opts.repoRoot — the repo whose row is being updated.
|
|
481
|
+
* @param {Array<{ repoRoot: string, repoName?: string, status?: string }>} [opts.repos]
|
|
482
|
+
* Full repo list; defaults to `[{ repoRoot, status: explicitStatus }]`.
|
|
483
|
+
* @param {string} [opts.explicitStatus] — per-repo status override ('closed' from session-end).
|
|
484
|
+
* @param {Date} [opts.now]
|
|
485
|
+
* @param {boolean} [opts.dryRun]
|
|
486
|
+
* @param {object} [opts.fs] — injectable fs for tests.
|
|
487
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
|
|
488
|
+
* — forwarded verbatim to {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653).
|
|
489
|
+
* Tests MUST pass a hermetic ctx (e.g. `{ env: {}, ownerConfig: undefined }`) when
|
|
490
|
+
* asserting a fixture's committed `vault-dir` — omitting it reads the REAL host
|
|
491
|
+
* `owner.yaml`, whose `paths.vault-dir` override (if set) wins over the fixture value
|
|
492
|
+
* and bleeds into the assertion (issue #783). Production callers omit this — the
|
|
493
|
+
* default (real owner.yaml resolution) is the correct host-local behavior there.
|
|
494
|
+
* @returns {Promise<{ action: string, path?: string }>}
|
|
495
|
+
*/
|
|
496
|
+
export async function mirrorBoard({ repoRoot, repos, explicitStatus, now = new Date(), dryRun = false, fs, hostPaths } = {}) {
|
|
497
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
|
|
498
|
+
return { action: 'skipped-vault-disabled' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Read + parse Session Config. Any failure → silent no-op.
|
|
502
|
+
let config;
|
|
503
|
+
try {
|
|
504
|
+
const text = await readConfigFile(repoRoot);
|
|
505
|
+
config = parseSessionConfig(text, { hostPaths });
|
|
506
|
+
} catch {
|
|
507
|
+
return { action: 'skipped-vault-disabled' };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const vault = config?.['vault-integration'];
|
|
511
|
+
if (!vault || vault.enabled !== true) {
|
|
512
|
+
return { action: 'skipped-vault-disabled' };
|
|
513
|
+
}
|
|
514
|
+
const vaultDir = vault['vault-dir'];
|
|
515
|
+
if (typeof vaultDir !== 'string' || vaultDir.length === 0) {
|
|
516
|
+
return { action: 'skipped-vault-disabled' };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Safety: the resolved vault dir must live under $HOME.
|
|
520
|
+
const expandedVault = expandHome(vaultDir);
|
|
521
|
+
const home = os.homedir();
|
|
522
|
+
const inHome = validatePathInsideProject(expandedVault, home);
|
|
523
|
+
if (!inHome.ok) {
|
|
524
|
+
return { action: 'skipped-vault-disabled' };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const repoList = Array.isArray(repos) && repos.length > 0
|
|
528
|
+
? repos
|
|
529
|
+
: [{ repoRoot, status: explicitStatus }];
|
|
530
|
+
|
|
531
|
+
const outputPath = resolveBoardPath(vaultDir);
|
|
532
|
+
|
|
533
|
+
// Read the EXISTING generator-owned board (if any) to:
|
|
534
|
+
// 1. preserve its `created:` — otherwise every render differs on `created:`
|
|
535
|
+
// and the noop-skip in writeBoard would never fire.
|
|
536
|
+
// 2. recover the prior per-repo status — drives the `closed` derivation for
|
|
537
|
+
// repos NOT in this update (idempotent merge: their rows are re-derived).
|
|
538
|
+
// Both maps are keyed by {@link foldKey}(repo) — case-insensitively folded
|
|
539
|
+
// (issue #719) — so two prior rows differing only by case (e.g.
|
|
540
|
+
// `some-repo` vs `Some-Repo`, the same physical directory on a
|
|
541
|
+
// case-insensitive-preserving filesystem like APFS) collapse to ONE entry
|
|
542
|
+
// instead of coexisting as duplicates. The row OBJECTS keep their original
|
|
543
|
+
// `repo` string untouched, so `renderBoard` still displays true casing.
|
|
544
|
+
const fsReadFile = fs?.readFileSync ?? readFileSync;
|
|
545
|
+
const fsExists = fs?.existsSync ?? existsSync;
|
|
546
|
+
let createdIso;
|
|
547
|
+
const priorStatusByRepo = new Map();
|
|
548
|
+
const preservedRows = new Map(); // foldKey(repoName) → prior row (for merge)
|
|
549
|
+
if (fsExists(outputPath)) {
|
|
550
|
+
let existing;
|
|
551
|
+
try {
|
|
552
|
+
existing = fsReadFile(outputPath, 'utf8');
|
|
553
|
+
} catch {
|
|
554
|
+
existing = null;
|
|
555
|
+
}
|
|
556
|
+
if (existing) {
|
|
557
|
+
const fm = parseFrontmatter(existing);
|
|
558
|
+
if (fm && fm['_generator'] === GENERATOR_MARKER) {
|
|
559
|
+
if (fm['created']) createdIso = fm['created'];
|
|
560
|
+
for (const prior of parseBoardRows(existing)) {
|
|
561
|
+
const key = foldKey(prior.repo);
|
|
562
|
+
const collidingPrior = preservedRows.get(key);
|
|
563
|
+
if (collidingPrior) {
|
|
564
|
+
// Collision WITHIN parseBoardRows output — two prior rows fold to
|
|
565
|
+
// the same key with no fresh row in play yet (that upsert happens
|
|
566
|
+
// below). Prefer the row with the most-recent `heartbeat` rather
|
|
567
|
+
// than silently last-in-file-order. Guard: if either heartbeat is
|
|
568
|
+
// unparsable, fall through to last-written-wins (the pre-#719
|
|
569
|
+
// default) by NOT skipping the overwrite below.
|
|
570
|
+
const collidingTs = Date.parse(collidingPrior.heartbeat ?? '');
|
|
571
|
+
const priorTs = Date.parse(prior.heartbeat ?? '');
|
|
572
|
+
if (Number.isFinite(collidingTs) && Number.isFinite(priorTs) && collidingTs > priorTs) {
|
|
573
|
+
// The already-preserved row is strictly newer — keep it, skip
|
|
574
|
+
// this older colliding row entirely.
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
priorStatusByRepo.set(key, prior.status);
|
|
579
|
+
preservedRows.set(key, prior);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const rows = await collectRows({ repos: repoList, now, priorStatusByRepo });
|
|
586
|
+
|
|
587
|
+
// TTL-staleness re-derivation for PRESERVED rows (issue #829 Finding 2).
|
|
588
|
+
// Without this pass, a preserved `in-progress` row (a repo NOT in this
|
|
589
|
+
// update) is copied forward FOREVER — a crashed/never-closed session's row
|
|
590
|
+
// never flips even after its heartbeat has aged well past the lock's TTL,
|
|
591
|
+
// because `collectRows` only re-derives status for repos actually IN
|
|
592
|
+
// `repoList`. Re-derive staleness for every preserved row here, BEFORE the
|
|
593
|
+
// freshly-derived `rows` are upserted over it below (fresh data always
|
|
594
|
+
// wins regardless of this pass — a live lock or an explicit-closed update
|
|
595
|
+
// always takes precedence over the TTL flip).
|
|
596
|
+
//
|
|
597
|
+
// Board rows carry only a raw `heartbeat` string, never the lock's own
|
|
598
|
+
// `ttl_hours` (that field is not part of the rendered board) — so this
|
|
599
|
+
// reuses the shared {@link DEFAULT_TTL_HOURS} constant rather than the
|
|
600
|
+
// per-lock TTL {@link isLockLive} uses when a live lock object is in hand.
|
|
601
|
+
// Rows with an unparseable/absent heartbeat are left UNCHANGED (fail-open,
|
|
602
|
+
// never crash on a malformed prior board).
|
|
603
|
+
const nowMs = now instanceof Date ? now.getTime() : Date.now();
|
|
604
|
+
const ttlMs = DEFAULT_TTL_HOURS * 3600 * 1000;
|
|
605
|
+
const staleRederivedRows = new Map();
|
|
606
|
+
for (const [key, row] of preservedRows) {
|
|
607
|
+
if (row.status === STATUS_IN_PROGRESS) {
|
|
608
|
+
const heartbeatMs = Date.parse(row.heartbeat ?? '');
|
|
609
|
+
if (Number.isFinite(heartbeatMs) && (nowMs - heartbeatMs) >= ttlMs) {
|
|
610
|
+
staleRederivedRows.set(key, { ...row, status: STATUS_FORCE_CLOSED });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
staleRederivedRows.set(key, row);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Idempotent merge: keep prior (TTL-rederived) rows for repos NOT in this
|
|
618
|
+
// update, then upsert the freshly-derived rows over them so repeated
|
|
619
|
+
// writes stay stable. Both the seed (`staleRederivedRows`) and the upsert
|
|
620
|
+
// key below are folded via {@link foldKey} (issue #719) — a
|
|
621
|
+
// freshly-derived row ALWAYS wins over a preserved row sharing its folded
|
|
622
|
+
// key, which is what collapses a live `Some-Repo` row over a stale
|
|
623
|
+
// preserved `some-repo` row on the next board write.
|
|
624
|
+
const merged = new Map(staleRederivedRows);
|
|
625
|
+
for (const row of rows) merged.set(foldKey(row.repo), row);
|
|
626
|
+
|
|
627
|
+
const content = renderBoard([...merged.values()], { now, createdIso });
|
|
628
|
+
|
|
629
|
+
return writeBoard({ outputPath, content, dryRun, fs });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ── Host-wide sweep (issue #716) ─────────────────────────────────────────────────
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Pure helper: reduce an {@link enumerateCandidates} result down to the sweep
|
|
636
|
+
* repo descriptors {@link mirrorBoard} expects — every BUSY candidate
|
|
637
|
+
* (`free === false`, i.e. `in-progress` or `force-closed`), unioned with the
|
|
638
|
+
* calling repo (`thisRepoRoot`) so its own row is always re-derived even when
|
|
639
|
+
* `enumerateCandidates` did not surface it (e.g. `thisRepoRoot` sits outside
|
|
640
|
+
* `startDir`'s confinement root).
|
|
641
|
+
*
|
|
642
|
+
* Free (`frei`) candidates are intentionally EXCLUDED — re-deriving them would
|
|
643
|
+
* add board noise for repos with nothing to report; their prior rows (if any)
|
|
644
|
+
* are preserved by {@link mirrorBoard}'s idempotent merge.
|
|
645
|
+
*
|
|
646
|
+
* Pure — no fs access, no I/O. Dedupe is by `path.resolve()` so a candidate
|
|
647
|
+
* already covering `thisRepoRoot` is not duplicated.
|
|
648
|
+
*
|
|
649
|
+
* @param {import('../dispatcher/enumerate.mjs').Candidate[]} candidates
|
|
650
|
+
* @param {{ thisRepoRoot: string }} opts
|
|
651
|
+
* @returns {Array<{ repoRoot: string }>}
|
|
652
|
+
*/
|
|
653
|
+
export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
|
|
654
|
+
const list = Array.isArray(candidates) ? candidates : [];
|
|
655
|
+
const seen = new Set();
|
|
656
|
+
const repos = [];
|
|
657
|
+
|
|
658
|
+
for (const candidate of list) {
|
|
659
|
+
if (!candidate || candidate.free !== false) continue;
|
|
660
|
+
if (typeof candidate.repoRoot !== 'string' || candidate.repoRoot.length === 0) continue;
|
|
661
|
+
const resolved = path.resolve(candidate.repoRoot);
|
|
662
|
+
if (seen.has(resolved)) continue;
|
|
663
|
+
seen.add(resolved);
|
|
664
|
+
repos.push({ repoRoot: candidate.repoRoot });
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (typeof thisRepoRoot === 'string' && thisRepoRoot.length > 0) {
|
|
668
|
+
const resolvedThis = path.resolve(thisRepoRoot);
|
|
669
|
+
if (!seen.has(resolvedThis)) {
|
|
670
|
+
seen.add(resolvedThis);
|
|
671
|
+
repos.push({ repoRoot: thisRepoRoot });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return repos;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Host-wide vault-board staleness sweep (issue #716). Extends {@link mirrorBoard}'s
|
|
680
|
+
* single-repo re-derivation to every BUSY repo on the host, so a crashed session
|
|
681
|
+
* in repo A renders as `force-closed` on the board from any repo B's
|
|
682
|
+
* session-start — not only from repo A's own next session-start/-end.
|
|
683
|
+
*
|
|
684
|
+
* Composition: {@link enumerateCandidates}(startDir) → {@link buildSweepRepos}
|
|
685
|
+
* (busy ∪ thisRepo) → {@link mirrorBoard}(repos).
|
|
686
|
+
*
|
|
687
|
+
* Notes:
|
|
688
|
+
* (a) THIS repo's `in-progress` row is rendered from ITS OWN live
|
|
689
|
+
* `session.lock` lease via {@link collectRows} — NOT via `explicitStatus`
|
|
690
|
+
* (that field is inert for `'in-progress'`; `collectRows` only honors an
|
|
691
|
+
* explicit `status: 'closed'` override). The lease is the one Phase 1.2
|
|
692
|
+
* writes/heartbeats for the calling session.
|
|
693
|
+
* (b) `frei` (lock-less) repos are excluded from re-derivation to avoid board
|
|
694
|
+
* noise — see {@link buildSweepRepos}. Prior rows of un-swept repos
|
|
695
|
+
* (busy-but-not-enumerated, or genuinely frei) are preserved by
|
|
696
|
+
* {@link mirrorBoard}'s idempotent merge, never dropped.
|
|
697
|
+
* (c) The enumerate + collectRows path is synchronous fs (readdirSync /
|
|
698
|
+
* existsSync / readLock per candidate) — O(repos) small reads, single-digit
|
|
699
|
+
* ms at host scale (~31 repos observed). No timeout is applied: a sync
|
|
700
|
+
* call cannot be preempted in-process, so a timeout would only convert a
|
|
701
|
+
* slow sweep into a thrown error, not a faster one.
|
|
702
|
+
* (d) Merge key is `repoName` (`path.basename`), case-insensitively folded via
|
|
703
|
+
* {@link foldKey} (issue #719) — two rows differing only by case (e.g.
|
|
704
|
+
* `some-repo` vs `Some-Repo`, the same physical directory on a
|
|
705
|
+
* case-insensitive-preserving filesystem like APFS) now collapse to ONE
|
|
706
|
+
* board row instead of rendering as duplicates. The survivor is whichever
|
|
707
|
+
* row is live/newest: a freshly-derived row (this sweep's own
|
|
708
|
+
* `collectRows` output) always wins over a preserved stale row; among two
|
|
709
|
+
* PRESERVED rows colliding on the folded key, the more-recent `heartbeat`
|
|
710
|
+
* wins (see the collision-resolution loop inside {@link mirrorBoard}).
|
|
711
|
+
* Two GENUINELY different, differently-rooted repos that happen to share
|
|
712
|
+
* a basename (case-insensitively) still collapse to one row — that
|
|
713
|
+
* remains a known limitation, inherited from {@link collectRows}/
|
|
714
|
+
* {@link mirrorBoard}; not addressed here.
|
|
715
|
+
*
|
|
716
|
+
* Best-effort contract: `sweepBoard` itself never throws for an enumeration
|
|
717
|
+
* failure — `enumerateCandidates` is wrapped in try/catch; on ANY failure the
|
|
718
|
+
* sweep degrades to the pre-#716 single-repo write
|
|
719
|
+
* (`mirrorBoard({ repoRoot, explicitStatus: 'in-progress' })`) so the board
|
|
720
|
+
* write still happens. `mirrorBoard`'s own internal guards (vault disabled,
|
|
721
|
+
* `_overview.md` refusal, noop-skip, …) are untouched and still apply.
|
|
722
|
+
*
|
|
723
|
+
* @param {object} [opts]
|
|
724
|
+
* @param {string} opts.repoRoot — the calling repo (always included in the sweep).
|
|
725
|
+
* @param {string} [opts.startDir] — enumeration root; omitted in production so
|
|
726
|
+
* {@link enumerateCandidates} defaults to `getConfinementRoot()` (~/Projects).
|
|
727
|
+
* Test seam only — do NOT compute `path.dirname(repoRoot)` here.
|
|
728
|
+
* @param {Date|number} [opts.now] — clock seam. A `Date` is used as-is; a finite
|
|
729
|
+
* number is treated as epoch-ms (matching {@link collectRows}'s own
|
|
730
|
+
* `now instanceof Date ? … : Date.now()` convention); anything else falls
|
|
731
|
+
* back to `Date.now()`. Always forwarded to {@link mirrorBoard} as a `Date`
|
|
732
|
+
* (see body) — {@link renderBoard} only special-cases `now instanceof Date`,
|
|
733
|
+
* so a bare number would otherwise be silently discarded there.
|
|
734
|
+
* @param {boolean} [opts.dryRun]
|
|
735
|
+
* @param {object} [opts.fs] — injectable fs for {@link mirrorBoard}/{@link writeBoard}.
|
|
736
|
+
* @param {object} [opts.deps] — injectable deps for {@link enumerateCandidates}
|
|
737
|
+
* (test seam: `readdirSync`, `existsSync`, `readLock`, `isLockLive`,
|
|
738
|
+
* `getCrossRepoProjects`, `validatePathInsideProject`, `now`).
|
|
739
|
+
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
|
|
740
|
+
* — forwarded verbatim to every {@link mirrorBoard} call below (both the happy-path
|
|
741
|
+
* and the enumeration-failure-fallback path), which in turn forwards it to
|
|
742
|
+
* {@link parseSessionConfig}'s `hostPaths` DI seam (issue #653/#783). See
|
|
743
|
+
* {@link mirrorBoard}'s own `hostPaths` doc for the hermetic-test rationale.
|
|
744
|
+
* @returns {Promise<{ action: string, path?: string }>}
|
|
745
|
+
*/
|
|
746
|
+
export async function sweepBoard({ repoRoot, startDir, now = new Date(), dryRun = false, fs, deps, hostPaths } = {}) {
|
|
747
|
+
// Accept both a Date and a caller-passed epoch-ms number (previously the
|
|
748
|
+
// numeric case was silently discarded by `now instanceof Date ? … : Date.now()`).
|
|
749
|
+
const nowMs = now instanceof Date
|
|
750
|
+
? now.getTime()
|
|
751
|
+
: (typeof now === 'number' && Number.isFinite(now) ? now : Date.now());
|
|
752
|
+
|
|
753
|
+
// mirrorBoard → renderBoard only special-case `now instanceof Date` (a bare
|
|
754
|
+
// number falls through to `new Date()` inside renderBoard, breaking
|
|
755
|
+
// determinism). Forward a real Date built from `nowMs` so a numeric `now`
|
|
756
|
+
// stays deterministic end-to-end.
|
|
757
|
+
const nowForMirror = now instanceof Date ? now : new Date(nowMs);
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
const candidates = await enumerateCandidates({ startDir, now: nowMs, deps });
|
|
761
|
+
const repos = buildSweepRepos(candidates, { thisRepoRoot: repoRoot });
|
|
762
|
+
return await mirrorBoard({ repoRoot, repos, now: nowForMirror, dryRun, fs, hostPaths });
|
|
763
|
+
} catch (err) {
|
|
764
|
+
console.warn('[sweepBoard] host-wide enumeration failed — degraded to single-repo board write:', err?.message ?? err);
|
|
765
|
+
// Best-effort fallback: enumeration failed for any reason — degrade to the
|
|
766
|
+
// pre-#716 single-repo write so the board is still updated for THIS repo.
|
|
767
|
+
return mirrorBoard({ repoRoot, explicitStatus: 'in-progress', now: nowForMirror, dryRun, fs, hostPaths });
|
|
768
|
+
}
|
|
769
|
+
}
|