session-orchestrator 5.11.0 → 5.13.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 +12 -1
- package/.claude-plugin/plugin.json +4 -1
- package/.codex-plugin/agents/explorer.toml +1 -1
- package/.codex-plugin/agents/session-reviewer.toml +2 -2
- package/.codex-plugin/agents/wave-worker.toml +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +8 -0
- package/.orchestrator/policy/public-export.json +30 -0
- package/CHANGELOG.md +180 -3
- package/README.md +126 -117
- package/agents/analyst.md +7 -2
- package/agents/architect-reviewer.md +7 -2
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +9 -2
- package/agents/dialectic-deriver.md +9 -3
- package/agents/docs-writer.md +7 -2
- package/agents/eval-judge.md +7 -1
- package/agents/qa-strategist.md +7 -2
- package/agents/security-reviewer.md +9 -2
- package/agents/session-reviewer.md +18 -2
- package/agents/skill-applied-judge.md +7 -1
- package/agents/test-writer.md +9 -2
- package/agents/ui-developer.md +9 -2
- package/agents/ux-evaluator.md +6 -1
- package/commands/session.md +4 -2
- package/commands/templates-ack.md +89 -18
- package/docs/agent-authoring.md +30 -11
- package/docs/ci-setup.md +38 -2
- package/docs/codex-setup.md +33 -9
- package/docs/components.md +13 -2
- package/docs/cursor-setup.md +3 -3
- package/docs/events-schema.md +5 -4
- package/docs/evidence-candidates.md +63 -0
- package/docs/fleet-checkin-audit.md +7 -4
- package/docs/github-mirror-protection.md +150 -3
- package/docs/install.md +102 -25
- package/docs/instruction-delivery.md +1 -1
- package/docs/migration-v4.md +2 -2
- package/docs/mods.md +118 -0
- package/docs/owner-config-schema.md +1 -0
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/pm-skills-marketplace.md +4 -3
- package/docs/rule-authoring.md +24 -13
- package/docs/scope-collision-guard.md +2 -1
- package/docs/session-config-reference.md +55 -21
- package/docs/session-config-template.md +32 -24
- package/docs/sessions-ledger-repair.md +137 -0
- package/docs/vault-docs-architecture.md +1 -1
- package/hooks/_lib/hook-import-set.json +406 -73
- package/hooks/_lib/hook-root.mjs +301 -0
- package/hooks/_lib/lock-bootstrap.mjs +434 -4
- package/hooks/_lib/orchestrator-ignore.mjs +3 -0
- package/hooks/_lib/subagent-transcript.mjs +2 -2
- package/hooks/_lib/vcs-create-matcher.mjs +13 -17
- package/hooks/cwd-change-restore.mjs +5 -1
- package/hooks/enforce-commands.mjs +4 -3
- package/hooks/enforce-scope.mjs +11 -1
- package/hooks/hooks-codex.json +48 -2
- package/hooks/hooks.json +31 -31
- package/hooks/on-session-end.mjs +38 -15
- package/hooks/on-session-start.mjs +158 -59
- package/hooks/on-stop.mjs +11 -6
- package/hooks/post-bash-issue-budget-refund.mjs +6 -1
- package/hooks/post-bash-write-verify.mjs +8 -4
- package/hooks/post-edit-validate.mjs +45 -52
- package/hooks/post-subagent-discovery-validator.mjs +2 -2
- package/hooks/post-tool-batch-wave-signal.mjs +8 -4
- package/hooks/post-tool-failure-corrective-context.mjs +5 -1
- package/hooks/pre-bash-destructive-guard.mjs +220 -8
- package/hooks/pre-bash-issue-budget.mjs +6 -1
- package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
- package/hooks/pre-bash-staging-fence.mjs +258 -14
- package/hooks/pre-bash-templates-first.mjs +28 -27
- package/hooks/run-node.sh +1 -1
- package/hooks/skill-invocation-telemetry.mjs +15 -8
- package/hooks/subagent-telemetry.mjs +77 -14
- package/hooks/wave-scope-commit-guard.mjs +143 -14
- package/mods/so-hud/.claude-plugin/plugin.json +11 -0
- package/mods/so-hud/README.md +57 -0
- package/mods/so-hud/hooks/beacon-io.mjs +101 -0
- package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
- package/mods/so-hud/hooks/hooks.json +1 -0
- package/mods/so-hud/hooks/logic.ts +182 -0
- package/mods/so-hud/hooks/register.tsx +670 -0
- package/mods/so-hud/hooks/rules.ts +2998 -0
- package/mods/so-hud/tsconfig.json +8 -0
- package/mods/so-hud/types/index.d.ts +36 -0
- package/package.json +4 -3
- package/rules/_index.md +1 -1
- package/rules/always-on/ask-via-tool.md +1 -1
- package/rules/always-on/loop-and-monitor.md +17 -194
- package/rules/always-on/parallel-sessions.md +3 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +1 -1
- package/rules/opt-in-stack/security-web.md +5 -1
- package/scripts/agent-usage-census.mjs +79 -0
- package/scripts/apply-session-learnings.mjs +34 -6
- package/scripts/archive-closed-prds.mjs +21 -5
- package/scripts/autopilot.mjs +11 -5
- package/scripts/backfill-abandoned-sessions.mjs +13 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +61 -6
- package/scripts/backfill-sessions.mjs +16 -2
- package/scripts/check-package-manager.mjs +26 -8
- package/scripts/check-sessions-integrity.mjs +4 -1
- package/scripts/ci/assert-vitest-green.mjs +3 -0
- package/scripts/ci/windows-smoke.mjs +201 -0
- package/scripts/close-precheck.mjs +55 -0
- package/scripts/dialectic-deriver.mjs +20 -12
- package/scripts/emit-event.mjs +2 -2
- package/scripts/emit-session.mjs +239 -20
- package/scripts/eval-session.mjs +17 -3
- package/scripts/export-hw-learnings.mjs +29 -2
- package/scripts/fleet-checkin-audit.mjs +8 -4
- package/scripts/fleet-checkin.mjs +73 -3
- package/scripts/gc-stale-worktrees.mjs +6 -1
- package/scripts/github-export.mjs +128 -0
- package/scripts/github-protection-audit.mjs +3 -1
- package/scripts/handover-queue-check.mjs +209 -0
- package/scripts/harness-audit.mjs +6 -4
- package/scripts/lib/agent-frontmatter.mjs +57 -10
- package/scripts/lib/agent-status.mjs +125 -7
- package/scripts/lib/agent-usage-census.mjs +394 -0
- package/scripts/lib/auto-dialectic.mjs +17 -10
- package/scripts/lib/auto-dream.mjs +22 -14
- package/scripts/lib/autopilot/loop.mjs +3 -1
- package/scripts/lib/autopilot/recent-runs.mjs +4 -2
- package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
- package/scripts/lib/autopilot/telemetry.mjs +5 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/baseline-archetypes.mjs +10 -2
- package/scripts/lib/blocked-commands-policy.mjs +4 -3
- package/scripts/lib/build-live-signals.mjs +13 -9
- package/scripts/lib/ci-status-banner.mjs +329 -24
- package/scripts/lib/codex/plugin-contract.mjs +8 -0
- package/scripts/lib/cold-start-detector.mjs +5 -2
- package/scripts/lib/command-blocker.mjs +603 -43
- package/scripts/lib/common.mjs +3 -2
- package/scripts/lib/config/cross-repo.mjs +74 -7
- package/scripts/lib/config/host-paths.mjs +6 -2
- package/scripts/lib/config/remote-hosts.mjs +2 -2
- package/scripts/lib/config/section-extractor.mjs +65 -11
- package/scripts/lib/config/template-keys.mjs +153 -0
- package/scripts/lib/config/vault-integration.mjs +1 -0
- package/scripts/lib/config.mjs +3 -3
- package/scripts/lib/coordinator-snapshot.mjs +1 -1
- package/scripts/lib/discovery/triage-state.mjs +2 -1
- package/scripts/lib/dispatcher/rank.mjs +5 -7
- package/scripts/lib/ecosystem-health.mjs +2 -2
- package/scripts/lib/eval/engine.mjs +3 -2
- package/scripts/lib/eval/session-resolve.mjs +6 -2
- package/scripts/lib/eval/sink.mjs +4 -3
- package/scripts/lib/events-retention-banner.mjs +3 -2
- package/scripts/lib/events-schema.mjs +10 -0
- package/scripts/lib/events.mjs +23 -12
- package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
- package/scripts/lib/fleet-protocol.mjs +21 -0
- package/scripts/lib/gates/gate-full.mjs +172 -11
- package/scripts/lib/gates/gate-helpers.mjs +236 -23
- package/scripts/lib/git-config-drift.mjs +141 -7
- package/scripts/lib/github-export.mjs +447 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
- package/scripts/lib/handover-queue-marks.mjs +159 -0
- package/scripts/lib/hardware-pattern-detector.mjs +103 -13
- package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
- package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
- package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
- package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
- package/scripts/lib/host-identity.mjs +9 -5
- package/scripts/lib/http-request.mjs +64 -0
- package/scripts/lib/instruction-budget-guard.mjs +30 -22
- package/scripts/lib/io.mjs +6 -3
- package/scripts/lib/issue-budget-reconcile.mjs +3 -2
- package/scripts/lib/issue-budget.mjs +36 -7
- package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
- package/scripts/lib/learnings/io.mjs +289 -15
- package/scripts/lib/learnings/select.mjs +53 -4
- package/scripts/lib/learnings/store-role.mjs +209 -0
- package/scripts/lib/learnings/surface.mjs +46 -7
- package/scripts/lib/ledger-path.mjs +113 -0
- package/scripts/lib/maintenance-due-banner.mjs +63 -12
- package/scripts/lib/memory-banner.mjs +5 -6
- package/scripts/lib/memory-proposals/collector.mjs +5 -2
- package/scripts/lib/memory-proposals/sink.mjs +7 -4
- package/scripts/lib/memory-proposals/store.mjs +22 -5
- package/scripts/lib/mode-selector-accuracy.mjs +2 -1
- package/scripts/lib/no-follow-open.mjs +57 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
- package/scripts/lib/own-lock-root.mjs +95 -0
- package/scripts/lib/owner-interview.mjs +2 -1
- package/scripts/lib/owner-yaml.mjs +70 -7
- package/scripts/lib/platform.mjs +161 -9
- package/scripts/lib/plugin-update-banner.mjs +15 -11
- package/scripts/lib/pre-push-local-partial.mjs +117 -0
- package/scripts/lib/pre-push-remote-gate.mjs +151 -25
- package/scripts/lib/process-group.mjs +270 -34
- package/scripts/lib/project-hygiene.mjs +28 -0
- package/scripts/lib/quality-gate.mjs +33 -9
- package/scripts/lib/reconcile/backlog.mjs +29 -15
- package/scripts/lib/reconcile/engine.mjs +26 -2
- package/scripts/lib/reconcile/idempotency.mjs +65 -3
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
- package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
- package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
- package/scripts/lib/resource-probe/evaluate.mjs +32 -9
- package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
- package/scripts/lib/rule-loader.mjs +20 -10
- package/scripts/lib/scope-baseline.mjs +123 -8
- package/scripts/lib/scope-echo.mjs +5 -2
- package/scripts/lib/scope-gate.mjs +55 -20
- package/scripts/lib/session-close-backfill.mjs +75 -7
- package/scripts/lib/session-end/close-precheck.mjs +151 -0
- package/scripts/lib/session-end/phase-skip.mjs +8 -7
- package/scripts/lib/session-end/tail-runner.mjs +7 -2
- package/scripts/lib/session-end/vault-orphans.mjs +205 -0
- package/scripts/lib/session-id.mjs +9 -1
- package/scripts/lib/session-identity/own-session.mjs +63 -0
- package/scripts/lib/session-invocation.mjs +14 -3
- package/scripts/lib/session-lock.mjs +114 -0
- package/scripts/lib/session-record-repair.mjs +245 -35
- package/scripts/lib/session-registry.mjs +85 -12
- package/scripts/lib/session-schema/constants.mjs +9 -0
- package/scripts/lib/session-schema/filters.mjs +56 -1
- package/scripts/lib/session-schema/validator.mjs +15 -5
- package/scripts/lib/session-start-probes.mjs +149 -22
- package/scripts/lib/session-token-rollup.mjs +2 -1
- package/scripts/lib/session-work-trees.mjs +166 -0
- package/scripts/lib/sessions-canonical.mjs +61 -3
- package/scripts/lib/sessions-integrity-banner.mjs +17 -39
- package/scripts/lib/sessions-staleness-banner.mjs +8 -5
- package/scripts/lib/sh-quote.mjs +1 -0
- package/scripts/lib/skill-evidence-window.mjs +5 -1
- package/scripts/lib/skill-health/join.mjs +10 -9
- package/scripts/lib/skill-invocations-schema.mjs +13 -0
- package/scripts/lib/skill-judgments-schema.mjs +6 -2
- package/scripts/lib/snapshot-probe.mjs +414 -0
- package/scripts/lib/so-hud/collect.mjs +174 -0
- package/scripts/lib/so-hud/runtime.mjs +87 -0
- package/scripts/lib/spiral-carryover.mjs +8 -1
- package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
- package/scripts/lib/state-md/idle-reset.mjs +248 -0
- package/scripts/lib/state-md/mission-status.mjs +5 -1
- package/scripts/lib/state-md/recommendations.mjs +26 -4
- package/scripts/lib/state-md-peer-guard.mjs +2 -0
- package/scripts/lib/state-md.mjs +31 -8
- package/scripts/lib/sunset/walker.mjs +4 -3
- package/scripts/lib/telemetry/consent.mjs +4 -2
- package/scripts/lib/telemetry/queue.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +45 -9
- package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
- package/scripts/lib/tmux-layout/layouts.mjs +20 -3
- package/scripts/lib/ux-grill/paths.mjs +5 -1
- package/scripts/lib/validate/check-agents.mjs +37 -6
- package/scripts/lib/validate/check-component-paths.mjs +25 -1
- package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
- package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
- package/scripts/lib/validate/check-json-files.mjs +3 -1
- package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
- package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
- package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
- package/scripts/lib/validate/check-unwired-features.mjs +4 -2
- package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
- package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
- package/scripts/lib/validate/confidential-names.mjs +3 -1
- package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
- package/scripts/lib/vault-mirror/namespace.mjs +22 -9
- package/scripts/lib/vault-mirror/process.mjs +197 -65
- package/scripts/lib/vault-mirror/prune.mjs +5 -2
- package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
- package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
- package/scripts/lib/vault-mirror/retention.mjs +2 -2
- package/scripts/lib/vault-mirror/utils.mjs +106 -13
- package/scripts/lib/vault-staleness-banner.mjs +4 -7
- package/scripts/lib/vault-status/board-writer.mjs +34 -5
- package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
- package/scripts/lib/win-process-tree.mjs +137 -0
- package/scripts/lib/workspace.mjs +1 -1
- package/scripts/lib/worktree/constants.mjs +1 -1
- package/scripts/lib/worktree/lifecycle.mjs +1 -1
- package/scripts/lib/worktree/listing.mjs +1 -1
- package/scripts/lib/zx.mjs +50 -0
- package/scripts/mcp-server.mjs +16 -7
- package/scripts/mcp-server.sh +13 -83
- package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
- package/scripts/memory-propose.mjs +10 -3
- package/scripts/migrate-cold-start-seed.mjs +7 -0
- package/scripts/migrate-legacy-learnings.sh +22 -0
- package/scripts/migrate-sessions-jsonl.mjs +16 -2
- package/scripts/migrate-vault-paths.mjs +29 -13
- package/scripts/parse-config.mjs +71 -14
- package/scripts/print-learnings-index.mjs +25 -2
- package/scripts/release-session-lock.mjs +94 -7
- package/scripts/release.mjs +451 -24
- package/scripts/relocate-vault-corpus.mjs +11 -7
- package/scripts/repair-invalid-sessions.mjs +68 -3
- package/scripts/resolve-session-invocation.mjs +57 -8
- package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
- package/scripts/run-quality-gate.mjs +33 -7
- package/scripts/self-update.mjs +56 -7
- package/scripts/session-commit-files.mjs +133 -0
- package/scripts/site-numbers.mjs +52 -9
- package/scripts/snapshot-probe.mjs +156 -0
- package/scripts/so-hud.mjs +68 -0
- package/scripts/state-md-idle-reset.mjs +82 -0
- package/scripts/sweep-expired-learnings.mjs +54 -6
- package/scripts/sweep-expired-rules.mjs +4 -1
- package/scripts/validate-plugin.mjs +22 -0
- package/scripts/vault-integration-watcher.mjs +54 -6
- package/scripts/vault-mirror.mjs +9 -0
- package/scripts/vault-orphans.mjs +171 -0
- package/skills/_shared/fleet-protocol.md +166 -3
- package/skills/_shared/instruction-file-resolution.md +1 -1
- package/skills/_shared/monitor-patterns.md +44 -25
- package/skills/_shared/platform-tools.md +7 -0
- package/skills/_shared/state-ownership.md +5 -4
- package/skills/architecture/references/domain-model.md +1 -1
- package/skills/autopilot/SKILL.md +3 -2
- package/skills/bootstrap/SKILL.md +3 -1
- package/skills/bootstrap/_shared-template.md +4 -4
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/bootstrap/private-contract.md +1 -1
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
- package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
- package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
- package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
- package/skills/claude-md-drift-check/SKILL.md +3 -3
- package/skills/claude-md-drift-check/checker.mjs +61 -101
- package/skills/close/SKILL.md +20 -7
- package/skills/discovery/probes/docs-staleness.mjs +7 -5
- package/skills/discovery/probes/frontend-slop.mjs +7 -3
- package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
- package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
- package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
- package/skills/discovery/probes/vault-staleness.mjs +5 -3
- package/skills/discovery/probes-session.md +3 -0
- package/skills/discovery/probes-vault.md +2 -0
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eval/SKILL.md +3 -1
- package/skills/evolve/SKILL.md +2 -0
- package/skills/evolve/references/evolve-analyze-mode.md +3 -3
- package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
- package/skills/gitlab-ops/SKILL.md +7 -6
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/go/SKILL.md +13 -1
- package/skills/npm-publish/SKILL.md +5 -2
- package/skills/plan/SKILL.md +23 -10
- package/skills/plan/mode-feature.md +13 -8
- package/skills/plan/mode-new.md +6 -14
- package/skills/plan/prd-feature-template.md +19 -0
- package/skills/plan/prd-full-template.md +12 -5
- package/skills/plan/prd-premortem-panel.md +48 -0
- package/skills/plan/prd-reviewer-prompt.md +11 -3
- package/skills/plan/soul.md +1 -1
- package/skills/portfolio/SKILL.md +5 -6
- package/skills/quality-gates/SKILL.md +2 -4
- package/skills/reconcile/SKILL.md +27 -4
- package/skills/release/SKILL.md +16 -8
- package/skills/session-end/SKILL.md +97 -13
- package/skills/session-end/phase-3-2-docs-verification.md +1 -0
- package/skills/session-end/phase-3-6-tail.md +11 -7
- package/skills/session-end/phase-3-7a-recommendations.md +9 -2
- package/skills/session-end/plan-verification.md +16 -1
- package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
- package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
- package/skills/session-end/references/session-summary-template.md +8 -0
- package/skills/session-end/session-metrics-write.md +7 -4
- package/skills/session-plan/SKILL.md +5 -3
- package/skills/session-start/SKILL.md +10 -4
- package/skills/session-start/phase-4-5-resource-health.md +11 -5
- package/skills/session-start/phase-7-5-mode-selector.md +3 -1
- package/skills/session-start/phase-8-5-express-path.md +4 -3
- package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
- package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
- package/skills/tmux-layout/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +3 -2
- package/skills/wave-executor/SKILL.md +5 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
- package/skills/wave-executor/references/wave-loop-review.md +13 -5
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
- package/AGENTS.md +0 -207
- package/assets/og-card.png +0 -0
- package/assets/og-card.svg +0 -47
- package/assets/wave-lifecycle.svg +0 -98
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* via `.passthrough()`, and it is NOT rewritten retroactively by this change.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { toDate, buildTag, slugifyIdSafe } from './utils.mjs';
|
|
18
|
+
import { toDate, buildTag, slugifyIdSafe, noteIdFor, yamlNoteId } from './utils.mjs';
|
|
19
19
|
import { stableHostname } from '../host-identity.mjs';
|
|
20
20
|
import { sourceRecordValue, sourceRecordLine } from './render-learnings.mjs';
|
|
21
21
|
import { sessionNarrativeText } from './retention.mjs';
|
|
@@ -118,38 +118,17 @@ function waveCount(value) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* ── RENDERABLE
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* tests/lib/vault-mirror/render-sessions.test.mjs.
|
|
130
|
-
*
|
|
131
|
-
* The relationship is deliberately asymmetric, not a duplicate:
|
|
132
|
-
*
|
|
133
|
-
* `REQUIRED_FIELDS` — "this record is well-formed". The writer
|
|
134
|
-
* (scripts/emit-session.mjs) REFUSES a record that fails it.
|
|
135
|
-
* these lists — "this record renders into a note a human can read".
|
|
136
|
-
* vault-mirror SKIPS a record that fails them; the record stays valid.
|
|
137
|
-
*
|
|
138
|
-
* Collapsing the two would destroy that distinction. Hence `effectiveness`:
|
|
139
|
-
* optional to the writer (a record lacking it is fine), required by the v1
|
|
140
|
-
* renderer (a note reading `planned=n/a, completed=n/a, carryover=n/a,
|
|
141
|
-
* rate=n/a` is worse than no note — indistinguishable from a real session whose
|
|
142
|
-
* metrics happened to be absent).
|
|
143
|
-
*
|
|
144
|
-
* The predicate here is VALUE presence (`null`/`undefined` both fail); the
|
|
145
|
-
* validator's is KEY presence. See `_validateRequiredFields` for why.
|
|
121
|
+
* ── RENDERABLE AND SCHEMA-VALID (#1575) ───────────────────────────────────
|
|
122
|
+
*
|
|
123
|
+
* The generators retain route-specific required-field lists. V1 requires the
|
|
124
|
+
* canonical REQUIRED_FIELDS; legacy V2/V3 derive aggregates from other shapes
|
|
125
|
+
* and cannot be emitted by the canonical writer. Effectiveness is optional
|
|
126
|
+
* and nullable in every route, matching the writer. Unknown metrics render
|
|
127
|
+
* n/a; a measured zero remains zero. Malformed non-object metrics still fail.
|
|
128
|
+
* Required fields use VALUE presence; the validator separately checks types.
|
|
146
129
|
*/
|
|
147
130
|
|
|
148
|
-
/**
|
|
149
|
-
* v1 — the only generator reachable from a write-path-valid record (measured:
|
|
150
|
-
* 205/205 live ledger records at HEAD 730ee9d route v1). Its list is
|
|
151
|
-
* `REQUIRED_FIELDS` plus `effectiveness`, i.e. a strict superset.
|
|
152
|
-
*/
|
|
131
|
+
/** V1 uses the canonical required-field set without requiring optional metrics. */
|
|
153
132
|
export const RENDERABLE_SESSION_FIELDS_V1 = Object.freeze([
|
|
154
133
|
'session_id',
|
|
155
134
|
'session_type',
|
|
@@ -160,7 +139,6 @@ export const RENDERABLE_SESSION_FIELDS_V1 = Object.freeze([
|
|
|
160
139
|
'total_files_changed',
|
|
161
140
|
'agent_summary',
|
|
162
141
|
'waves',
|
|
163
|
-
'effectiveness',
|
|
164
142
|
]);
|
|
165
143
|
|
|
166
144
|
/**
|
|
@@ -183,7 +161,6 @@ export const RENDERABLE_SESSION_FIELDS_V2 = Object.freeze([
|
|
|
183
161
|
'completed_at',
|
|
184
162
|
'waves',
|
|
185
163
|
'files_changed',
|
|
186
|
-
'effectiveness',
|
|
187
164
|
]);
|
|
188
165
|
|
|
189
166
|
/**
|
|
@@ -200,9 +177,18 @@ export const RENDERABLE_SESSION_FIELDS_V3 = Object.freeze([
|
|
|
200
177
|
'started_at',
|
|
201
178
|
'completed_at',
|
|
202
179
|
'waves',
|
|
203
|
-
'effectiveness',
|
|
204
180
|
]);
|
|
205
181
|
|
|
182
|
+
/** Optional metrics share the writer's object-or-null contract in all routes. */
|
|
183
|
+
function sessionEffectiveness(entry) {
|
|
184
|
+
const value = entry.effectiveness;
|
|
185
|
+
if (value === undefined || value === null) return {};
|
|
186
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
187
|
+
throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${entry.session_id})`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
206
192
|
/**
|
|
207
193
|
* Session-ledger status → vault-frontmatter status mapping (#909).
|
|
208
194
|
*
|
|
@@ -422,12 +408,9 @@ export function generateSessionNote(entry, options = {}) {
|
|
|
422
408
|
total_files_changed,
|
|
423
409
|
agent_summary,
|
|
424
410
|
waves,
|
|
425
|
-
effectiveness,
|
|
426
411
|
} = entry;
|
|
427
412
|
|
|
428
|
-
|
|
429
|
-
throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${session_id})`);
|
|
430
|
-
}
|
|
413
|
+
const effectiveness = sessionEffectiveness(entry);
|
|
431
414
|
if (typeof agent_summary !== 'object' || agent_summary === null) {
|
|
432
415
|
throw new Error(`vault-mirror: session entry missing nested field 'agent_summary' (session_id=${session_id})`);
|
|
433
416
|
}
|
|
@@ -463,7 +446,7 @@ export function generateSessionNote(entry, options = {}) {
|
|
|
463
446
|
// and cap the combined tag at 64 chars so the frontmatter passes the vault
|
|
464
447
|
// tagPathRegex. The `id` is likewise slugified to a kebab slug below —
|
|
465
448
|
// entry.session_id may carry an ISO-timestamp uppercase `T`/`:`/`.`/`Z`.
|
|
466
|
-
const noteId = slugifyIdSafe(session_id) ?? session_id;
|
|
449
|
+
const noteId = noteIdFor(options.repoNs, slugifyIdSafe(session_id) ?? session_id);
|
|
467
450
|
// #909: the real ledger status, mapped into the vault enum — never hard-coded.
|
|
468
451
|
const vaultStatus = vaultStatusForSession(entry);
|
|
469
452
|
const tags = `[${buildTag(['session', session_type])}, ${buildTag(['status', vaultStatus])}]`;
|
|
@@ -554,7 +537,7 @@ export function generateSessionNote(entry, options = {}) {
|
|
|
554
537
|
const { repoNs } = options;
|
|
555
538
|
|
|
556
539
|
return `---
|
|
557
|
-
id: ${noteId}
|
|
540
|
+
id: ${yamlNoteId(noteId)}
|
|
558
541
|
type: session
|
|
559
542
|
title: ${title}
|
|
560
543
|
status: ${vaultStatus}
|
|
@@ -592,11 +575,9 @@ export function generateSessionNoteV2(entry, options = {}) {
|
|
|
592
575
|
if (!Array.isArray(entry.waves)) {
|
|
593
576
|
throw new Error(`vault-mirror: session entry 'waves' must be an array (session_id=${entry.session_id})`);
|
|
594
577
|
}
|
|
595
|
-
|
|
596
|
-
throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${entry.session_id})`);
|
|
597
|
-
}
|
|
578
|
+
const effectiveness = sessionEffectiveness(entry);
|
|
598
579
|
|
|
599
|
-
const { session_id, session_type, started_at, completed_at, duration_seconds, branch, planned_issues, waves, files_changed, issues_closed, issues_created
|
|
580
|
+
const { session_id, session_type, started_at, completed_at, duration_seconds, branch, planned_issues, waves, files_changed, issues_closed, issues_created } = entry;
|
|
600
581
|
|
|
601
582
|
const created = toDate(started_at);
|
|
602
583
|
const updated = toDate(completed_at);
|
|
@@ -620,7 +601,7 @@ export function generateSessionNoteV2(entry, options = {}) {
|
|
|
620
601
|
// and cap the combined tag at 64 chars so the frontmatter passes the vault
|
|
621
602
|
// tagPathRegex. The `id` is likewise slugified to a kebab slug below —
|
|
622
603
|
// entry.session_id may carry an ISO-timestamp uppercase `T`/`:`/`.`/`Z`.
|
|
623
|
-
const noteId = slugifyIdSafe(session_id) ?? session_id;
|
|
604
|
+
const noteId = noteIdFor(options.repoNs, slugifyIdSafe(session_id) ?? session_id);
|
|
624
605
|
// #909: the real ledger status, mapped into the vault enum — never hard-coded.
|
|
625
606
|
const vaultStatus = vaultStatusForSession(entry);
|
|
626
607
|
const tags = `[${buildTag(['session', session_type])}, ${buildTag(['status', vaultStatus])}]`;
|
|
@@ -641,7 +622,7 @@ export function generateSessionNoteV2(entry, options = {}) {
|
|
|
641
622
|
const { repoNs } = options;
|
|
642
623
|
|
|
643
624
|
return `---
|
|
644
|
-
id: ${noteId}
|
|
625
|
+
id: ${yamlNoteId(noteId)}
|
|
645
626
|
type: session
|
|
646
627
|
title: ${title}
|
|
647
628
|
status: ${vaultStatus}
|
|
@@ -690,14 +671,12 @@ export function generateSessionNoteV3(entry, options = {}) {
|
|
|
690
671
|
if (typeof entry.waves !== 'number') {
|
|
691
672
|
throw new Error(`vault-mirror: session entry 'waves' must be a number (session_id=${entry.session_id})`);
|
|
692
673
|
}
|
|
693
|
-
|
|
694
|
-
throw new Error(`vault-mirror: session entry missing nested field 'effectiveness' (session_id=${entry.session_id})`);
|
|
695
|
-
}
|
|
674
|
+
const effectiveness = sessionEffectiveness(entry);
|
|
696
675
|
|
|
697
676
|
const {
|
|
698
677
|
session_id, session_type, platform, branch, started_at, completed_at,
|
|
699
678
|
duration_minutes, duration_seconds, waves, agents_dispatched, agent_summary,
|
|
700
|
-
planned_issues,
|
|
679
|
+
planned_issues, commits, issues_closed, issues_created,
|
|
701
680
|
follow_ups_filed, tests_total_pre, tests_total_post, tests_added,
|
|
702
681
|
} = entry;
|
|
703
682
|
|
|
@@ -727,8 +706,12 @@ export function generateSessionNoteV3(entry, options = {}) {
|
|
|
727
706
|
const failed = as.failed ?? MISSING_VALUE;
|
|
728
707
|
const spiral = as.spiral ?? MISSING_VALUE;
|
|
729
708
|
|
|
709
|
+
// A qualified cross-repo reference (`group/repo#n`, #1603) already carries
|
|
710
|
+
// its own `#`; only a bare number gets the prefix.
|
|
730
711
|
const fmtIssues = (list) =>
|
|
731
|
-
Array.isArray(list) && list.length
|
|
712
|
+
Array.isArray(list) && list.length
|
|
713
|
+
? list.map((i) => (typeof i === 'number' ? `#${i}` : String(i))).join(', ')
|
|
714
|
+
: '—';
|
|
732
715
|
const closedList = fmtIssues(issues_closed);
|
|
733
716
|
const createdList = fmtIssues(issues_created);
|
|
734
717
|
const followList = fmtIssues(follow_ups_filed);
|
|
@@ -752,7 +735,7 @@ export function generateSessionNoteV3(entry, options = {}) {
|
|
|
752
735
|
// and cap the combined tag at 64 chars so the frontmatter passes the vault
|
|
753
736
|
// tagPathRegex. The `id` is likewise slugified to a kebab slug below —
|
|
754
737
|
// entry.session_id may carry an ISO-timestamp uppercase `T`/`:`/`.`/`Z`.
|
|
755
|
-
const noteId = slugifyIdSafe(session_id) ?? session_id;
|
|
738
|
+
const noteId = noteIdFor(options.repoNs, slugifyIdSafe(session_id) ?? session_id);
|
|
756
739
|
// #909: the real ledger status, mapped into the vault enum — never hard-coded.
|
|
757
740
|
const vaultStatus = vaultStatusForSession(entry);
|
|
758
741
|
const tags = `[${buildTag(['session', session_type])}, ${buildTag(['status', vaultStatus])}]`;
|
|
@@ -768,7 +751,7 @@ export function generateSessionNoteV3(entry, options = {}) {
|
|
|
768
751
|
const { repoNs } = options;
|
|
769
752
|
|
|
770
753
|
return `---
|
|
771
|
-
id: ${noteId}
|
|
754
|
+
id: ${yamlNoteId(noteId)}
|
|
772
755
|
type: session
|
|
773
756
|
title: ${title}
|
|
774
757
|
status: ${vaultStatus}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { readdirSync } from 'node:fs';
|
|
35
35
|
import { join } from 'node:path';
|
|
36
36
|
|
|
37
|
-
import { subjectToSlug } from './utils.mjs';
|
|
37
|
+
import { subjectToSlug, noteIdFor, yamlNoteId } from './utils.mjs';
|
|
38
38
|
|
|
39
39
|
/** The generator marker every mirror-written note carries. */
|
|
40
40
|
export const GENERATOR_MARKER = 'session-orchestrator-vault-mirror@1';
|
|
@@ -367,7 +367,7 @@ export function renderRollup(existing, rows, { ns, month }) {
|
|
|
367
367
|
.map((r) => `| ${r.date} | \`${r.id}\` | ${r.type} | ${r.waves} | ${r.agents} | ${r.files} |`)
|
|
368
368
|
.join('\n');
|
|
369
369
|
return `---
|
|
370
|
-
id: ${idSlug}
|
|
370
|
+
id: ${yamlNoteId(noteIdFor(null, idSlug))}
|
|
371
371
|
type: session
|
|
372
372
|
title: "Sessions ${month} (${label}, metrics only)"
|
|
373
373
|
status: verified
|
|
@@ -10,32 +10,43 @@
|
|
|
10
10
|
import { parseSessionId } from '../session-id.mjs';
|
|
11
11
|
import { digestSha256Short } from '../crypto-digest-utils.mjs';
|
|
12
12
|
|
|
13
|
+
/** Normalize letters for new note slugs. */
|
|
14
|
+
function normalizeSlugLetters(value) {
|
|
15
|
+
const german = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' };
|
|
16
|
+
return value.normalize('NFC').toLowerCase()
|
|
17
|
+
.replace(/[äöüß]/g, (letter) => german[letter])
|
|
18
|
+
.normalize('NFKD').replace(/\p{M}/gu, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Historical ASCII deletion, frozen for record identity and old note lookup. */
|
|
22
|
+
export function legacySubjectToSlug(subject) {
|
|
23
|
+
return slugSubject(subject, (value) => value.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
27
|
* Convert a subject string to a kebab slug.
|
|
15
28
|
* - If subject contains slashes, collapse to last path segment.
|
|
29
|
+
* - Transliterate German letters and decompose accents.
|
|
16
30
|
* - Replace dots and underscores with hyphens.
|
|
17
31
|
* - Strip all non-[a-z0-9-] chars.
|
|
18
32
|
* - Collapse consecutive hyphens.
|
|
19
33
|
* - Trim leading/trailing hyphens.
|
|
20
34
|
*/
|
|
21
35
|
export function subjectToSlug(subject) {
|
|
22
|
-
|
|
36
|
+
return slugSubject(subject, normalizeSlugLetters);
|
|
37
|
+
}
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
function slugSubject(subject, normalize) {
|
|
40
|
+
let s = subject;
|
|
25
41
|
if (s.includes('/')) {
|
|
26
42
|
const parts = s.split('/').filter(Boolean);
|
|
27
43
|
s = parts[parts.length - 1];
|
|
28
44
|
}
|
|
29
|
-
|
|
30
|
-
// Normalise: lowercase, dots/underscores → hyphens, strip invalid chars
|
|
31
|
-
s = s
|
|
32
|
-
.toLowerCase()
|
|
45
|
+
return normalize(s)
|
|
33
46
|
.replace(/[._]/g, '-')
|
|
34
47
|
.replace(/[^a-z0-9-]/g, '')
|
|
35
48
|
.replace(/-+/g, '-')
|
|
36
49
|
.replace(/^-+|-+$/g, '');
|
|
37
|
-
|
|
38
|
-
return s;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -80,8 +91,8 @@ export const TAG_MAX_LENGTH = 64;
|
|
|
80
91
|
* @returns {string}
|
|
81
92
|
*/
|
|
82
93
|
export function slugifyTagSegment(input) {
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
// Tags are persistent identifiers, so retain historical ASCII deletion.
|
|
95
|
+
const slug = String(input ?? '').toLowerCase()
|
|
85
96
|
.replace(/[\s._]+/g, '-')
|
|
86
97
|
.replace(/[^a-z0-9-]/g, '')
|
|
87
98
|
.replace(/-+/g, '-')
|
|
@@ -123,14 +134,95 @@ export function buildTag(segments) {
|
|
|
123
134
|
*/
|
|
124
135
|
export function slugifyIdSafe(input) {
|
|
125
136
|
// Normalise whitespace to hyphens first (so "My ID" → "my-id" rather than
|
|
126
|
-
// "myid"), then
|
|
137
|
+
// "myid"), then preserve the historical ASCII kebab contract.
|
|
127
138
|
const normalised = String(input ?? '').replace(/\s+/g, '-');
|
|
128
|
-
|
|
139
|
+
// Session identity retains historical deletion; transliteration is for new
|
|
140
|
+
// learning names, never an incidental Session-ID migration.
|
|
141
|
+
const slug = legacySubjectToSlug(normalised);
|
|
129
142
|
if (!slug) return null;
|
|
130
143
|
// Schema requires min length 2; pad a single-char slug deterministically.
|
|
131
144
|
return slug.length >= 2 ? slug : `${slug}-x`;
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
/** The vault-sync schema's `id` cap (`skills/vault-sync/validator.mjs`, `.max(128)`). */
|
|
148
|
+
export const NOTE_ID_MAX_LENGTH = 128;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The frontmatter `id` of a generated note (#1601, #1602). The FILENAME slug
|
|
152
|
+
* stays the note's identity on disk (paths never change); the `id` adds the
|
|
153
|
+
* repo namespace so it is unique across the vault's per-repo folders — a
|
|
154
|
+
* session id or learning subject recurs across repos — and is capped at
|
|
155
|
+
* {@link NOTE_ID_MAX_LENGTH}. An over-long id keeps a 119-char prefix plus an
|
|
156
|
+
* 8-hex SHA-256 suffix of the FULL id, so two long subjects sharing a prefix
|
|
157
|
+
* stay distinct. Deterministic: the same `(repoNs, slug)` always yields the
|
|
158
|
+
* same id. Without a namespace (vault-less callers, unit tests) the slug is
|
|
159
|
+
* only capped.
|
|
160
|
+
*
|
|
161
|
+
* BV-004 ceiling: the separator is a single `-` (the schema slug forbids
|
|
162
|
+
* `--`), so namespace `a` + slug `b-c` and namespace `a-b` + slug `c` render
|
|
163
|
+
* the same id. Revisit-Trigger: the vault validator reports a duplicate id
|
|
164
|
+
* whose two notes sit in different namespace folders.
|
|
165
|
+
*
|
|
166
|
+
* @param {string|null|undefined} repoNs - the resolved repo namespace folder.
|
|
167
|
+
* @param {string} slug - the note's filename slug (or rendered session slug).
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
export function noteIdFor(repoNs, slug) {
|
|
171
|
+
const base = String(slug ?? '');
|
|
172
|
+
const ns = typeof repoNs === 'string' && repoNs.length > 0 ? (isValidSlug(repoNs) ? repoNs : legacySubjectToSlug(repoNs)) : '';
|
|
173
|
+
const full = ns ? `${ns}-${base}` : base;
|
|
174
|
+
if (full.length <= NOTE_ID_MAX_LENGTH) return full;
|
|
175
|
+
const suffix = digestSha256Short(full);
|
|
176
|
+
return `${full.slice(0, NOTE_ID_MAX_LENGTH - suffix.length - 1).replace(/-+$/, '')}-${suffix}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Does a note's frontmatter `id` name the note `slug`? Accepts both the
|
|
181
|
+
* pre-#1601 id (the bare slug — a numeric one parses as the same string here,
|
|
182
|
+
* `parseFrontmatter` keeps scalars verbatim) and the current {@link noteIdFor}
|
|
183
|
+
* form, so notes written before the id change keep their ownership evidence.
|
|
184
|
+
*
|
|
185
|
+
* @param {unknown} fmId
|
|
186
|
+
* @param {string} slug
|
|
187
|
+
* @param {string|null|undefined} repoNs
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
export function noteIdMatches(fmId, slug, repoNs) {
|
|
191
|
+
return typeof fmId === 'string' && (fmId === slug || fmId === noteIdFor(repoNs, slug));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The `id:` frontmatter value, always a double-quoted YAML string (#1601): an
|
|
196
|
+
* unquoted all-digit id (`id: 433`) parses as a NUMBER under a real YAML parser
|
|
197
|
+
* and fails the schema's `z.string()`.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} id
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
export function yamlNoteId(id) {
|
|
203
|
+
return JSON.stringify(String(id));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Replace the frontmatter `id:` line of a note — used when a displaced note
|
|
208
|
+
* moves to its disambiguated name (#1506.2), whose `id` must match the file,
|
|
209
|
+
* and by the id migration (#1601, `migrate-ids.mjs`). Only the `id:` line
|
|
210
|
+
* INSIDE the leading frontmatter block changes; every other byte — body,
|
|
211
|
+
* other fields, `[REDACTED]` spans (#1025) — is kept verbatim. A note without
|
|
212
|
+
* a frontmatter `id:` line is returned unchanged.
|
|
213
|
+
* @param {string} content
|
|
214
|
+
* @param {string} id
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
export function withNoteId(content, id) {
|
|
218
|
+
if (!content.startsWith('---')) return content;
|
|
219
|
+
const end = content.indexOf('\n---', 3);
|
|
220
|
+
if (end === -1) return content;
|
|
221
|
+
const head = content.slice(0, end);
|
|
222
|
+
const nextHead = head.replace(/^id:.*$/m, `id: ${yamlNoteId(id)}`);
|
|
223
|
+
return nextHead === head ? content : nextHead + content.slice(end);
|
|
224
|
+
}
|
|
225
|
+
|
|
134
226
|
/** Derive the first 8 chars of a UUID (strip hyphens, take first 8 hex chars). */
|
|
135
227
|
export function uuidPrefix8(id) {
|
|
136
228
|
return id.replace(/-/g, '').slice(0, 8);
|
|
@@ -198,7 +290,8 @@ export function resolveSourceSessionLink(source_session, opts = {}) {
|
|
|
198
290
|
if (!raw || raw === 'unknown') return { isLink: false, target: raw || 'unknown' };
|
|
199
291
|
// Provenance tags like "agent-proposed@wave-1" are never session notes.
|
|
200
292
|
if (/@/.test(raw)) return { isLink: false, target: raw };
|
|
201
|
-
|
|
293
|
+
// Session targets retain the same historical identity as their note filenames.
|
|
294
|
+
const slug = legacySubjectToSlug(raw) || raw;
|
|
202
295
|
if (typeof opts.noteExists === 'function') {
|
|
203
296
|
// EXISTENCE-based (authoritative when a predicate is supplied).
|
|
204
297
|
return opts.noteExists(slug) ? { isLink: true, target: slug } : { isLink: false, target: raw };
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { existsSync, readFileSync } from 'node:fs';
|
|
26
|
-
import
|
|
26
|
+
import { resolveLedgerPath } from './ledger-path.mjs';
|
|
27
27
|
|
|
28
28
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
29
29
|
|
|
@@ -95,12 +95,9 @@ export function checkVaultStaleness({ repoRoot, now = Date.now() } = {}) {
|
|
|
95
95
|
try {
|
|
96
96
|
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'metrics',
|
|
102
|
-
'vault-staleness.jsonl',
|
|
103
|
-
);
|
|
98
|
+
// #1722: the ledger the probe writes (skills/discovery/probes/vault-staleness.mjs)
|
|
99
|
+
// — the main checkout's when repoRoot is a linked worktree that ignores it.
|
|
100
|
+
const jsonlPath = resolveLedgerPath('.orchestrator/metrics/vault-staleness.jsonl', repoRoot);
|
|
104
101
|
|
|
105
102
|
if (!existsSync(jsonlPath)) return null;
|
|
106
103
|
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* canonical-vault refusal (#1450). Plain Node ESM. No external deps.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
51
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'node:fs';
|
|
52
52
|
import os from 'node:os';
|
|
53
53
|
import path from 'node:path';
|
|
54
54
|
|
|
@@ -56,6 +56,7 @@ import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
|
56
56
|
import { isLockLive, readLock, DEFAULT_TTL_HOURS } from '../session-lock.mjs';
|
|
57
57
|
import { stableHostname } from '../host-identity.mjs';
|
|
58
58
|
import { readRegistry, repoPathHash, isRegistryEntryFresh } from '../session-registry.mjs';
|
|
59
|
+
import { digestSha256 } from '../crypto-digest-utils.mjs';
|
|
59
60
|
import { parseFrontmatter } from '../vault-mirror/utils.mjs';
|
|
60
61
|
import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
61
62
|
import { validatePathInsideProject } from '../path-utils.mjs';
|
|
@@ -134,10 +135,29 @@ const CASE_INSENSITIVE_FS = process.platform === 'darwin' || process.platform ==
|
|
|
134
135
|
* @returns {string|null} `KEY_LENGTH`-char hex prefix, or null for an unusable path.
|
|
135
136
|
*/
|
|
136
137
|
export function boardKey(repoRoot) {
|
|
138
|
+
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
|
|
139
|
+
try {
|
|
140
|
+
let resolved = path.resolve(repoRoot);
|
|
141
|
+
try { resolved = realpathSync.native(resolved); } catch { /* Keep unavailable paths' legacy identity. */ }
|
|
142
|
+
// Fold AFTER realpath, then hash directly: repoPathHash would restore
|
|
143
|
+
// on-disk casing and change existing case-folded board keys on macOS.
|
|
144
|
+
return digestSha256(CASE_INSENSITIVE_FS ? resolved.toLowerCase() : resolved).slice(0, KEY_LENGTH);
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Pre-#1557 lexical board key, used to migrate rows when alias paths now
|
|
152
|
+
* resolve to a canonical key. BV-004 revisit/deletion trigger: remove once a
|
|
153
|
+
* host-wide board audit confirms no rows written before the #1557 canonical-key
|
|
154
|
+
* rollout remain for 30 consecutive days; age alone does not prove migration.
|
|
155
|
+
*/
|
|
156
|
+
function legacyBoardKey(repoRoot) {
|
|
137
157
|
if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
|
|
138
158
|
try {
|
|
139
159
|
const resolved = path.resolve(repoRoot);
|
|
140
|
-
return
|
|
160
|
+
return digestSha256(CASE_INSENSITIVE_FS ? resolved.toLowerCase() : resolved).slice(0, KEY_LENGTH);
|
|
141
161
|
} catch {
|
|
142
162
|
return null;
|
|
143
163
|
}
|
|
@@ -435,6 +455,7 @@ export async function collectRows({ repos, now = new Date(), registry, priorStat
|
|
|
435
455
|
rows.push({
|
|
436
456
|
repo: repoName,
|
|
437
457
|
key,
|
|
458
|
+
legacyKey: legacyBoardKey(repo.repoRoot), // Internal migration identity; never rendered.
|
|
438
459
|
status,
|
|
439
460
|
session: status === STATUS_FREI ? null : session,
|
|
440
461
|
branch: status === STATUS_FREI ? null : branch,
|
|
@@ -1098,8 +1119,8 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
1098
1119
|
// skips `frei` candidates and the TTL pass only rewrites `status`, never
|
|
1099
1120
|
// removes a row), and the board would grow a permanent duplicate per repo.
|
|
1100
1121
|
// So a fresh row first claims its hash slot; if that slot is new, it ADOPTS
|
|
1101
|
-
// the legacy name slot for the same folded name
|
|
1102
|
-
//
|
|
1122
|
+
// the legacy name slot for the same folded name and the pre-#1557 lexical
|
|
1123
|
+
// key slot — one board write converts the row for that repo.
|
|
1103
1124
|
const merged = new Map(staleRederivedRows);
|
|
1104
1125
|
for (const row of rows) {
|
|
1105
1126
|
const slot = row.key ? hashSlot(row.key) : nameSlot(row.repo);
|
|
@@ -1107,6 +1128,13 @@ async function mirrorBoardInner({ repoRoot, repos, explicitStatus, now = new Dat
|
|
|
1107
1128
|
// First keyed write for this repo — take over its legacy row rather than
|
|
1108
1129
|
// rendering a second one beside it.
|
|
1109
1130
|
merged.delete(nameSlot(row.repo));
|
|
1131
|
+
if (row.legacyKey && row.legacyKey !== row.key) {
|
|
1132
|
+
// An 8-char key can collide: adopt the legacy-key row only when it
|
|
1133
|
+
// names this repo, never another repo's row that shares the prefix.
|
|
1134
|
+
const legacySlot = hashSlot(row.legacyKey);
|
|
1135
|
+
const legacyRow = merged.get(legacySlot);
|
|
1136
|
+
if (legacyRow && foldKey(legacyRow.repo) === foldKey(row.repo)) merged.delete(legacySlot);
|
|
1137
|
+
}
|
|
1110
1138
|
}
|
|
1111
1139
|
merged.set(slot, row);
|
|
1112
1140
|
}
|
|
@@ -1311,7 +1339,8 @@ export function buildSweepRepos(candidates, { thisRepoRoot } = {}) {
|
|
|
1311
1339
|
* @param {object} [opts]
|
|
1312
1340
|
* @param {string} opts.repoRoot — the calling repo (always included in the sweep).
|
|
1313
1341
|
* @param {string} [opts.startDir] — enumeration root; omitted in production so
|
|
1314
|
-
* {@link enumerateCandidates} defaults to `getConfinementRoot()` (~/Projects
|
|
1342
|
+
* {@link enumerateCandidates} defaults to `getConfinementRoot()` (~/Projects
|
|
1343
|
+
* unless the host sets `projects-root`, #1562).
|
|
1315
1344
|
* Test seam only — do NOT compute `path.dirname(repoRoot)` here.
|
|
1316
1345
|
* @param {Date|number} [opts.now] — clock seam. A `Date` is used as-is; a finite
|
|
1317
1346
|
* number is treated as epoch-ms (matching {@link collectRows}'s own
|
|
@@ -31,12 +31,15 @@ import { readFile } from 'node:fs/promises';
|
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
|
|
33
33
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
34
|
-
import { parseStateMd, parseMissionStatus
|
|
34
|
+
import { parseStateMd, parseMissionStatus } from '../state-md.mjs';
|
|
35
|
+
import { resolveStateMdWriteTarget, warnForeignStateMd } from '../state-md/frontmatter-mutators.mjs';
|
|
35
36
|
import {
|
|
36
37
|
parseFrontmatter,
|
|
37
38
|
toDate,
|
|
38
39
|
yamlQuoteIfNeeded,
|
|
39
40
|
subjectToSlug,
|
|
41
|
+
noteIdFor,
|
|
42
|
+
yamlNoteId,
|
|
40
43
|
} from '../vault-mirror/utils.mjs';
|
|
41
44
|
import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
|
|
42
45
|
import { readConfigFile, parseSessionConfig } from '../config.mjs';
|
|
@@ -317,7 +320,7 @@ export function renderNarrative(opts) {
|
|
|
317
320
|
|
|
318
321
|
// Frontmatter — `_generator` LAST (markdown-writer / render-sessions convention).
|
|
319
322
|
lines.push('---');
|
|
320
|
-
lines.push(`id: ${noteId}`);
|
|
323
|
+
lines.push(`id: ${yamlNoteId(noteIdFor(null, noteId))}`);
|
|
321
324
|
lines.push('type: session');
|
|
322
325
|
lines.push(`title: ${title}`);
|
|
323
326
|
lines.push(`created: ${createdValue}`);
|
|
@@ -380,6 +383,31 @@ export function resolveNarrativePath(vaultDir, repoSlug) {
|
|
|
380
383
|
return path.join(vaultDir, '01-projects', repoSlug, '_session-narrative.md');
|
|
381
384
|
}
|
|
382
385
|
|
|
386
|
+
/**
|
|
387
|
+
* The `created` date of the narrative already at `outputPath` when THIS generator
|
|
388
|
+
* wrote it (#1602; same rule as board-writer.mjs). A hand-authored or foreign
|
|
389
|
+
* file, a missing one, or a malformed date yields `undefined` — the render then
|
|
390
|
+
* stamps today, exactly as on a first write. A read failure is not fatal here:
|
|
391
|
+
* `writeNarrative` reads the file again and owns that error contract.
|
|
392
|
+
*
|
|
393
|
+
* @param {string} outputPath
|
|
394
|
+
* @param {{ readFileSync?: Function, existsSync?: Function }} [injectedFs]
|
|
395
|
+
* @returns {string|undefined} `YYYY-MM-DD`
|
|
396
|
+
*/
|
|
397
|
+
function existingNarrativeCreated(outputPath, injectedFs) {
|
|
398
|
+
const fsExists = injectedFs?.existsSync ?? existsSync;
|
|
399
|
+
const fsReadFile = injectedFs?.readFileSync ?? readFileSync;
|
|
400
|
+
try {
|
|
401
|
+
if (!fsExists(outputPath)) return undefined;
|
|
402
|
+
const fm = parseFrontmatter(fsReadFile(outputPath, 'utf8'));
|
|
403
|
+
if (fm?.['_generator'] !== GENERATOR_MARKER) return undefined;
|
|
404
|
+
const created = fm['created'];
|
|
405
|
+
return typeof created === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(created) ? created : undefined;
|
|
406
|
+
} catch {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
383
411
|
// ── Idempotent write ─────────────────────────────────────────────────────────────
|
|
384
412
|
|
|
385
413
|
/**
|
|
@@ -393,6 +421,26 @@ function normalizeUpdated(content) {
|
|
|
393
421
|
return content.replace(/^(updated:\s*)(.+)$/m, `$1${UPDATED_PLACEHOLDER}`);
|
|
394
422
|
}
|
|
395
423
|
|
|
424
|
+
/**
|
|
425
|
+
* The comparison form of a narrative for the noop check: {@link normalizeUpdated}
|
|
426
|
+
* plus the `id:` line dropped to a bare `id:` (#1601, release-review HOCH 2).
|
|
427
|
+
* The id differs between generations of the renderer in more than one way: the
|
|
428
|
+
* render now quotes it (`id: "x"`) where older files carry `id: x`, and
|
|
429
|
+
* `noteIdFor` caps it, so a pre-cap file can carry a longer value. Any such
|
|
430
|
+
* difference made the whole document "changed", which skipped the `[REDACTED]`
|
|
431
|
+
* comparison below and rewrote the file — with the raw secret whenever it was
|
|
432
|
+
* missing from the current env (#1025). The id is derived from the note's
|
|
433
|
+
* path, not from its content, so no id difference may ever trigger a rewrite;
|
|
434
|
+
* a schema-breaking id on disk is the job of `vault-mirror/migrate-ids.mjs`,
|
|
435
|
+
* which changes that one line and nothing else.
|
|
436
|
+
*
|
|
437
|
+
* @param {string} content
|
|
438
|
+
* @returns {string}
|
|
439
|
+
*/
|
|
440
|
+
function normalizeForNoopCompare(content) {
|
|
441
|
+
return normalizeUpdated(content).replace(/^id:.*$/m, 'id:');
|
|
442
|
+
}
|
|
443
|
+
|
|
396
444
|
/**
|
|
397
445
|
* Write the narrative file with idempotency guards (mirrors gitlab-portfolio
|
|
398
446
|
* markdown-writer.mjs contract).
|
|
@@ -501,8 +549,9 @@ export function writeNarrative(opts) {
|
|
|
501
549
|
// #1025 — identical once every `[REDACTED]` span already on disk is read
|
|
502
550
|
// as a wildcard. See the guard-order note above for why the second arm is
|
|
503
551
|
// load-bearing rather than a nicety.
|
|
504
|
-
|
|
505
|
-
const
|
|
552
|
+
// #1601: the id line is left out of the compare — see normalizeForNoopCompare.
|
|
553
|
+
const existingNormalized = normalizeForNoopCompare(existingContent);
|
|
554
|
+
const candidateNormalized = normalizeForNoopCompare(content);
|
|
506
555
|
if (
|
|
507
556
|
existingNormalized === candidateNormalized ||
|
|
508
557
|
matchesModuloRedaction(existingNormalized, candidateNormalized)
|
|
@@ -963,8 +1012,15 @@ async function runNarrativeMirror(opts) {
|
|
|
963
1012
|
return { result: { action: 'skipped-invalid-path', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
964
1013
|
}
|
|
965
1014
|
|
|
966
|
-
// Read STATE.md (best-effort; absent STATE.md → nothing to mirror).
|
|
967
|
-
|
|
1015
|
+
// Read STATE.md (best-effort; absent STATE.md → nothing to mirror). #1622:
|
|
1016
|
+
// another platform's STATE.md not proven to be this process's counts as absent.
|
|
1017
|
+
// #1640: `rejected` is also set when the target ITSELF is foreign
|
|
1018
|
+
// (`blocked`) — never read and mirror that file either.
|
|
1019
|
+
const { path: stateMdPath, rejected } = resolveStateMdWriteTarget(repoRoot);
|
|
1020
|
+
if (rejected) {
|
|
1021
|
+
warnForeignStateMd('mirrorNarrative', rejected, stateMdPath);
|
|
1022
|
+
return { result: { action: 'skipped-no-statemd', path: outputPath }, needleCount: masker.needleCount, hits: 0, dryRun };
|
|
1023
|
+
}
|
|
968
1024
|
let stateContents;
|
|
969
1025
|
try {
|
|
970
1026
|
stateContents = await readFile(stateMdPath, 'utf8');
|
|
@@ -976,7 +1032,10 @@ async function runNarrativeMirror(opts) {
|
|
|
976
1032
|
// parse (so `[REDACTED]` can never break it) and before the render, the
|
|
977
1033
|
// idempotency comparison and the write. See maskNarrative above.
|
|
978
1034
|
const { narrative, hits } = maskNarrative(extractNarrative(stateContents), masker);
|
|
979
|
-
|
|
1035
|
+
// #1602: `created` is the day the narrative was FIRST written — carried over
|
|
1036
|
+
// from the file on disk, so only `updated` moves between runs.
|
|
1037
|
+
const createdIso = existingNarrativeCreated(outputPath, injectedFs);
|
|
1038
|
+
const content = renderNarrative({ repo: repoName, narrative, now, createdIso });
|
|
980
1039
|
|
|
981
1040
|
// `chars` measures the document THIS call rendered — so it is present on
|
|
982
1041
|
// `skipped-noop` and `dry-run` too, where the render happened but nothing
|