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
|
@@ -391,7 +391,14 @@ export function mergeCandidates({ candidates, repoRoot, storePath } = {}) {
|
|
|
391
391
|
* @param {number} [params.fallbackConfidence] - used only when no existing record is found.
|
|
392
392
|
* @param {string} [params.repoRoot]
|
|
393
393
|
* @param {string} [params.storePath]
|
|
394
|
-
* @returns {{ written: boolean, stamped: ReconcileCandidate|null }}
|
|
394
|
+
* @returns {{ written: boolean, stamped: ReconcileCandidate|null, alreadyProcessed?: boolean }}
|
|
395
|
+
* `stamped` is the record as PERSISTED (read back out of `mergeCandidates`'s
|
|
396
|
+
* merged array), never the one this call merely proposed — when a prior
|
|
397
|
+
* terminal verdict wins the dedupe, the two differ and only the former is
|
|
398
|
+
* true. `alreadyProcessed: true` marks exactly that case; `written` stays
|
|
399
|
+
* `true` there, because the store holds the intended terminal state and
|
|
400
|
+
* `written: false` means a WRITE FAILURE to the one production caller
|
|
401
|
+
* (`writer.mjs`, which turns it into an operator-visible error).
|
|
395
402
|
*/
|
|
396
403
|
export function markCandidateProcessed({
|
|
397
404
|
learningKey,
|
|
@@ -419,6 +426,7 @@ export function markCandidateProcessed({
|
|
|
419
426
|
stamped = { ...found, processed_at: stampAt, outcome: typeof outcome === 'string' ? outcome : (found.outcome ?? null) };
|
|
420
427
|
} else {
|
|
421
428
|
const slug = typeof fallbackSlug === 'string' ? fallbackSlug : '';
|
|
429
|
+
const finalOutcome = typeof outcome === 'string' ? outcome : null;
|
|
422
430
|
stamped = buildCandidate({
|
|
423
431
|
id:
|
|
424
432
|
typeof fallbackCandidateId === 'string' && fallbackCandidateId.length > 0
|
|
@@ -426,15 +434,40 @@ export function markCandidateProcessed({
|
|
|
426
434
|
: makeCandidateId(learningKey, slug),
|
|
427
435
|
learningKey,
|
|
428
436
|
slug,
|
|
429
|
-
|
|
437
|
+
// A minted record is TERMINAL from birth (`processed_at` is set two lines
|
|
438
|
+
// below), so `status` must agree with `outcome` instead of always saying
|
|
439
|
+
// `'proposed'` — a record reading `status:'proposed'` while carrying
|
|
440
|
+
// `outcome:'rejected'` is self-contradictory and misreads as a live
|
|
441
|
+
// proposal to anything that renders `status`. The vocabulary is exactly
|
|
442
|
+
// the two members of the ReconcileCandidate typedef; nothing new is
|
|
443
|
+
// invented here: `'rejected'` for the operator-rejection outcome
|
|
444
|
+
// (writer.mjs, issue #1042), `'proposed'` for the accepted outcomes
|
|
445
|
+
// (`'written'` / `'already-on-disk'`), where the proposal stood.
|
|
446
|
+
status: finalOutcome === 'rejected' ? 'rejected' : 'proposed',
|
|
430
447
|
reason: 'stamped without a prior sidecar record',
|
|
431
448
|
confidence: typeof fallbackConfidence === 'number' ? fallbackConfidence : 0,
|
|
432
449
|
createdAt: stampAt,
|
|
433
450
|
});
|
|
434
451
|
stamped.processed_at = stampAt;
|
|
435
|
-
stamped.outcome =
|
|
452
|
+
stamped.outcome = finalOutcome;
|
|
436
453
|
}
|
|
437
454
|
|
|
438
455
|
const result = mergeCandidates({ candidates: [stamped], repoRoot, storePath });
|
|
439
|
-
|
|
456
|
+
|
|
457
|
+
// Report what is PERSISTED, not what we asked for. `mergeCandidates`'s dedupe
|
|
458
|
+
// rule keeps an already-terminal existing record and DROPS the incoming one,
|
|
459
|
+
// so when `found` was already processed the store still holds the OLD
|
|
460
|
+
// `processed_at`/`outcome` — returning our freshly-built `stamped` would tell
|
|
461
|
+
// the caller a verdict that is nowhere on disk. `merged` IS the array that was
|
|
462
|
+
// just written, so reading the record back out of it is the honest answer.
|
|
463
|
+
const persisted = result.merged.find((rec) => rec && rec.learning_key === learningKey);
|
|
464
|
+
return {
|
|
465
|
+
written: result.written === true,
|
|
466
|
+
stamped: persisted ?? stamped,
|
|
467
|
+
// `true` iff a prior terminal verdict won and this call changed nothing on
|
|
468
|
+
// disk. `written` deliberately stays `true` in that case: the store IS in
|
|
469
|
+
// the intended terminal state, and writer.mjs treats `written:false` as a
|
|
470
|
+
// stamp FAILURE worth an operator-visible error.
|
|
471
|
+
alreadyProcessed: isTerminal(found),
|
|
472
|
+
};
|
|
440
473
|
}
|
|
@@ -457,26 +457,37 @@ function frontmatterRefusalReason(content) {
|
|
|
457
457
|
* not yet mature enough — silently, and with no way back short of editing the
|
|
458
458
|
* sidecar by hand.
|
|
459
459
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* `
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
460
|
+
* PRIMARY discriminator (#1153 P15): the explicit `operatorRejected: true`
|
|
461
|
+
* flag that `skills/session-end/phase-3-6-tail.md` step 6 stamps on every
|
|
462
|
+
* proposal the operator left unselected before concatenating it into the one
|
|
463
|
+
* `rejected` array. `engine.mjs` pushes its own rejections as
|
|
464
|
+
* `{learningKey, type, reason, status:'rejected'}` and never sets the flag, so
|
|
465
|
+
* the two shapes are now distinguishable by a marker rather than by inference.
|
|
466
|
+
*
|
|
467
|
+
* FALLBACK (@deprecated — remove once no supported skill body predates the P15
|
|
468
|
+
* stamp): when the flag is absent, fall back to the old implicit signal — the
|
|
469
|
+
* rendered `content`. Engine rejections never reach the renderer, so they never
|
|
470
|
+
* carry `content`/`slug`/`path`, while an operator-declined proposal is a full
|
|
471
|
+
* `ReconcileProposal` whose `content` is the rule text the AUQ showed him. The
|
|
472
|
+
* fallback exists because the skill prose and this module ship together but
|
|
473
|
+
* consumer repos may pin an OLDER `phase-3-6-tail.md` that emits no flag;
|
|
474
|
+
* dropping it immediately would silently stop stamping their operator
|
|
475
|
+
* rejections terminal (the exact #1042 bug). REMOVAL TRIGGER: the next major
|
|
476
|
+
* release in which no supported consumer ships a pre-P15 skill body.
|
|
477
|
+
*
|
|
478
|
+
* Either way the decision stays conservative in the same direction: a false
|
|
479
|
+
* negative costs nothing (the learning is re-proposed next run); a false
|
|
480
|
+
* positive would permanently suppress a learning that was only capped.
|
|
481
|
+
*
|
|
482
|
+
* @param {WriterRejectedItem & {content?: unknown, learningKey?: unknown, operatorRejected?: unknown}} item
|
|
474
483
|
* @returns {boolean}
|
|
475
484
|
*/
|
|
476
485
|
function isOperatorRejection(item) {
|
|
477
486
|
if (!item || typeof item !== 'object') return false;
|
|
478
|
-
if (typeof item.
|
|
479
|
-
|
|
487
|
+
if (typeof item.learningKey !== 'string' || item.learningKey.length === 0) return false;
|
|
488
|
+
if (item.operatorRejected === true) return true;
|
|
489
|
+
// @deprecated fallback — pre-P15 skill bodies emit no flag; see above.
|
|
490
|
+
return typeof item.content === 'string' && item.content.length > 0;
|
|
480
491
|
}
|
|
481
492
|
|
|
482
493
|
// ---------------------------------------------------------------------------
|
|
@@ -663,8 +674,12 @@ export async function writeApprovedRules({
|
|
|
663
674
|
repoRoot,
|
|
664
675
|
});
|
|
665
676
|
if (!stampResult.written) {
|
|
677
|
+
// Same discrimination as the rejected path below (identical shape,
|
|
678
|
+
// identical reader): an already-terminal record is not a lost stamp.
|
|
666
679
|
errors.push(
|
|
667
|
-
|
|
680
|
+
stampResult.alreadyProcessed
|
|
681
|
+
? `sidecar-stamp for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) wrote nothing — a prior terminal verdict is already on disk, so the rule file landed and the candidate stays terminal`
|
|
682
|
+
: `sidecar-stamp failed for "${item.path ?? item.slug}" (learningKey=${item.learningKey}) — rule file was written but the idempotency sidecar was not updated`,
|
|
668
683
|
);
|
|
669
684
|
}
|
|
670
685
|
}
|
|
@@ -724,8 +739,15 @@ export async function writeApprovedRules({
|
|
|
724
739
|
repoRoot,
|
|
725
740
|
});
|
|
726
741
|
if (!stampResult.written) {
|
|
742
|
+
// `alreadyProcessed` distinguishes the two failures that look
|
|
743
|
+
// identical from `written:false` alone. When a PRIOR terminal
|
|
744
|
+
// verdict is already on disk, the re-proposal warning is
|
|
745
|
+
// simply false — the sidecar holds the state we wanted — and
|
|
746
|
+
// telling the operator otherwise sends him after a non-bug.
|
|
727
747
|
errors.push(
|
|
728
|
-
|
|
748
|
+
stampResult.alreadyProcessed
|
|
749
|
+
? `sidecar-stamp for rejected "${item.learningKey}" wrote nothing — a prior terminal verdict is already on disk, so the rejection is recorded and it will NOT be re-proposed`
|
|
750
|
+
: `sidecar-stamp failed for rejected "${item.learningKey}" — the rejection was archived but the idempotency sidecar was not updated, so a later run may re-propose it`,
|
|
729
751
|
);
|
|
730
752
|
}
|
|
731
753
|
}
|
|
@@ -918,6 +918,28 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
|
|
|
918
918
|
);
|
|
919
919
|
}
|
|
920
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Prefix that marks an aggregate-sidecar record as a PEER SESSION's declared
|
|
923
|
+
* scope rather than one of this wave's own agents (#1195).
|
|
924
|
+
*
|
|
925
|
+
* SSOT: this constant lives here — in the module both the `--union` helper and
|
|
926
|
+
* `hooks/post-bash-write-verify.mjs` already import — so the union exclusion
|
|
927
|
+
* below and the hook's peer-write notice can never disagree about what a peer
|
|
928
|
+
* record IS.
|
|
929
|
+
*/
|
|
930
|
+
export const PEER_RECORD_PREFIX = 'peer-session-';
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Is this record id a PEER SESSION's record (see {@link PEER_RECORD_PREFIX})?
|
|
934
|
+
* Fail-closed: a non-string id is not a peer record.
|
|
935
|
+
*
|
|
936
|
+
* @param {unknown} id
|
|
937
|
+
* @returns {boolean}
|
|
938
|
+
*/
|
|
939
|
+
export function isPeerRecordId(id) {
|
|
940
|
+
return typeof id === 'string' && id.startsWith(PEER_RECORD_PREFIX);
|
|
941
|
+
}
|
|
942
|
+
|
|
921
943
|
/**
|
|
922
944
|
* Merge many agents' declared file scopes into ONE deduplicated, order-stable
|
|
923
945
|
* list — the mechanical form of "allowedPaths is the UNION of all agent file
|
|
@@ -943,6 +965,19 @@ export function suggestForScopeViolation(relPath, allowedCsv) {
|
|
|
943
965
|
* non-array / non-object members and non-string, empty entries are skipped.
|
|
944
966
|
* Pure, sync, no I/O — hook-safe per the module header.
|
|
945
967
|
*
|
|
968
|
+
* PEER RECORDS ARE EXCLUDED (#1195 follow-through). A record whose id starts
|
|
969
|
+
* with `peer-session-` declares a territory NO agent of this wave may write —
|
|
970
|
+
* it exists so a peer's paths take part in the DISJOINTNESS check and so
|
|
971
|
+
* `hooks/post-bash-write-verify.mjs` can name a peer write instead of alarming
|
|
972
|
+
* about it. Unioning it into `allowedPaths` would do the exact inverse: Gate 7
|
|
973
|
+
* (`hooks/enforce-scope.mjs`) would GRANT every agent of the wave write access
|
|
974
|
+
* to the peer's files, and the hook's peer branch would become dead code (a
|
|
975
|
+
* peer path can only reach it while it is OUTSIDE `allowedPaths`). The
|
|
976
|
+
* exclusion lives HERE — the one helper `--union` runs — rather than in the
|
|
977
|
+
* CLI, so every consumer of the union inherits it.
|
|
978
|
+
* `findScopeCollisions` deliberately does NOT filter: a peer/agent path
|
|
979
|
+
* collision is a real collision and must surface.
|
|
980
|
+
*
|
|
946
981
|
* @param {Array<string[]|{id?: string, files?: string[]}>} scopes
|
|
947
982
|
* @returns {string[]} deduplicated union in first-seen order
|
|
948
983
|
*/
|
|
@@ -954,6 +989,7 @@ export function unionFileScopes(scopes) {
|
|
|
954
989
|
let files = null;
|
|
955
990
|
if (Array.isArray(scope)) files = scope;
|
|
956
991
|
else if (scope !== null && typeof scope === 'object' && Array.isArray(scope.files)) {
|
|
992
|
+
if (isPeerRecordId(scope.id)) continue;
|
|
957
993
|
files = scope.files;
|
|
958
994
|
}
|
|
959
995
|
if (files === null) continue;
|
|
@@ -67,12 +67,31 @@ import { parseStateMd as defaultParseStateMd } from './state-md/yaml-parser.mjs'
|
|
|
67
67
|
// Constants
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The three real session MODES — deliberately NOT the schema's VALID_SESSION_TYPES,
|
|
72
|
+
* which since GitLab #1234 also carries `unknown`. This set answers a narrower
|
|
73
|
+
* question: "is `gathered.mode` a MEASUREMENT?". `unknown` must never pass it, or
|
|
74
|
+
* an events record carrying `mode: 'unknown'` would be recorded as a measured type
|
|
75
|
+
* (`_session_type_inferred` absent) when it is the opposite.
|
|
76
|
+
*/
|
|
77
|
+
const MEASURED_SESSION_MODES = new Set(['feature', 'deep', 'housekeeping']);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* session_type written when nothing in events.jsonl measured the mode
|
|
81
|
+
* (GitLab #1234). Always paired with `_session_type_inferred` + `_synthetic`.
|
|
82
|
+
*/
|
|
83
|
+
const UNMEASURED_SESSION_TYPE = 'unknown';
|
|
72
84
|
|
|
73
85
|
const EVENT_STARTED = 'orchestrator.session.started';
|
|
74
86
|
const EVENT_LOCK_ACQUIRED = 'orchestrator.session.lock.acquired';
|
|
87
|
+
// Both names for one generation (GitLab #1234): `hooks/on-stop.mjs` now emits
|
|
88
|
+
// `orchestrator.turn.stopped` as the canonical name and keeps the legacy
|
|
89
|
+
// `orchestrator.session.stopped` (with `deprecated: true`) beside it until
|
|
90
|
+
// 2027-03-06. A terminal-event probe must accept EITHER, or every session that
|
|
91
|
+
// closes after the legacy name is dropped silently loses its attested end and
|
|
92
|
+
// falls back to the flagged `lastEventMs` estimate.
|
|
75
93
|
const EVENT_STOPPED = 'orchestrator.session.stopped';
|
|
94
|
+
const EVENT_TURN_STOPPED = 'orchestrator.turn.stopped';
|
|
76
95
|
const EVENT_ENDED = 'orchestrator.session.ended';
|
|
77
96
|
|
|
78
97
|
const EVENTS_REL = ['.orchestrator', 'metrics', 'events.jsonl'];
|
|
@@ -108,14 +127,24 @@ function markerName(id) {
|
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
/**
|
|
111
|
-
* Read a JSONL file into an array of parsed objects. Missing file →
|
|
112
|
-
*
|
|
130
|
+
* Read a JSONL file into an array of parsed objects. Missing file (ENOENT) →
|
|
131
|
+
* [] silently; an unreadable one (EACCES/EISDIR/…) → [] with a stderr WARN
|
|
132
|
+
* (#1210 — ENOENT and other read failures are different facts, same split as
|
|
133
|
+
* `sessions-canonical.mjs` `readCanonicalSessions`). Each malformed line is
|
|
134
|
+
* skipped rather than aborting the whole read. Never throws.
|
|
113
135
|
*/
|
|
114
136
|
function readJsonlSafe(readFileSync, filePath) {
|
|
115
137
|
let raw;
|
|
116
138
|
try {
|
|
117
139
|
raw = readFileSync(filePath, 'utf8');
|
|
118
|
-
} catch {
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (!err || err.code !== 'ENOENT') {
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`⚠ readJsonlSafe: cannot read ${filePath} ` +
|
|
144
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
|
|
145
|
+
'treating as EMPTY, counts below are floors\n',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
119
148
|
return [];
|
|
120
149
|
}
|
|
121
150
|
const out = [];
|
|
@@ -153,18 +182,33 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
|
|
|
153
182
|
|
|
154
183
|
let mode = null;
|
|
155
184
|
let semanticFromLock = null;
|
|
185
|
+
// #1167 — the SECOND semantic bridge. `orchestrator.session.ended` carries
|
|
186
|
+
// `semantic_session_id` alongside the raw UUID since #1068 AC1, but nothing
|
|
187
|
+
// read it: a session that LOST the lock-acquire race emits no lock.acquired,
|
|
188
|
+
// so the lock bridge above resolved null and the caller fell through to the
|
|
189
|
+
// synthetic-id mint — writing a SECOND `abandoned` stub for a session the
|
|
190
|
+
// SessionEnd hook had already recorded under its semantic id. Measured
|
|
191
|
+
// 2026-09-02 @ c3ab480: 8 such duplicate pairs in sessions.jsonl.
|
|
192
|
+
let semanticFromEvents = null;
|
|
156
193
|
|
|
157
|
-
// First pass —
|
|
194
|
+
// First pass — bridge the UUID set + carry mode + semantic id. lock.acquired
|
|
195
|
+
// is the original bridge; session.ended is the #1167 addition.
|
|
158
196
|
for (const ev of events) {
|
|
159
|
-
|
|
197
|
+
const isLock = ev.event === EVENT_LOCK_ACQUIRED;
|
|
198
|
+
const isEnded = ev.event === EVENT_ENDED && typeof ev.semantic_session_id === 'string';
|
|
199
|
+
if (!isLock && !isEnded) continue;
|
|
160
200
|
const matchesUuid = isUuid(sessionId) && ev.session_id === sessionId;
|
|
161
201
|
const matchesSemantic =
|
|
162
202
|
(semanticSessionId && ev.semantic_session_id === semanticSessionId) ||
|
|
163
203
|
(!isUuid(sessionId) && sessionId && ev.semantic_session_id === sessionId);
|
|
164
204
|
if (!matchesUuid && !matchesSemantic) continue;
|
|
165
205
|
if (typeof ev.session_id === 'string') uuids.add(ev.session_id);
|
|
166
|
-
if (
|
|
167
|
-
|
|
206
|
+
if (isLock) {
|
|
207
|
+
if (typeof ev.mode === 'string') mode = ev.mode;
|
|
208
|
+
if (typeof ev.semantic_session_id === 'string') semanticFromLock = ev.semantic_session_id;
|
|
209
|
+
} else {
|
|
210
|
+
semanticFromEvents = ev.semantic_session_id;
|
|
211
|
+
}
|
|
168
212
|
}
|
|
169
213
|
|
|
170
214
|
// Second pass — started + terminal timestamps from every matched UUID.
|
|
@@ -192,7 +236,7 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
|
|
|
192
236
|
if (typeof ev.timestamp === 'string') startedAt = ev.timestamp;
|
|
193
237
|
if (typeof ev.branch === 'string' && ev.branch.length > 0) branch = ev.branch;
|
|
194
238
|
if (typeof ev.project === 'string') project = ev.project;
|
|
195
|
-
} else if (ev.event === EVENT_STOPPED || ev.event === EVENT_ENDED) {
|
|
239
|
+
} else if (ev.event === EVENT_STOPPED || ev.event === EVENT_TURN_STOPPED || ev.event === EVENT_ENDED) {
|
|
196
240
|
if (!Number.isNaN(ts)) {
|
|
197
241
|
lastTerminalMs = lastTerminalMs === null ? ts : Math.max(lastTerminalMs, ts);
|
|
198
242
|
}
|
|
@@ -200,7 +244,18 @@ function collectSessionEvents(events, { sessionId, semanticSessionId }) {
|
|
|
200
244
|
}
|
|
201
245
|
}
|
|
202
246
|
|
|
203
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
uuids,
|
|
249
|
+
mode,
|
|
250
|
+
semanticFromLock,
|
|
251
|
+
semanticFromEvents,
|
|
252
|
+
startedAt,
|
|
253
|
+
branch,
|
|
254
|
+
project,
|
|
255
|
+
lastTerminalMs,
|
|
256
|
+
earliestMs,
|
|
257
|
+
lastEventMs,
|
|
258
|
+
};
|
|
204
259
|
}
|
|
205
260
|
|
|
206
261
|
/**
|
|
@@ -256,7 +311,7 @@ function isCandidateDeadByAge({ relaxDeadByAge, assumeDeadBeforeMs, lastEventMs,
|
|
|
256
311
|
* this record replaces. It is emitted only when non-null, so every existing
|
|
257
312
|
* record shape is byte-identical to before.
|
|
258
313
|
*/
|
|
259
|
-
function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'abandoned', backfillSource = 'events-jsonl', supersedes = null }) {
|
|
314
|
+
function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'abandoned', backfillSource = 'events-jsonl', supersedes = null, rawSessionId = null }) {
|
|
260
315
|
const startedIso = canonicalIso(gathered.startedAt, gathered.earliestMs ?? nowMs);
|
|
261
316
|
const startedMs = Date.parse(startedIso);
|
|
262
317
|
// completed_at is events-attested, never the backfill-run wall-clock (#914 R1).
|
|
@@ -281,9 +336,9 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
|
|
|
281
336
|
// Guard the same monotonic invariant as before: never earlier than started_at.
|
|
282
337
|
const completedIso = new Date(Math.max(startedMs, completedMs)).toISOString();
|
|
283
338
|
|
|
284
|
-
let sessionType =
|
|
339
|
+
let sessionType = UNMEASURED_SESSION_TYPE;
|
|
285
340
|
let inferred = true;
|
|
286
|
-
if (gathered.mode &&
|
|
341
|
+
if (gathered.mode && MEASURED_SESSION_MODES.has(gathered.mode)) {
|
|
287
342
|
sessionType = gathered.mode;
|
|
288
343
|
inferred = false;
|
|
289
344
|
}
|
|
@@ -320,7 +375,36 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
|
|
|
320
375
|
_backfill_incomplete_fields: incomplete,
|
|
321
376
|
};
|
|
322
377
|
if (branchFound) record.branch = gathered.branch;
|
|
323
|
-
if (inferred)
|
|
378
|
+
if (inferred) {
|
|
379
|
+
record._session_type_inferred = true;
|
|
380
|
+
// GitLab #1234 — BACKFILLER HONESTY, half landed 2026-09-06.
|
|
381
|
+
//
|
|
382
|
+
// `session_type` above is now `'unknown'`, not the old `'housekeeping'`
|
|
383
|
+
// DEFAULT that nothing in events.jsonl ever said. Measured 2026-09-06, all
|
|
384
|
+
// 1.656 `abandoned` records in the 90-day fleet window carry
|
|
385
|
+
// `_session_type_inferred: true` + `total_waves: 0`, and NO organically
|
|
386
|
+
// written `abandoned` record exists anywhere — that `housekeeping` guess is
|
|
387
|
+
// what produced the "27 % close rate" figure that turned out to be an
|
|
388
|
+
// artefact (the real rate is 21,3 %). VALID_SESSION_TYPES was widened with
|
|
389
|
+
// `unknown` in `scripts/lib/session-schema/constants.mjs` to make this
|
|
390
|
+
// sayable; historical records keep their `housekeeping` label verbatim
|
|
391
|
+
// (sessions.jsonl is append-only), so a reader wanting the honest
|
|
392
|
+
// population still filters on `_synthetic !== true` rather than on the type.
|
|
393
|
+
//
|
|
394
|
+
// The OTHER half is deliberately NOT landed: `status` stays `'abandoned'`
|
|
395
|
+
// even though `'unresolved'` is the honest word and the schema now accepts
|
|
396
|
+
// it. Six executable phantom-stub filters key on the literal `abandoned`
|
|
397
|
+
// (census + revisit trigger in `scripts/lib/session-schema/validator.mjs`
|
|
398
|
+
// § SESSION_STATUS) and none of them is in this change's file scope —
|
|
399
|
+
// flipping the emitter first would make every new stub invisible to all six
|
|
400
|
+
// and re-open the #834 phantom-in-signal class fleet-wide. Repoint those
|
|
401
|
+
// filters onto one both-accepting predicate, then flip this one line.
|
|
402
|
+
//
|
|
403
|
+
// `_synthetic: true` still carries the claim no enum can: this record was
|
|
404
|
+
// COMPOSED. A consumer that filters on `_synthetic !== true` gets only
|
|
405
|
+
// measured records without needing to know either enum.
|
|
406
|
+
record._synthetic = true;
|
|
407
|
+
}
|
|
324
408
|
if (synthetic) record._synthetic_session_id = true;
|
|
325
409
|
if (completedEstimated) record._completed_at_estimated = true;
|
|
326
410
|
// #1068 AC3/AC4 — forensic supersede marker. sessions.jsonl is append-only,
|
|
@@ -329,6 +413,13 @@ function synthesizeRecord({ recordId, synthetic, gathered, nowMs, status = 'aban
|
|
|
329
413
|
// "historische Stub-Provenance bleibt erhalten"). Readers resolve one
|
|
330
414
|
// canonical state per id by taking the NEWEST record for that id.
|
|
331
415
|
if (typeof supersedes === 'string' && supersedes.length > 0) record.supersedes = supersedes;
|
|
416
|
+
// #1167 — the harness UUID this record was reconstructed from, when known.
|
|
417
|
+
// Additive and optional (the schema validates unknown keys pass-through, see
|
|
418
|
+
// session-schema/validator.mjs `_validateOptionalFields`): it is the ONLY key
|
|
419
|
+
// that lets a reader join a semantic record back to its raw uuid. Measured
|
|
420
|
+
// 2026-09-02 @ c3ab480: 0 of 286 existing records carry it, which is exactly
|
|
421
|
+
// why the two backfill writers could not see each other's work.
|
|
422
|
+
if (typeof rawSessionId === 'string' && rawSessionId.length > 0) record.raw_session_id = rawSessionId;
|
|
332
423
|
return record;
|
|
333
424
|
}
|
|
334
425
|
|
|
@@ -535,8 +626,11 @@ export async function backfillAbandonedSession({
|
|
|
535
626
|
|
|
536
627
|
// -- Resolve a deferred id from the lock bridge or a synthetic mint ------
|
|
537
628
|
if (recordId === null) {
|
|
538
|
-
|
|
539
|
-
|
|
629
|
+
// Prefer the lock bridge (it also carries `mode`), then the #1167
|
|
630
|
+
// session.ended bridge. Only when NEITHER attests a semantic id do we
|
|
631
|
+
// mint a synthetic one — that fallback was the duplicate-stub source.
|
|
632
|
+
if (gathered.semanticFromLock || gathered.semanticFromEvents) {
|
|
633
|
+
recordId = gathered.semanticFromLock || gathered.semanticFromEvents;
|
|
540
634
|
} else {
|
|
541
635
|
// No semantic bridge — mint a synthetic id. Both components are STABLE
|
|
542
636
|
// across re-runs so dedupe/marker suppress a double write (idempotency
|
|
@@ -619,7 +713,13 @@ export async function backfillAbandonedSession({
|
|
|
619
713
|
}
|
|
620
714
|
|
|
621
715
|
// -- Synthesize + validate (round-trip gate) BEFORE any disk mutation ---
|
|
622
|
-
const record = synthesizeRecord({
|
|
716
|
+
const record = synthesizeRecord({
|
|
717
|
+
recordId,
|
|
718
|
+
synthetic,
|
|
719
|
+
gathered,
|
|
720
|
+
nowMs,
|
|
721
|
+
rawSessionId: isUuid(sessionId) ? sessionId : null,
|
|
722
|
+
});
|
|
623
723
|
let validated;
|
|
624
724
|
try {
|
|
625
725
|
validated = validateSession(record);
|
|
@@ -845,6 +945,13 @@ export async function backfillCompletedFromStateMd({
|
|
|
845
945
|
status: 'completed',
|
|
846
946
|
backfillSource: 'state-md-completed',
|
|
847
947
|
supersedes,
|
|
948
|
+
// #1167 — the abandoned path stamps this; so must the authoritative
|
|
949
|
+
// one, or the join key exists on exactly the weaker half of the pair.
|
|
950
|
+
// `gathered.uuids` is bridged from lock.acquired / session.ended, so it
|
|
951
|
+
// normally holds EXACTLY the one uuid this semantic id ran under. Two
|
|
952
|
+
// (or zero) means the bridge is ambiguous — omit rather than guess, the
|
|
953
|
+
// same fail-quiet posture as `isUuid(sessionId) ? sessionId : null`.
|
|
954
|
+
rawSessionId: gathered.uuids?.size === 1 ? [...gathered.uuids][0] : null,
|
|
848
955
|
});
|
|
849
956
|
let validated;
|
|
850
957
|
try {
|
|
@@ -32,6 +32,28 @@
|
|
|
32
32
|
* - Lock takes precedence over registry (more detail per session); registry
|
|
33
33
|
* supplements missing per-worktree entries (e.g., when the hook ran but
|
|
34
34
|
* the prose Phase 1.2 acquire was skipped).
|
|
35
|
+
* - A live local session.lock does NOT supersede same-repo registry entries
|
|
36
|
+
* with a different raw session_id (GH#67 candidate fix, measured 2026-09-02
|
|
37
|
+
* and NOT adopted): the session lock is advisory, so a second session can
|
|
38
|
+
* and demonstrably does run in the same working copy without holding it.
|
|
39
|
+
* Filtering those entries out hides genuine same-working-copy peers — the
|
|
40
|
+
* #1085 semantic-alias boundary (tests/integration/session-identity-
|
|
41
|
+
* boundaries.test.mjs) and 7 peer-discovery cases pin exactly that
|
|
42
|
+
* visibility. Any GH#67 fix must discriminate finished-but-fresh entries
|
|
43
|
+
* some other way; do not re-attempt lock-ownership supersession here.
|
|
44
|
+
* See ADR-0014 (docs/adr/0014-peer-visibility-under-advisory-lock.md) for
|
|
45
|
+
* the annotate-never-filter decision and the 9-test refutation of the
|
|
46
|
+
* filtering alternative.
|
|
47
|
+
* - The adopted GH#67 shape is therefore ADDITIVE ANNOTATION, never a filter.
|
|
48
|
+
* Registry-sourced sessions carry `registryOnly: true` plus
|
|
49
|
+
* `lockSuperseded` / `lockOwnerId`. `lockSuperseded: true` means: a LIVE
|
|
50
|
+
* lock at this repoRoot is owned by a different raw session_id than this
|
|
51
|
+
* registry entry. It is a HINT, not a verdict — the lock is advisory, so
|
|
52
|
+
* the entry may still be a live session that lost the acquire race (#1085
|
|
53
|
+
* contract). Consumers deciding a worktree-PROMOTION_OFFER downgrade such
|
|
54
|
+
* a peer to an advisory line (GH#67); consumers counting or displaying
|
|
55
|
+
* peers keep it. Lock-sourced sessions carry none of the three fields, so
|
|
56
|
+
* their objects stay byte-identical to the pre-GH#67 shape.
|
|
35
57
|
*
|
|
36
58
|
* Timeout + A1 fallback:
|
|
37
59
|
* - listWorktrees() is raced against DEFAULT_DISCOVERY_TIMEOUT_MS (2 s).
|
|
@@ -93,10 +115,24 @@ function sessionFromLock(lock, worktreePath, branch = '') {
|
|
|
93
115
|
* does not record per-worktree paths — only repo-path-hashes. Branch is
|
|
94
116
|
* passed through from the registry entry when available.
|
|
95
117
|
*
|
|
118
|
+
* GH#67 annotation (additive, never a filter — see the module header): every
|
|
119
|
+
* registry-sourced session carries `registryOnly: true` and the pair
|
|
120
|
+
* `lockSuperseded` / `lockOwnerId`. `lockSuperseded: true` says a LIVE lock at
|
|
121
|
+
* this repoRoot is owned by a DIFFERENT raw session_id than this entry — a
|
|
122
|
+
* HINT that the entry may be a finished-but-still-fresh task (the GH#67 case:
|
|
123
|
+
* Codex emits no SessionEnd, so the registry entry outlives the task by up to
|
|
124
|
+
* `freshnessMin`). It is not a verdict: the lock is advisory, so the entry may
|
|
125
|
+
* equally be a live session that never acquired it (#1085). Consumers deciding
|
|
126
|
+
* a worktree PROMOTION_OFFER downgrade such a peer to an advisory line;
|
|
127
|
+
* consumers counting or displaying peers keep it unchanged.
|
|
128
|
+
*
|
|
96
129
|
* @param {object} entry Registry entry.
|
|
97
130
|
* @param {string} repoRoot Discovery repoRoot (used as worktreePath fallback).
|
|
131
|
+
* @param {object} [ctx]
|
|
132
|
+
* @param {string|null} [ctx.lockOwnerId] Raw session_id owning a LIVE lock at
|
|
133
|
+
* repoRoot, or null when there is no live local lock.
|
|
98
134
|
*/
|
|
99
|
-
function sessionFromRegistryEntry(entry, repoRoot) {
|
|
135
|
+
function sessionFromRegistryEntry(entry, repoRoot, { lockOwnerId = null } = {}) {
|
|
100
136
|
return {
|
|
101
137
|
worktreePath: repoRoot,
|
|
102
138
|
sessionId: entry.session_id,
|
|
@@ -111,6 +147,11 @@ function sessionFromRegistryEntry(entry, repoRoot) {
|
|
|
111
147
|
// single machine, so consumers comparing hosts need the normalised form.
|
|
112
148
|
host_id: stableHostname(),
|
|
113
149
|
branch: typeof entry.branch === 'string' ? entry.branch : '',
|
|
150
|
+
// GH#67 additive annotation (see the JSDoc above). Lock-sourced sessions
|
|
151
|
+
// deliberately carry none of these three fields.
|
|
152
|
+
registryOnly: true,
|
|
153
|
+
lockSuperseded: lockOwnerId !== null && entry.session_id !== lockOwnerId,
|
|
154
|
+
lockOwnerId,
|
|
114
155
|
};
|
|
115
156
|
}
|
|
116
157
|
|
|
@@ -177,7 +218,10 @@ function dedupeBySessionId(sessions) {
|
|
|
177
218
|
* @param {Function} [opts.registryReader] DI hook replacing readRegistry() for tests.
|
|
178
219
|
* @param {number} [opts.freshnessMin=15] Registry-entry freshness threshold in minutes.
|
|
179
220
|
* @param {number} [opts.now] ms-since-epoch (test seam for heartbeat freshness).
|
|
180
|
-
* @returns {Promise<Array<{worktreePath:string,sessionId:string,mode:string,startedAt:string,pid:number,host:string,host_id:string,branch:string}>>}
|
|
221
|
+
* @returns {Promise<Array<{worktreePath:string,sessionId:string,mode:string,startedAt:string,pid:number,host:string,host_id:string,branch:string,registryOnly?:boolean,lockSuperseded?:boolean,lockOwnerId?:string|null}>>}
|
|
222
|
+
* The last three fields appear ONLY on registry-sourced sessions (GH#67
|
|
223
|
+
* annotation — see `sessionFromRegistryEntry`); lock-sourced sessions omit
|
|
224
|
+
* them entirely.
|
|
181
225
|
*/
|
|
182
226
|
export async function discoverActiveSessions(repoRoot, opts = {}) {
|
|
183
227
|
const listWorktreesFn = opts.listWorktreesImpl ?? listWorktrees;
|
|
@@ -232,10 +276,20 @@ export async function discoverActiveSessions(repoRoot, opts = {}) {
|
|
|
232
276
|
try {
|
|
233
277
|
const allEntries = await registryReaderFn();
|
|
234
278
|
const myRepoHash = repoPathHash(repoRoot);
|
|
279
|
+
// GH#67: who (if anyone) holds a LIVE lock at this repoRoot right now.
|
|
280
|
+
// Read once, outside the map — it is a property of the repo, not of an
|
|
281
|
+
// entry. A stale lock proves nothing, so it yields null.
|
|
282
|
+
const localLock = readLock({ repoRoot });
|
|
283
|
+
const lockOwnerId = (
|
|
284
|
+
localLock
|
|
285
|
+
&& isLockLive(localLock, nowMs)
|
|
286
|
+
&& typeof localLock.session_id === 'string'
|
|
287
|
+
&& localLock.session_id
|
|
288
|
+
) ? localLock.session_id : null;
|
|
235
289
|
registrySessions = allEntries
|
|
236
290
|
.filter((e) => e && e.repo_path_hash === myRepoHash)
|
|
237
291
|
.filter((e) => isRegistryEntryFresh(e, { freshnessMin, now: nowMs }))
|
|
238
|
-
.map((e) => sessionFromRegistryEntry(e, repoRoot));
|
|
292
|
+
.map((e) => sessionFromRegistryEntry(e, repoRoot, { lockOwnerId }));
|
|
239
293
|
} catch {
|
|
240
294
|
// Registry read failure → keep going with lock-only results.
|
|
241
295
|
registrySessions = [];
|
|
@@ -55,7 +55,7 @@ import { sweepExpiredLearnings } from '../learnings/expiry-sweep.mjs';
|
|
|
55
55
|
import { shouldDispatchAutoDream } from '../auto-dream.mjs';
|
|
56
56
|
import { shouldDispatchAutoDialectic } from '../auto-dialectic.mjs';
|
|
57
57
|
import { readSkillInvocations } from '../skill-invocations-schema.mjs';
|
|
58
|
-
import {
|
|
58
|
+
import { runReconcileFromPhaseSkip, resolveEffectiveTargets } from '../reconcile/engine.mjs';
|
|
59
59
|
import { resolveMemoryDir } from '../memory-paths.mjs';
|
|
60
60
|
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
@@ -285,7 +285,7 @@ async function decideReconcile({ repoRoot, cfg }) {
|
|
|
285
285
|
if (!existsSync(learningsPath)) {
|
|
286
286
|
return mkSkip(phase, 'learnings.jsonl absent', 'learnings.jsonl');
|
|
287
287
|
}
|
|
288
|
-
const { proposals, summary, error } = await
|
|
288
|
+
const { proposals, summary, error } = await runReconcileFromPhaseSkip({
|
|
289
289
|
repoRoot,
|
|
290
290
|
ruleExpiryDays: cfg?.reconcile?.['rule-expiry-days'] ?? undefined,
|
|
291
291
|
minRuleDays: cfg?.reconcile?.['min-rule-days'] ?? undefined,
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { open, readFile } from 'node:fs/promises';
|
|
43
43
|
import path from 'node:path';
|
|
44
44
|
|
|
45
|
+
import { readCanonicalSessions } from './sessions-canonical.mjs';
|
|
45
46
|
import { withStateMdLock } from './session-lock.mjs';
|
|
46
47
|
import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
47
48
|
import { parseStateMd } from './state-md/yaml-parser.mjs';
|
|
@@ -127,34 +128,22 @@ function isValidMode(mode) {
|
|
|
127
128
|
* - Malformed JSONL line → silently skipped (per-line try/catch).
|
|
128
129
|
* - Lines without a string `session_id` field → filtered out.
|
|
129
130
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
131
|
+
* #1209: migrated to `readCanonicalSessions` (dedup per `session_id`, drop
|
|
132
|
+
* id-less lines — same three rules `sessions-canonical.mjs` documents).
|
|
133
|
+
* Number-neutral for THIS reader: the only consumer of its output
|
|
134
|
+
* (`resolveSemanticSessionId`) folds `candidateIds` through `Math.max` over
|
|
135
|
+
* the parsed `n` — "Duplicates are fine — Math.max handles them" (see that
|
|
136
|
+
* function's body) — so collapsing a duplicate `session_id` line to one
|
|
137
|
+
* record changes nothing observable here. Migrated anyway for one collapse
|
|
138
|
+
* implementation across all `sessions.jsonl` raw-line-scan readers (#1209).
|
|
134
139
|
*
|
|
135
140
|
* @param {string} repoRoot
|
|
136
|
-
* @returns {Promise<string[]>} Array of session_id strings (may include duplicates
|
|
141
|
+
* @returns {Promise<string[]>} Array of session_id strings (may include duplicates
|
|
142
|
+
* only across DIFFERENT ids — never the same id twice, see above).
|
|
137
143
|
*/
|
|
138
144
|
async function readSessionIdsFromHistory(repoRoot) {
|
|
139
145
|
const filePath = path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl');
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
raw = await readFile(filePath, 'utf8');
|
|
143
|
-
} catch {
|
|
144
|
-
return [];
|
|
145
|
-
}
|
|
146
|
-
const ids = [];
|
|
147
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
148
|
-
const trimmed = line.trim();
|
|
149
|
-
if (trimmed === '') continue;
|
|
150
|
-
try {
|
|
151
|
-
const parsed = JSON.parse(trimmed);
|
|
152
|
-
if (typeof parsed?.session_id === 'string') ids.push(parsed.session_id);
|
|
153
|
-
} catch {
|
|
154
|
-
// Malformed line — skip silently (audit §3.1 robustness contract).
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return ids;
|
|
146
|
+
return readCanonicalSessions({ filePath }).map((rec) => rec.session_id);
|
|
158
147
|
}
|
|
159
148
|
|
|
160
149
|
/**
|