session-orchestrator 3.23.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-fences.mjs — the ONE fenced-code-block tracker shared by every
|
|
3
|
+
* Markdown-scanning validator/extractor in this repo (#1181).
|
|
4
|
+
*
|
|
5
|
+
* ## Why one module
|
|
6
|
+
*
|
|
7
|
+
* The same regex pair + open/close comparison had drifted into four separate
|
|
8
|
+
* copies — `check-doc-cli-commands.mjs`, `check-skill-script-paths.mjs`,
|
|
9
|
+
* `check-vcs-repo-flag.mjs` and `auq/parse.mjs` — each tracking fence depth
|
|
10
|
+
* and language independently. A drifted copy is a silent one: nothing fails
|
|
11
|
+
* when copy #3 diverges from copy #1, because each copy only has to agree
|
|
12
|
+
* with itself.
|
|
13
|
+
*
|
|
14
|
+
* ## The rule this module encodes
|
|
15
|
+
*
|
|
16
|
+
* A fence opens on a line beginning with 3+ backticks or 3+ tildes, carrying
|
|
17
|
+
* an optional info string (most commonly a language tag). It closes on a
|
|
18
|
+
* line whose marker CHARACTER matches, whose LENGTH is at least the
|
|
19
|
+
* opener's, and whose info string is empty — CommonMark reserves the info
|
|
20
|
+
* string for the OPENING fence only, so a fence-shaped line that still
|
|
21
|
+
* carries one is fence CONTENT (most often a nested fence one level in), not
|
|
22
|
+
* a closer.
|
|
23
|
+
*
|
|
24
|
+
* ## The one real divergence between the four original copies, preserved as a parameter
|
|
25
|
+
*
|
|
26
|
+
* Three of the four copies anchor the fence-line regex only at the START of
|
|
27
|
+
* the line — trailing text after the info string is simply not captured,
|
|
28
|
+
* never rejected. `auq/parse.mjs`'s copy anchors at BOTH ends: a line
|
|
29
|
+
* carrying anything past the info string besides trailing whitespace is not
|
|
30
|
+
* recognised as a fence line at all. This is `{ wholeLine: true }` below —
|
|
31
|
+
* a real behavioural difference, not stylistic, so it stays a caller-chosen
|
|
32
|
+
* option rather than being silently resolved one way. Every other
|
|
33
|
+
* consumer-specific behaviour (the shell-language predicate, blockquote
|
|
34
|
+
* stripping, unbalanced-fence reporting) likewise stays at the call site —
|
|
35
|
+
* this module owns only the fence-line grammar itself.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** Fence languages whose body is shell. Both `check-doc-cli-commands.mjs` and
|
|
39
|
+
* `check-vcs-repo-flag.mjs` filtered on this identical literal set before
|
|
40
|
+
* extraction; centralised here rather than kept as two copies of the same
|
|
41
|
+
* five strings. */
|
|
42
|
+
export const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
|
|
43
|
+
|
|
44
|
+
/** Start-anchored: matches CommonMark, tolerates trailing info-string text. */
|
|
45
|
+
const FENCE_LINE_START_RE = /^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/;
|
|
46
|
+
|
|
47
|
+
/** Whole-line-anchored: nothing but whitespace may follow the info string. */
|
|
48
|
+
const FENCE_LINE_WHOLE_RE = /^[ \t]*(`{3,}|~{3,})[ \t]*([^\s`~]*)[ \t]*$/u;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Match a candidate fence-marker line.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} line
|
|
54
|
+
* @param {{ wholeLine?: boolean }} [options] `wholeLine: true` requires the
|
|
55
|
+
* ENTIRE line (after the marker) to be nothing but the info string plus
|
|
56
|
+
* trailing whitespace — the stricter reading `auq/parse.mjs` needs.
|
|
57
|
+
* Default `false` only anchors the START of the line, matching
|
|
58
|
+
* CommonMark and the three `check-*.mjs` validators.
|
|
59
|
+
* @returns {{ marker: string, length: number, info: string } | null}
|
|
60
|
+
*/
|
|
61
|
+
export function matchFenceLine(line, { wholeLine = false } = {}) {
|
|
62
|
+
const match = (wholeLine ? FENCE_LINE_WHOLE_RE : FENCE_LINE_START_RE).exec(line);
|
|
63
|
+
if (!match) return null;
|
|
64
|
+
return { marker: match[1][0], length: match[1].length, info: match[2] ?? '' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Does `candidate` close the fence opened by `open`? Same marker character,
|
|
69
|
+
* at least as long, and carrying no info string of its own.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ marker: string, length: number }} open
|
|
72
|
+
* @param {{ marker: string, length: number, info: string }} candidate
|
|
73
|
+
* @returns {boolean}
|
|
74
|
+
*/
|
|
75
|
+
export function closesFence(open, candidate) {
|
|
76
|
+
return candidate.marker === open.marker && candidate.length >= open.length && candidate.info === '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalise a fence's info string into a comparable language tag. Every
|
|
81
|
+
* caller that classified a fence by language did `.toLowerCase()` (never
|
|
82
|
+
* `.trim()`, since the capturing regex cannot include leading/trailing
|
|
83
|
+
* whitespace in the first place) before comparing against a language set —
|
|
84
|
+
* this makes that normalisation a single, explicit step instead of an
|
|
85
|
+
* implicit property of the extraction regex.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} info
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeLang(info) {
|
|
91
|
+
return String(info ?? '').trim().toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Strip a leading blockquote `>` chain so a quoted fence (`> \`\`\``) is
|
|
96
|
+
* still recognised as a fence line. Only `check-skill-script-paths.mjs`
|
|
97
|
+
* needs this — a fence inside a blockquote is still a fence there, but the
|
|
98
|
+
* other callers never scan quoted content.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} line
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function stripBlockquote(line) {
|
|
104
|
+
return line.replace(/^(?:\s*>)+\s?/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Walk `text` line by line, tracking fence state, and call `onLine` for
|
|
109
|
+
* every CONTENT line. Fence-marker lines themselves (the opener and the
|
|
110
|
+
* closer) are consumed by the tracker and never handed to the callback —
|
|
111
|
+
* all four original copies treated marker lines as structural, never as
|
|
112
|
+
* scannable content. A fence-shaped line that neither opens nor validly
|
|
113
|
+
* closes (a nested fence one level in) IS still content and reaches the
|
|
114
|
+
* callback, exactly as in the original copies.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} text
|
|
117
|
+
* @param {(line: string, state: { lineNumber: number, inFence: boolean, lang: string | null }) => void} onLine
|
|
118
|
+
* @param {{ wholeLine?: boolean, stripBlockquotes?: boolean }} [options]
|
|
119
|
+
* @returns {{ unbalancedFenceLine: number | null }} the 1-based line of a
|
|
120
|
+
* fence that opened and never closed by EOF, or `null` if every fence
|
|
121
|
+
* this walk saw was balanced.
|
|
122
|
+
*/
|
|
123
|
+
export function forEachLine(text, onLine, options = {}) {
|
|
124
|
+
const { wholeLine = false, stripBlockquotes = false } = options;
|
|
125
|
+
const lines = text.split('\n');
|
|
126
|
+
/** @type {{ marker: string, length: number, openLine: number } | null} */
|
|
127
|
+
let fence = null;
|
|
128
|
+
let lang = null;
|
|
129
|
+
|
|
130
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
131
|
+
const raw = lines[index];
|
|
132
|
+
const probe = stripBlockquotes ? stripBlockquote(raw) : raw;
|
|
133
|
+
const candidate = matchFenceLine(probe, { wholeLine });
|
|
134
|
+
if (candidate) {
|
|
135
|
+
if (fence === null) {
|
|
136
|
+
fence = { marker: candidate.marker, length: candidate.length, openLine: index + 1 };
|
|
137
|
+
lang = normalizeLang(candidate.info);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (closesFence(fence, candidate)) {
|
|
141
|
+
fence = null;
|
|
142
|
+
lang = null;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Otherwise it is fence content (a nested fence inside a wider one) — falls through.
|
|
146
|
+
}
|
|
147
|
+
onLine(raw, { lineNumber: index + 1, inFence: fence !== null, lang });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { unbalancedFenceLine: fence ? fence.openLine : null };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract fence BLOCKS (an open/close pair plus its body) rather than a
|
|
155
|
+
* per-line walk — the shape `auq/parse.mjs` needs, since it iterates fences
|
|
156
|
+
* as units, not lines.
|
|
157
|
+
*
|
|
158
|
+
* An unbalanced fence (opens, never closes) contributes NO block, mirroring
|
|
159
|
+
* the original `fencesOf()` contract: it silently drops the dangling opener
|
|
160
|
+
* rather than reporting it. Unlike `check-skill-script-paths.mjs`, whose
|
|
161
|
+
* unterminated fence is a doc DEFECT worth its own finding, `auq/parse.mjs`
|
|
162
|
+
* scans AskUserQuestion blocks in production code and prose, where an
|
|
163
|
+
* unterminated fence was never treated as a reportable condition in its own
|
|
164
|
+
* right — only as "no block found here".
|
|
165
|
+
*
|
|
166
|
+
* @param {string} text
|
|
167
|
+
* @param {{ wholeLine?: boolean }} [options]
|
|
168
|
+
* @returns {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>}
|
|
169
|
+
*/
|
|
170
|
+
export function scanFenceBlocks(text, options = {}) {
|
|
171
|
+
const { wholeLine = false } = options;
|
|
172
|
+
const lines = text.split('\n');
|
|
173
|
+
/** @type {Array<{ openLine: number, closeLine: number, lang: string, bodyLines: string[], bodyStartLine: number }>} */
|
|
174
|
+
const blocks = [];
|
|
175
|
+
/** @type {{ marker: string, length: number, info: string, startIdx: number } | null} */
|
|
176
|
+
let open = null;
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
179
|
+
const candidate = matchFenceLine(lines[index], { wholeLine });
|
|
180
|
+
if (!candidate) continue;
|
|
181
|
+
if (open === null) {
|
|
182
|
+
open = { ...candidate, startIdx: index };
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!closesFence(open, candidate)) continue; // fence content (nested), not a close
|
|
186
|
+
blocks.push({
|
|
187
|
+
openLine: open.startIdx + 1,
|
|
188
|
+
closeLine: index + 1,
|
|
189
|
+
lang: open.info,
|
|
190
|
+
bodyLines: lines.slice(open.startIdx + 1, index),
|
|
191
|
+
bodyStartLine: open.startIdx + 2,
|
|
192
|
+
});
|
|
193
|
+
open = null;
|
|
194
|
+
}
|
|
195
|
+
return blocks;
|
|
196
|
+
}
|
|
@@ -1,17 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* template.mjs — Canonical .vault.yaml template renderer for vault-backfill.
|
|
3
3
|
*
|
|
4
|
-
* Reads the template once from projects-baseline; subsequent calls
|
|
4
|
+
* Reads the template once from a projects-baseline checkout; subsequent calls
|
|
5
|
+
* use the cache. The checkout is optional — see docs/baseline.md.
|
|
5
6
|
* Part of scripts/vault-backfill.mjs (Issue #241).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { readFileSync, existsSync } from 'node:fs';
|
|
9
10
|
import { homedir } from 'node:os';
|
|
10
|
-
import { resolve } from 'node:path';
|
|
11
|
+
import { dirname, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { resolveHostPath } from '../config/host-paths.mjs';
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
/** Path of the template RELATIVE to a projects-baseline checkout root. */
|
|
16
|
+
const TEMPLATE_REL_PATH = 'templates/shared/.vault.yaml.template';
|
|
17
|
+
|
|
18
|
+
/** This file lives at `<repoRoot>/scripts/lib/vault-backfill/`. */
|
|
19
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Candidate projects-baseline checkout roots. The baseline is optional and
|
|
23
|
+
* private (`docs/baseline.md`), so no host-specific directory name may be
|
|
24
|
+
* committed here.
|
|
25
|
+
*
|
|
26
|
+
* Two tiers, and the split is load-bearing:
|
|
27
|
+
*
|
|
28
|
+
* EXPLICIT — `PROJECTS_BASELINE_DIR`, else `SO_BASELINE_PATH` / `owner.yaml`
|
|
29
|
+
* `paths.baseline-path`. When the operator has SAID where the baseline is,
|
|
30
|
+
* that answer is used ALONE. Probing past a wrong explicit value would resolve
|
|
31
|
+
* a DIFFERENT baseline than the one named and report success — silently using
|
|
32
|
+
* a corpus nobody asked for is worse than the abort, and it would hide the
|
|
33
|
+
* typo forever.
|
|
34
|
+
*
|
|
35
|
+
* CONVENTION — the sibling checkout `scripts/sync-vault-schema.mjs` already
|
|
36
|
+
* uses, then the legacy `~/Projects` default this module shipped with. These
|
|
37
|
+
* are guesses, so probing among them is exactly right.
|
|
38
|
+
*
|
|
39
|
+
* @returns {string[]} never empty
|
|
40
|
+
*/
|
|
41
|
+
function baselineCandidates() {
|
|
42
|
+
const envDir = (process.env.PROJECTS_BASELINE_DIR || '').trim();
|
|
43
|
+
if (envDir) return [envDir];
|
|
44
|
+
const hostDir = resolveHostPath('baseline-path', null);
|
|
45
|
+
if (typeof hostDir === 'string' && hostDir.trim() !== '') return [hostDir.trim()];
|
|
46
|
+
return [
|
|
47
|
+
resolve(REPO_ROOT, '..', 'projects-baseline'),
|
|
48
|
+
resolve(homedir(), 'Projects/projects-baseline'),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolved absolute path of the canonical template.
|
|
54
|
+
*
|
|
55
|
+
* Import-time resolution is retained deliberately: the export is a plain string
|
|
56
|
+
* that `scripts/vault-backfill.mjs` and the tests both read directly. Ceiling:
|
|
57
|
+
* at most two `existsSync` calls at import. When no candidate exists the FIRST
|
|
58
|
+
* candidate is exported anyway, so `loadTemplate`'s die message names the path
|
|
59
|
+
* the operator most likely meant rather than `undefined`.
|
|
60
|
+
*/
|
|
61
|
+
export const TEMPLATE_PATH = (() => {
|
|
62
|
+
const candidates = baselineCandidates();
|
|
63
|
+
for (const base of candidates) {
|
|
64
|
+
const candidate = resolve(base, TEMPLATE_REL_PATH);
|
|
65
|
+
if (existsSync(candidate)) return candidate;
|
|
66
|
+
}
|
|
67
|
+
return resolve(candidates[0], TEMPLATE_REL_PATH);
|
|
68
|
+
})();
|
|
15
69
|
|
|
16
70
|
const TODAY = new Date().toISOString().slice(0, 10);
|
|
17
71
|
|
|
@@ -27,7 +81,10 @@ export function loadTemplate(dieFn) {
|
|
|
27
81
|
dieFn(
|
|
28
82
|
2,
|
|
29
83
|
`canonical template not found at ${TEMPLATE_PATH} — ` +
|
|
30
|
-
`
|
|
84
|
+
`the projects-baseline checkout is optional and private (see docs/baseline.md). ` +
|
|
85
|
+
`Point at it with owner.yaml \`paths.baseline-path\` (host-local, never committed), ` +
|
|
86
|
+
`the SO_BASELINE_PATH env var, or PROJECTS_BASELINE_DIR; a sibling checkout at ` +
|
|
87
|
+
`../projects-baseline is picked up automatically.`,
|
|
31
88
|
);
|
|
32
89
|
}
|
|
33
90
|
|
|
@@ -213,6 +213,37 @@ function learningContentMatches(existingContent, renderedContent) {
|
|
|
213
213
|
);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Would the process-wide masker (see `ensureMasker` below) still change `text`
|
|
218
|
+
* if it ran again right now? (#1028 residue 1)
|
|
219
|
+
*
|
|
220
|
+
* Used as a LAST guard before returning `skipped-noop` in BOTH `processLearning`
|
|
221
|
+
* and `processSession` — the session channel's two skip sites were date-only
|
|
222
|
+
* until the W4 review of #1028 (see the module note above).
|
|
223
|
+
* `learningContentMatches` above compares only five canonical fields, so it
|
|
224
|
+
* cannot see a raw secret sitting in a NON-canonical field (`evidence` is the
|
|
225
|
+
* realistic one — see the KNOWN-RESIDUAL note on `maskEntrySecrets` below): a
|
|
226
|
+
* candidate that matches on those five fields can still be an on-disk leak.
|
|
227
|
+
* When this returns `true`, the caller must NOT skip — it falls through to the
|
|
228
|
+
* write path so the note is re-rendered (and re-masked) from the current entry.
|
|
229
|
+
*
|
|
230
|
+
* NAMED CEILING: this only heals a needle that is still present in the CURRENT
|
|
231
|
+
* process env. A needle that has since been rotated out of the env no longer
|
|
232
|
+
* masks to anything different, so `mask(text) === text` and this returns
|
|
233
|
+
* `false` — that needle's leak is healed only by an explicit `force: true`
|
|
234
|
+
* re-mirror (which bypasses the comparison entirely), never by a plain re-run.
|
|
235
|
+
*
|
|
236
|
+
* COST: one extra `mask()` call per candidate no-op — cheap relative to the
|
|
237
|
+
* read/parse/render already done to reach this check, and only paid on the
|
|
238
|
+
* no-op path (a real content change already writes without reaching here).
|
|
239
|
+
*
|
|
240
|
+
* @param {string} text — on-disk note content to probe
|
|
241
|
+
* @returns {boolean} true when masking `text` again would produce different output
|
|
242
|
+
*/
|
|
243
|
+
function maskerWouldChange(text) {
|
|
244
|
+
return ensureMasker().mask(text) !== text;
|
|
245
|
+
}
|
|
246
|
+
|
|
216
247
|
// ── repo derivation ───────────────────────────────────────────────────────────
|
|
217
248
|
|
|
218
249
|
/**
|
|
@@ -451,36 +482,54 @@ export function getMaskerStats() {
|
|
|
451
482
|
* as a wildcard (see that function) so this direction resolves to
|
|
452
483
|
* `skipped-noop` again.
|
|
453
484
|
*
|
|
454
|
-
*
|
|
455
|
-
* That revision claimed a COLD-START
|
|
456
|
-
* run without the env writes the raw
|
|
457
|
-
* and the note freezes. Measured, that
|
|
458
|
-
* on-disk side `matchesModuloRedaction`
|
|
459
|
-
* canonical fields differ, and the run
|
|
460
|
-
* asymmetry is still deliberate (an on-disk
|
|
461
|
-
* an on-disk raw value is evidence of
|
|
462
|
-
* anything the field comparison can see.
|
|
463
|
-
*
|
|
464
|
-
* What
|
|
465
|
-
*
|
|
466
|
-
* `
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
485
|
+
* RESIDUAL (#1028), NOW HEALED BY A MASK RE-PROBE — and NOT the residual an
|
|
486
|
+
* earlier revision of this note named. That revision claimed a COLD-START
|
|
487
|
+
* FREEZE over the CANONICAL fields: first run without the env writes the raw
|
|
488
|
+
* value, later runs render `[REDACTED]`, and the note freezes. Measured, that
|
|
489
|
+
* direction HEALS: with no marker on the on-disk side `matchesModuloRedaction`
|
|
490
|
+
* returns false at its first line, the canonical fields differ, and the run
|
|
491
|
+
* writes the masked content. The asymmetry is still deliberate (an on-disk
|
|
492
|
+
* redaction is evidence a mask ran; an on-disk raw value is evidence of
|
|
493
|
+
* nothing) — it simply does not freeze anything the field comparison can see.
|
|
494
|
+
*
|
|
495
|
+
* What the field comparison ALONE cannot see: `learningContentMatches`
|
|
496
|
+
* compares exactly five canonical fields — `status`, `expires`, `confidence`,
|
|
497
|
+
* `insight`, `source_session`. A raw secret sitting in any OTHER field
|
|
498
|
+
* (`evidence` is the realistic one; it is agent-authored free text and it is
|
|
499
|
+
* rendered into the note) leaves all five identical between the raw on-disk
|
|
500
|
+
* note and the masked candidate — the comparison alone reports a match.
|
|
501
|
+
*
|
|
502
|
+
* THE FIX — IN BOTH GENERATORS, which is the half an earlier revision of
|
|
503
|
+
* this note got wrong: it described the guard as a `processLearning` matter,
|
|
504
|
+
* and `processSession` shipped with two purely DATE-based `skipped-noop`
|
|
505
|
+
* returns beside it. The session note's narrative body is the LARGER
|
|
506
|
+
* free-text surface of the two, so that omission was the bigger half of the
|
|
507
|
+
* leak. Every `skipped-noop` return in BOTH `processLearning` (legacy-flat,
|
|
508
|
+
* disambig-collision, same-id) and `processSession` (legacy-flat, same-id)
|
|
509
|
+
* is now gated by `maskerWouldChange` (see that function above). When the
|
|
510
|
+
* on-disk content would still be changed by the CURRENT masker, the run
|
|
511
|
+
* falls through to the write path and re-renders (masked) instead of
|
|
512
|
+
* skipping — so a plain re-mirror heals the leak on its own; `force: true`
|
|
513
|
+
* is no longer the only escape hatch. The disambig-collision branch's
|
|
514
|
+
* `!force` guard now also matches the same-id and legacy-flat branches (it
|
|
515
|
+
* previously lacked one, so `force: true` was silently ignored there).
|
|
516
|
+
*
|
|
517
|
+
* THE LEGACY-FLAT BRANCH NEEDS ONE MORE STEP (#1028 residue 2). Falling
|
|
518
|
+
* through there writes the NAMESPACED path, and from the next run on
|
|
519
|
+
* `existsSync(targetPath)` is true — the flat file is never read again. A
|
|
520
|
+
* guard alone would therefore leave the plaintext on disk and merely make it
|
|
521
|
+
* unreachable to the heal, with the ledger reporting a clean write. Both
|
|
522
|
+
* generators now re-render the legacy flat note IN PLACE (masked) before
|
|
523
|
+
* falling through, and mark the resulting action `healed_legacy_flat: true`.
|
|
524
|
+
* Rewriting rather than deleting is what the deferred-migration decision
|
|
525
|
+
* above asks for: that decision is about not MOVING the note, which a masked
|
|
526
|
+
* re-render preserves and a deletion would silently undo.
|
|
527
|
+
* Revisit-Trigger: `maskerWouldChange` only detects needles present in the
|
|
528
|
+
* CURRENT process env — a needle since rotated out of the env is not seen as
|
|
529
|
+
* still-leaking and is healed only by an explicit `force: true` re-mirror.
|
|
530
|
+
* Widen this (e.g. persist a needle-shape fingerprint, or diff the whole
|
|
531
|
+
* rendered body) the first time a rotated-out secret is observed staying on
|
|
532
|
+
* disk after a plain re-run.
|
|
484
533
|
*
|
|
485
534
|
* FRONTMATTER AND BODY ARE TREATED IDENTICALLY. A credential is exactly as
|
|
486
535
|
* published in `title:` as it is under `## Insight` — both live in the same
|
|
@@ -636,22 +685,48 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
636
685
|
// duplicating the note. The deferred-migration decision means we only skip;
|
|
637
686
|
// we do NOT move the flat note into the namespaced dir here.
|
|
638
687
|
const legacyFlatPath = join(resolve(vaultDir), '40-learnings', `${slug}.md`);
|
|
688
|
+
// Set when a still-leaking legacy flat note was healed in place below, so the
|
|
689
|
+
// action emitted for THIS entry can say so (see the write sites at the end).
|
|
690
|
+
let healedLegacyFlat = false;
|
|
639
691
|
if (!existsSync(targetPath) && existsSync(legacyFlatPath)) {
|
|
640
692
|
const legacyContent = readFileSync(legacyFlatPath, 'utf8');
|
|
641
693
|
const legacyFm = parseFrontmatter(legacyContent);
|
|
642
694
|
// Only skip if the flat note is ours (has our generator marker and matching id).
|
|
643
695
|
if (legacyFm && legacyFm['_generator'] === GENERATOR_MARKER && legacyFm['id'] === slug) {
|
|
644
696
|
const entryUpdated = toDate(dateSource);
|
|
697
|
+
// #1028 residue 2 (W4 review HIGH-2): the leak probe must run on EVERY
|
|
698
|
+
// path that leaves this branch, not only the date-not-advanced one — every
|
|
699
|
+
// one of them falls through to the NAMESPACED path, after which
|
|
700
|
+
// `existsSync(targetPath)` is true forever and this legacy file is never
|
|
701
|
+
// read again. A raw secret left in it would become unreachable to the
|
|
702
|
+
// self-heal while staying fully published in the tracked vault.
|
|
703
|
+
const legacyStillLeaks = maskerWouldChange(legacyContent);
|
|
645
704
|
if (!force && legacyFm['updated'] && legacyFm['updated'] >= entryUpdated) {
|
|
646
705
|
// Date has not advanced — but content may have changed (confidence, insight, etc.).
|
|
647
706
|
// Render the candidate and compare canonical fields before deciding to skip.
|
|
648
707
|
const candidateContent = generator(entry, slug, generatorOpts);
|
|
649
|
-
if (learningContentMatches(legacyContent, candidateContent)) {
|
|
708
|
+
if (learningContentMatches(legacyContent, candidateContent) && !legacyStillLeaks) {
|
|
709
|
+
// #1028 residue 1: the five-field compare cannot see a raw secret
|
|
710
|
+
// sitting in a non-canonical field (e.g. `evidence`) — the
|
|
711
|
+
// `legacyStillLeaks` probe above is what makes this match trustworthy.
|
|
650
712
|
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: legacyFlatPath, id: slug });
|
|
651
713
|
}
|
|
652
|
-
// Content differs
|
|
714
|
+
// Content differs, or the on-disk note still leaks under the current
|
|
715
|
+
// env — fall through to write into the namespaced path.
|
|
653
716
|
}
|
|
654
717
|
// Updated date would advance or content changed — fall through to write into the namespaced path.
|
|
718
|
+
//
|
|
719
|
+
// Heal the leaking legacy note IN PLACE first. Rewriting is the option
|
|
720
|
+
// consistent with the deferred-migration rationale above: that decision is
|
|
721
|
+
// about not MOVING the note (the flat path stays authoritative for older
|
|
722
|
+
// vaults and for hand-made links into it), which a masked re-render
|
|
723
|
+
// preserves and a deletion would silently undo. Deleting an operator-visible
|
|
724
|
+
// vault file to fix a leak also trades one irreversible loss for another —
|
|
725
|
+
// the content here is already ours (generator marker + id checked above).
|
|
726
|
+
if (legacyStillLeaks) {
|
|
727
|
+
if (!dryRun) writeFileSync(legacyFlatPath, generator(entry, slug, generatorOpts), 'utf8');
|
|
728
|
+
healedLegacyFlat = true;
|
|
729
|
+
}
|
|
655
730
|
}
|
|
656
731
|
}
|
|
657
732
|
|
|
@@ -686,13 +761,22 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
686
761
|
return emitEntryAction(_lineNum, ctx, { action: 'skipped-handwritten', path: targetPath, id: entryId });
|
|
687
762
|
}
|
|
688
763
|
// Check updated advancement; if date has not advanced, also diff content.
|
|
764
|
+
// #1028 residue 1: this guard previously lacked the `!force &&` that the
|
|
765
|
+
// legacy-flat and same-id branches carry, so `force: true` was silently
|
|
766
|
+
// ignored on this one path — byte-identical guard shape to those two now.
|
|
689
767
|
const entryUpdated = toDate(dateSource);
|
|
690
|
-
if (disambigFm['updated'] && disambigFm['updated'] >= entryUpdated) {
|
|
768
|
+
if (!force && disambigFm['updated'] && disambigFm['updated'] >= entryUpdated) {
|
|
691
769
|
const candidateContent = generator(entry, slug, generatorOpts);
|
|
692
770
|
if (learningContentMatches(disambigContent, candidateContent)) {
|
|
693
|
-
|
|
771
|
+
// #1028 residue 1: probe the on-disk content against the CURRENT
|
|
772
|
+
// masker before trusting the five-field match — see maskerWouldChange.
|
|
773
|
+
const stillLeaks = maskerWouldChange(disambigContent);
|
|
774
|
+
if (!stillLeaks) {
|
|
775
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: disambigSlug });
|
|
776
|
+
}
|
|
694
777
|
}
|
|
695
|
-
// Content differs
|
|
778
|
+
// Content differs, or the on-disk note still leaks under the current
|
|
779
|
+
// env — fall through to write.
|
|
696
780
|
}
|
|
697
781
|
}
|
|
698
782
|
|
|
@@ -708,9 +792,15 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
708
792
|
if (!force && fm['updated'] && fm['updated'] >= entryUpdated) {
|
|
709
793
|
const candidateContent = generator(entry, slug, generatorOpts);
|
|
710
794
|
if (learningContentMatches(existingContent, candidateContent)) {
|
|
711
|
-
|
|
795
|
+
// #1028 residue 1: probe the on-disk content against the CURRENT masker
|
|
796
|
+
// before trusting the five-field match — see maskerWouldChange above.
|
|
797
|
+
const stillLeaks = maskerWouldChange(existingContent);
|
|
798
|
+
if (!stillLeaks) {
|
|
799
|
+
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: slug });
|
|
800
|
+
}
|
|
712
801
|
}
|
|
713
|
-
// Content differs
|
|
802
|
+
// Content differs, or the on-disk note still leaks under the current env —
|
|
803
|
+
// fall through to overwrite (same path as date-advance branch).
|
|
714
804
|
}
|
|
715
805
|
|
|
716
806
|
// Overwrite with advanced updated date (or forced re-render)
|
|
@@ -722,7 +812,15 @@ export async function processLearning(rawEntry, _lineNum, ctx) {
|
|
|
722
812
|
// File does not exist — create
|
|
723
813
|
const content = generator(entry, slug, generatorOpts);
|
|
724
814
|
if (!dryRun) writeFileSync(targetPath, content, 'utf8');
|
|
725
|
-
return emitEntryAction(_lineNum, ctx, {
|
|
815
|
+
return emitEntryAction(_lineNum, ctx, {
|
|
816
|
+
action: 'created',
|
|
817
|
+
path: targetPath,
|
|
818
|
+
id: slug,
|
|
819
|
+
// No PATH in the meta: the stdout payload's `path` is vault-relative on
|
|
820
|
+
// purpose (emitAction relativises it), a raw absolute path here would put
|
|
821
|
+
// the operator's home dir on stdout and into the ledger record.
|
|
822
|
+
...(healedLegacyFlat ? { meta: { healed_legacy_flat: true } } : {}),
|
|
823
|
+
});
|
|
726
824
|
}
|
|
727
825
|
|
|
728
826
|
export async function processSession(rawEntry, _lineNum, ctx) {
|
|
@@ -841,15 +939,30 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
841
939
|
// the namespaced path as absent. If a session note already exists flat
|
|
842
940
|
// (pre-namespace migration), skip creating a duplicate.
|
|
843
941
|
const legacyFlatPath = join(resolve(vaultDir), '50-sessions', `${session_id}.md`);
|
|
942
|
+
let healedLegacyFlat = false;
|
|
844
943
|
if (!existsSync(targetPath) && existsSync(legacyFlatPath)) {
|
|
845
944
|
const legacyContent = readFileSync(legacyFlatPath, 'utf8');
|
|
846
945
|
const legacyFm = parseFrontmatter(legacyContent);
|
|
847
946
|
if (legacyFm && legacyFm['_generator'] === GENERATOR_MARKER && legacyFm['id'] === session_id) {
|
|
848
947
|
const entryUpdated = toDate(entry.completed_at);
|
|
849
|
-
|
|
948
|
+
// #1028 residue 1, session channel (W4 review HIGH-1): the skip decision
|
|
949
|
+
// here is DATE-ONLY — it never looks at the note's content, so a raw
|
|
950
|
+
// secret written into the narrative before the env carried the needle
|
|
951
|
+
// would stay published forever. Same `maskerWouldChange` probe the
|
|
952
|
+
// learning channel carries; see that function above.
|
|
953
|
+
const legacyStillLeaks = maskerWouldChange(legacyContent);
|
|
954
|
+
if (!force && !legacyStillLeaks && legacyFm['updated'] && legacyFm['updated'] >= entryUpdated) {
|
|
850
955
|
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: legacyFlatPath, id: session_id });
|
|
851
956
|
}
|
|
852
|
-
// Updated date would advance
|
|
957
|
+
// Updated date would advance, or the on-disk note still leaks — fall
|
|
958
|
+
// through to write into the namespaced path. Heal the legacy note in place
|
|
959
|
+
// first (HIGH-2, same rationale as processLearning's legacy-flat branch):
|
|
960
|
+
// after this run `existsSync(targetPath)` is true, so the flat file is
|
|
961
|
+
// never read again and its plaintext would be unreachable to the heal.
|
|
962
|
+
if (legacyStillLeaks) {
|
|
963
|
+
if (!dryRun) writeFileSync(legacyFlatPath, renderedBody, 'utf8');
|
|
964
|
+
healedLegacyFlat = true;
|
|
965
|
+
}
|
|
853
966
|
}
|
|
854
967
|
}
|
|
855
968
|
|
|
@@ -871,7 +984,11 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
871
984
|
// Same generator: check id and updated
|
|
872
985
|
if (fm['id'] === session_id) {
|
|
873
986
|
const entryUpdated = toDate(entry.completed_at);
|
|
874
|
-
|
|
987
|
+
// #1028 residue 1, session channel (W4 review HIGH-1): probe the on-disk
|
|
988
|
+
// content against the CURRENT masker before trusting the date-only skip —
|
|
989
|
+
// the session note's narrative body is the larger free-text leak surface
|
|
990
|
+
// of the two channels. See maskerWouldChange above.
|
|
991
|
+
if (!force && !maskerWouldChange(existingContent) && fm['updated'] && fm['updated'] >= entryUpdated) {
|
|
875
992
|
return emitEntryAction(_lineNum, ctx, { action: 'skipped-noop', path: targetPath, id: session_id });
|
|
876
993
|
}
|
|
877
994
|
if (!dryRun) writeFileSync(targetPath, renderedBody, 'utf8');
|
|
@@ -882,5 +999,11 @@ export async function processSession(rawEntry, _lineNum, ctx) {
|
|
|
882
999
|
// File does not exist — create. Reuse the rendered body computed during the
|
|
883
1000
|
// quality-gate check (avoids a second generator invocation).
|
|
884
1001
|
if (!dryRun) writeFileSync(targetPath, renderedBody, 'utf8');
|
|
885
|
-
return emitEntryAction(_lineNum, ctx, {
|
|
1002
|
+
return emitEntryAction(_lineNum, ctx, {
|
|
1003
|
+
action: 'created',
|
|
1004
|
+
path: targetPath,
|
|
1005
|
+
id: session_id,
|
|
1006
|
+
// Boolean only — never a path; see the same note in processLearning.
|
|
1007
|
+
...(healedLegacyFlat ? { meta: { healed_legacy_flat: true } } : {}),
|
|
1008
|
+
});
|
|
886
1009
|
}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { emitEvent, sessionAttribution } from '../events.mjs';
|
|
41
|
-
import {
|
|
41
|
+
import { getProjectDir } from '../platform.mjs';
|
|
42
42
|
|
|
43
43
|
/** Canonical event name for a single vault-mirror JSONL entry. */
|
|
44
44
|
export const MIRROR_EVENT = 'orchestrator.vault.mirror_completed';
|
|
@@ -81,7 +81,7 @@ const present = (v) => v !== undefined && v !== null;
|
|
|
81
81
|
* @returns {{session_id?: string, semantic_session_id?: string}}
|
|
82
82
|
*/
|
|
83
83
|
function mirrorSessionAttribution() {
|
|
84
|
-
return sessionAttribution(
|
|
84
|
+
return sessionAttribution(getProjectDir());
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|