session-orchestrator 3.22.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/hooks/subagent-telemetry.mjs +11 -26
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +517 -18
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +220 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +20 -9
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +54 -17
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
* 2. Enumerate tracked .md files under --prd-dir (git ls-files), excluding
|
|
17
17
|
* *.original-uncommitted.md. A missing/empty --prd-dir yields [] → clean
|
|
18
18
|
* report, exit 0 (no crash) — see listTrackedPrds.
|
|
19
|
-
* 3. For each doc: parse
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* 3. For each doc: parse an Epic/Issue DECLARATION out of the header region
|
|
20
|
+
* (first ~20 lines, up to the first `## ` section) — frontmatter key, label
|
|
21
|
+
* line, or H1 title suffix. A bare `#NNN` in prose is a CITATION and never
|
|
22
|
+
* counts (`citation-only`); no `#NNN` at all → `no-epic-ref`. Both skip.
|
|
23
|
+
* 4. Session ownership: a doc with uncommitted changes (`uncommitted`) or one
|
|
24
|
+
* committed at/after this session's `started_at` (`foreign-session`) is
|
|
25
|
+
* skipped — it belongs to a parallel session, not to us (#1123).
|
|
26
|
+
* 5. `glab issue view <iid> --output json` → state. Only `closed` refs archive;
|
|
22
27
|
* `opened` and unknown/error states skip (never guess).
|
|
23
|
-
*
|
|
28
|
+
* 6. Closed → archiveFileToVault(...) into <vault>/<--vault-subdir>/. Under
|
|
24
29
|
* --apply the source doc is removed with `git rm`; --dry-run (default) writes
|
|
25
30
|
* NOTHING (not even to the vault).
|
|
26
31
|
*
|
|
@@ -29,8 +34,9 @@
|
|
|
29
34
|
*
|
|
30
35
|
* Exit codes: 0 success · 1 input/config error · 2 system error.
|
|
31
36
|
*
|
|
32
|
-
* Exports (for tests): main, parseEpicRef, readHeaderRegion,
|
|
33
|
-
* epicState,
|
|
37
|
+
* Exports (for tests): main, parseEpicDeclaration, parseEpicRef, readHeaderRegion,
|
|
38
|
+
* listTrackedPrds, epicState, classifyOwnership, readSessionStartedAt,
|
|
39
|
+
* defaultGlabRepo.
|
|
34
40
|
*/
|
|
35
41
|
|
|
36
42
|
import { readFileSync } from 'node:fs';
|
|
@@ -43,6 +49,9 @@ import { parseSessionConfig } from './lib/config.mjs';
|
|
|
43
49
|
import { glabRun as defaultGlabRun } from './lib/vault-backfill/glab.mjs';
|
|
44
50
|
import { archiveFileToVault, titleFromMarkdown } from './lib/vault-archive.mjs';
|
|
45
51
|
import { defaultGlabRepo } from './lib/vcs-repo-spec.mjs';
|
|
52
|
+
import { readLock } from './lib/session-lock.mjs';
|
|
53
|
+
import { resolveStateMdPath } from './lib/state-md/frontmatter-mutators.mjs';
|
|
54
|
+
import { parseStateMd } from './lib/state-md/yaml-parser.mjs';
|
|
46
55
|
|
|
47
56
|
const DEFAULT_PRD_DIR = 'docs/prd';
|
|
48
57
|
const DEFAULT_VAULT_SUBDIR = '01-projects/session-orchestrator/prd';
|
|
@@ -74,14 +83,184 @@ export { defaultGlabRepo };
|
|
|
74
83
|
// Pure helpers
|
|
75
84
|
// ---------------------------------------------------------------------------
|
|
76
85
|
|
|
86
|
+
// A DECLARATION line names the doc's own tracking Epic/Issue. Three accepted
|
|
87
|
+
// spellings, all anchored at line start so prose can never reach them:
|
|
88
|
+
// (a) `**Epic:** #1113` — colon INSIDE the bold (the shape 2 of 4 live PRDs use)
|
|
89
|
+
// (b) `**Epic**: #1113` — colon outside the bold
|
|
90
|
+
// (c) `Source: docs/prd/x.md (#786)` — unbolded (the plan-header shape,
|
|
91
|
+
// skills/write-executable-plan/SKILL.md:193)
|
|
92
|
+
// The optional leading `-`/`*` covers list items (`- **Issue:** #366`).
|
|
93
|
+
const LABEL_WORD = String.raw`(?:parent[ \t]+)?(?:epic|issues?|source)`;
|
|
94
|
+
const LABEL_LINE_RE = new RegExp(
|
|
95
|
+
String.raw`^[-*]?[ \t]*(?:` +
|
|
96
|
+
String.raw`\*\*[ \t]*${LABEL_WORD}[ \t]*:[ \t]*\*\*` +
|
|
97
|
+
String.raw`|\*\*[ \t]*${LABEL_WORD}[ \t]*\*\*[ \t]*:` +
|
|
98
|
+
String.raw`|${LABEL_WORD}[ \t]*:` +
|
|
99
|
+
String.raw`)[ \t]*(\S.*)$`,
|
|
100
|
+
'i',
|
|
101
|
+
);
|
|
102
|
+
const FRONTMATTER_LINE_RE = /^(?:epic|issues?)[ \t]*:[ \t]*\[?[ \t]*['"]?#?(\d+)/i;
|
|
103
|
+
const TITLE_SUFFIX_RE = /^#[ \t]+.*\(#(\d+)\)[ \t]*$/;
|
|
104
|
+
const ANY_IID_RE = /#(\d+)/;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse the doc's DECLARED Epic/Issue out of its header region.
|
|
108
|
+
*
|
|
109
|
+
* Precedence (first hit wins): frontmatter key → label line → H1 title suffix.
|
|
110
|
+
* A bare `#NNN` in prose is a CITATION and NEVER counts — that conflation is
|
|
111
|
+
* what deleted a live PRD on 2026-08-22 (#1112): a quoted, long-closed `#214`
|
|
112
|
+
* inside a blockquote read as the doc's own Epic, so the doc looked finished.
|
|
113
|
+
* The rule therefore prefers, by construction, documents that carry LESS
|
|
114
|
+
* context — hence the anchored line shapes below.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} headerText — output of readHeaderRegion().
|
|
117
|
+
* @returns {{ iid: string, via: 'frontmatter'|'label'|'title' }|null}
|
|
118
|
+
*/
|
|
119
|
+
export function parseEpicDeclaration(headerText) {
|
|
120
|
+
const lines = String(headerText).split(/\r?\n/);
|
|
121
|
+
|
|
122
|
+
// (1) Frontmatter — only inside the LEADING `---` block, never a stray
|
|
123
|
+
// `epic:` line further down the header.
|
|
124
|
+
//
|
|
125
|
+
// A leading `---` alone does NOT make a frontmatter block: in Markdown it is
|
|
126
|
+
// also a thematic break, and a doc that opens with one would otherwise have
|
|
127
|
+
// its BODY read as frontmatter (a body line `epic: 1113` — or any `issue:`
|
|
128
|
+
// note — would then win over the real declaration below). Require a CLOSING
|
|
129
|
+
// `---` inside the header region; without it, fall through to label/title.
|
|
130
|
+
if (lines[0]?.trim() === '---') {
|
|
131
|
+
const close = lines.findIndex((line, i) => i > 0 && line.trim() === '---');
|
|
132
|
+
if (close !== -1) {
|
|
133
|
+
for (let i = 1; i < close; i++) {
|
|
134
|
+
const m = FRONTMATTER_LINE_RE.exec(lines[i]);
|
|
135
|
+
if (m) return { iid: m[1], via: 'frontmatter' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// (2) Label line — the first `#NNN` on a recognised label line. Taking the
|
|
141
|
+
// first is correct for `**Epic:** #1048 · **Sub-Issues:** #1049 …`: the
|
|
142
|
+
// primary declaration leads, the sub-issues trail.
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const label = LABEL_LINE_RE.exec(line);
|
|
145
|
+
if (!label) continue;
|
|
146
|
+
const iid = ANY_IID_RE.exec(label[1]);
|
|
147
|
+
if (iid) return { iid: iid[1], via: 'label' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// (3) H1 title suffix — `# Feature: Foo (#1113)`.
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const m = TITLE_SUFFIX_RE.exec(line);
|
|
153
|
+
if (m) return { iid: m[1], via: 'title' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
/**
|
|
78
|
-
*
|
|
160
|
+
* @deprecated Use {@link parseEpicDeclaration} — it also reports HOW the iid was
|
|
161
|
+
* declared, which the caller needs to tell `citation-only` from `no-epic-ref`.
|
|
162
|
+
* Retained as a thin shim for existing importers.
|
|
79
163
|
* @param {string} headerText
|
|
80
|
-
* @returns {string|null} the
|
|
164
|
+
* @returns {string|null} the declared iid as a string, or null when absent.
|
|
81
165
|
*/
|
|
82
166
|
export function parseEpicRef(headerText) {
|
|
83
|
-
|
|
84
|
-
|
|
167
|
+
return parseEpicDeclaration(headerText)?.iid ?? null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Decide whether a doc belongs to THIS session or to a parallel one (#1123).
|
|
172
|
+
*
|
|
173
|
+
* Author IDENTITY carries no signal here — every session commits as the same
|
|
174
|
+
* human — so the discriminator is commit TIME against this session's
|
|
175
|
+
* `started_at`, taken as MAX(author date, committer date) so a rebased or
|
|
176
|
+
* cherry-picked peer commit cannot read as old.
|
|
177
|
+
* Fail-closed in every ambiguous direction: a failed git probe,
|
|
178
|
+
* an unparseable commit timestamp, or a missing `sessionStartedAt` all yield
|
|
179
|
+
* `'foreign'`, because 'foreign' is the verdict that does NOT delete.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} args
|
|
182
|
+
* @param {string} args.repoRoot
|
|
183
|
+
* @param {string} args.rel — repo-relative doc path.
|
|
184
|
+
* @param {string|null|undefined} args.sessionStartedAt — ISO-8601.
|
|
185
|
+
* @param {(args: string[]) => { ok: boolean, stdout: string, stderr: string }} args.gitRunFn
|
|
186
|
+
* @returns {'mine'|'foreign'|'uncommitted'} never throws.
|
|
187
|
+
*/
|
|
188
|
+
export function classifyOwnership({ repoRoot, rel, sessionStartedAt, gitRunFn }) {
|
|
189
|
+
let status;
|
|
190
|
+
try {
|
|
191
|
+
status = gitRunFn(['-C', repoRoot, 'status', '--porcelain', '--', rel]);
|
|
192
|
+
} catch {
|
|
193
|
+
return 'foreign';
|
|
194
|
+
}
|
|
195
|
+
if (!status?.ok) return 'foreign';
|
|
196
|
+
// Non-empty porcelain output = staged, unstaged or untracked (`??`) — either
|
|
197
|
+
// way there is work in the tree that a `git rm` would destroy.
|
|
198
|
+
if (String(status.stdout).trim() !== '') return 'uncommitted';
|
|
199
|
+
|
|
200
|
+
const startedMs = Date.parse(String(sessionStartedAt ?? ''));
|
|
201
|
+
if (Number.isNaN(startedMs)) return 'foreign';
|
|
202
|
+
|
|
203
|
+
let log;
|
|
204
|
+
try {
|
|
205
|
+
// BOTH timestamps, newest wins. `%aI` alone is the AUTHOR date, which
|
|
206
|
+
// survives rebase, cherry-pick and `--amend` UNCHANGED — a peer session's
|
|
207
|
+
// doc rebased onto this branch keeps its old author date and would read as
|
|
208
|
+
// 'mine', i.e. as ours to `git rm`. `%cI` (committer date) moves with every
|
|
209
|
+
// rewrite, so MAX(author, committer) is the moment the object entered THIS
|
|
210
|
+
// history — the quantity the session-ownership guard actually needs.
|
|
211
|
+
log = gitRunFn(['-C', repoRoot, 'log', '-1', '--format=%aI%n%cI', '--', rel]);
|
|
212
|
+
} catch {
|
|
213
|
+
return 'foreign';
|
|
214
|
+
}
|
|
215
|
+
if (!log?.ok) return 'foreign';
|
|
216
|
+
const stamps = String(log.stdout)
|
|
217
|
+
.split(/\r?\n/)
|
|
218
|
+
.map((s) => Date.parse(s.trim()))
|
|
219
|
+
.filter((n) => !Number.isNaN(n));
|
|
220
|
+
if (stamps.length === 0) return 'foreign';
|
|
221
|
+
const committedMs = Math.max(...stamps);
|
|
222
|
+
|
|
223
|
+
return committedMs >= startedMs ? 'foreign' : 'mine';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve THIS session's `started_at` (ISO-8601): the session lock first, the
|
|
228
|
+
* STATE.md frontmatter as fallback.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} repoRoot
|
|
231
|
+
* @returns {string|null} null when neither source yields a parseable timestamp.
|
|
232
|
+
*/
|
|
233
|
+
export function readSessionStartedAt(repoRoot) {
|
|
234
|
+
try {
|
|
235
|
+
const startedAt = readLock({ repoRoot })?.started_at;
|
|
236
|
+
if (typeof startedAt === 'string' && !Number.isNaN(Date.parse(startedAt))) return startedAt;
|
|
237
|
+
} catch {
|
|
238
|
+
// fall through to STATE.md
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const parsed = parseStateMd(readFileSync(resolveStateMdPath(repoRoot), 'utf8'));
|
|
242
|
+
const startedAt = parsed?.frontmatter?.started_at;
|
|
243
|
+
if (typeof startedAt === 'string' && !Number.isNaN(Date.parse(startedAt))) return startedAt;
|
|
244
|
+
} catch {
|
|
245
|
+
// no STATE.md, or unreadable → null (fail-closed at the call site)
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Read the issue iids this session declared in STATE.md frontmatter (`issues:`).
|
|
252
|
+
* @param {string} repoRoot
|
|
253
|
+
* @returns {Set<string>} empty when STATE.md is absent or carries no list.
|
|
254
|
+
*/
|
|
255
|
+
function readSessionIssues(repoRoot) {
|
|
256
|
+
try {
|
|
257
|
+
const parsed = parseStateMd(readFileSync(resolveStateMdPath(repoRoot), 'utf8'));
|
|
258
|
+
const issues = parsed?.frontmatter?.issues;
|
|
259
|
+
if (Array.isArray(issues)) return new Set(issues.map((n) => String(n)));
|
|
260
|
+
} catch {
|
|
261
|
+
// absent/unreadable → empty set
|
|
262
|
+
}
|
|
263
|
+
return new Set();
|
|
85
264
|
}
|
|
86
265
|
|
|
87
266
|
/**
|
|
@@ -167,7 +346,8 @@ function printHelp() {
|
|
|
167
346
|
|
|
168
347
|
USAGE
|
|
169
348
|
node scripts/archive-closed-prds.mjs [--dry-run|--apply] [--json]
|
|
170
|
-
[--prd-dir DIR] [--vault-subdir DIR] [--
|
|
349
|
+
[--prd-dir DIR] [--vault-subdir DIR] [--glab-repo SPEC]
|
|
350
|
+
[--ignore-session-guard] [--owned-issues-only] [--help]
|
|
171
351
|
|
|
172
352
|
FLAGS
|
|
173
353
|
--dry-run (default) Plan the archive; write NOTHING (not even the vault).
|
|
@@ -180,6 +360,16 @@ FLAGS
|
|
|
180
360
|
shell wrapper and uses the ambient GITLAB_HOST). When
|
|
181
361
|
omitted, the spec is auto-detected from the local git
|
|
182
362
|
remote (prefers 'gitlab', else 'origin').
|
|
363
|
+
--ignore-session-guard
|
|
364
|
+
Escape hatch for a CATCH-UP SWEEP: also consider docs last
|
|
365
|
+
committed at/after this session's start (normally skipped
|
|
366
|
+
as 'foreign-session', because they belong to a parallel
|
|
367
|
+
session — #1123). Docs with uncommitted changes are STILL
|
|
368
|
+
skipped; this flag never widens that guard.
|
|
369
|
+
--owned-issues-only
|
|
370
|
+
Opt-in: archive only docs whose declared iid appears in
|
|
371
|
+
this session's STATE.md 'issues:' list. Everything else
|
|
372
|
+
skips as 'epic-#NNN-not-owned'.
|
|
183
373
|
-h, --help Show this help and exit.
|
|
184
374
|
|
|
185
375
|
EXIT CODES
|
|
@@ -229,6 +419,8 @@ function printHuman(archived, skipped, isDryRun, vaultDir, vaultSubdir) {
|
|
|
229
419
|
* @param {{ env?: Record<string, string|undefined>, ownerConfig?: object }} [opts.hostPaths]
|
|
230
420
|
* — forwarded to parseSessionConfig. Tests pass `{ env: {}, ownerConfig: undefined }`
|
|
231
421
|
* for hermetic, owner.yaml-free vault-dir resolution (issue #653 bleed guard).
|
|
422
|
+
* @param {string|null} [opts.sessionStartedAt] — ISO-8601 override for the
|
|
423
|
+
* session-ownership guard (#1123). Omitted → readSessionStartedAt(root).
|
|
232
424
|
* @returns {{ code: 0|1|2, archived: object[], skipped: object[], vaultDir?: string, dryRun?: boolean }}
|
|
233
425
|
*/
|
|
234
426
|
export function main({
|
|
@@ -238,6 +430,7 @@ export function main({
|
|
|
238
430
|
gitRunFn = defaultGitRun,
|
|
239
431
|
now,
|
|
240
432
|
hostPaths,
|
|
433
|
+
sessionStartedAt,
|
|
241
434
|
} = {}) {
|
|
242
435
|
// ── Parse flags ──────────────────────────────────────────────────────────
|
|
243
436
|
let apply = false;
|
|
@@ -247,6 +440,8 @@ export function main({
|
|
|
247
440
|
let prdDir = DEFAULT_PRD_DIR;
|
|
248
441
|
let vaultSubdir = DEFAULT_VAULT_SUBDIR;
|
|
249
442
|
let glabRepo;
|
|
443
|
+
let ignoreSessionGuard = false;
|
|
444
|
+
let ownedIssuesOnly = false;
|
|
250
445
|
|
|
251
446
|
for (let i = 0; i < argv.length; i++) {
|
|
252
447
|
const a = argv[i];
|
|
@@ -260,6 +455,8 @@ export function main({
|
|
|
260
455
|
else if (a.startsWith('--vault-subdir=')) vaultSubdir = a.slice('--vault-subdir='.length);
|
|
261
456
|
else if (a === '--glab-repo') glabRepo = argv[++i];
|
|
262
457
|
else if (a.startsWith('--glab-repo=')) glabRepo = a.slice('--glab-repo='.length);
|
|
458
|
+
else if (a === '--ignore-session-guard') ignoreSessionGuard = true;
|
|
459
|
+
else if (a === '--owned-issues-only') ownedIssuesOnly = true;
|
|
263
460
|
else {
|
|
264
461
|
process.stderr.write(`archive-closed-prds: unknown argument: ${a}\n`);
|
|
265
462
|
process.stderr.write('Run with --help for usage.\n');
|
|
@@ -314,6 +511,16 @@ export function main({
|
|
|
314
511
|
// the local git remote (host-agnostic; nothing host-specific committed).
|
|
315
512
|
const effectiveGlabRepo = glabRepo ?? defaultGlabRepo(root, gitRunFn);
|
|
316
513
|
|
|
514
|
+
// ── Session identity (ownership guard, #1123) ────────────────────────────
|
|
515
|
+
const startedAt = sessionStartedAt !== undefined ? sessionStartedAt : readSessionStartedAt(root);
|
|
516
|
+
if (!startedAt && !ignoreSessionGuard) {
|
|
517
|
+
warn(
|
|
518
|
+
'archive-closed-prds: no session started_at (session.lock/STATE.md) — every doc is treated ' +
|
|
519
|
+
'as foreign-session. Use --ignore-session-guard for a deliberate catch-up sweep.',
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const ownedIssues = ownedIssuesOnly ? readSessionIssues(root) : null;
|
|
523
|
+
|
|
317
524
|
// ── Enumerate + classify PRDs ────────────────────────────────────────────
|
|
318
525
|
const prds = listTrackedPrds(root, prdDir, gitRunFn);
|
|
319
526
|
const archived = [];
|
|
@@ -331,20 +538,51 @@ export function main({
|
|
|
331
538
|
continue;
|
|
332
539
|
}
|
|
333
540
|
|
|
334
|
-
const
|
|
335
|
-
if (!
|
|
336
|
-
|
|
337
|
-
|
|
541
|
+
const decl = parseEpicDeclaration(header);
|
|
542
|
+
if (!decl) {
|
|
543
|
+
// Distinguish "nothing to go on" from "only a CITATION to go on" — the
|
|
544
|
+
// latter is the #1112 shape and the more dangerous of the two, because a
|
|
545
|
+
// quoted closed issue reads exactly like a declared one.
|
|
546
|
+
const citationOnly = ANY_IID_RE.test(header);
|
|
547
|
+
skipped.push({ source: rel, reason: citationOnly ? 'citation-only' : 'no-epic-ref' });
|
|
548
|
+
warn(
|
|
549
|
+
citationOnly
|
|
550
|
+
? `archive-closed-prds: ${rel} header has #NNN only as a citation, not a declaration — skipped (#1112).`
|
|
551
|
+
: `archive-closed-prds: no Epic reference in header of ${rel} — skipped (never guess).`,
|
|
552
|
+
);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const { iid, via } = decl;
|
|
556
|
+
|
|
557
|
+
// Session ownership BEFORE any glab call: a doc a parallel session just
|
|
558
|
+
// committed is not ours to archive, whatever its Epic's state (#1123).
|
|
559
|
+
const ownership = classifyOwnership({ repoRoot: root, rel, sessionStartedAt: startedAt, gitRunFn });
|
|
560
|
+
if (ownership === 'uncommitted') {
|
|
561
|
+
skipped.push({ source: rel, reason: 'uncommitted', iid, via, ownership });
|
|
562
|
+
warn(`archive-closed-prds: ${rel} has uncommitted changes — skipped (never 'git rm' live work).`);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (ownership === 'foreign' && !ignoreSessionGuard) {
|
|
566
|
+
skipped.push({ source: rel, reason: 'foreign-session', iid, via, ownership });
|
|
567
|
+
warn(
|
|
568
|
+
`archive-closed-prds: ${rel} was last committed at/after this session's start — skipped ` +
|
|
569
|
+
'(belongs to a parallel session; --ignore-session-guard overrides).',
|
|
570
|
+
);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (ownedIssues && !ownedIssues.has(String(iid))) {
|
|
575
|
+
skipped.push({ source: rel, reason: `epic-#${iid}-not-owned`, iid, via, ownership });
|
|
338
576
|
continue;
|
|
339
577
|
}
|
|
340
578
|
|
|
341
579
|
const state = epicState(iid, glabRunFn, effectiveGlabRepo);
|
|
342
580
|
if (state === 'opened') {
|
|
343
|
-
skipped.push({ source: rel, reason: `epic-#${iid}-open`, iid });
|
|
581
|
+
skipped.push({ source: rel, reason: `epic-#${iid}-open`, iid, via, ownership });
|
|
344
582
|
continue;
|
|
345
583
|
}
|
|
346
584
|
if (state === 'unknown') {
|
|
347
|
-
skipped.push({ source: rel, reason: `epic-#${iid}-state-unknown`, iid });
|
|
585
|
+
skipped.push({ source: rel, reason: `epic-#${iid}-state-unknown`, iid, via, ownership });
|
|
348
586
|
warn(`archive-closed-prds: could not resolve state of Epic #${iid} for ${rel} — skipped.`);
|
|
349
587
|
continue;
|
|
350
588
|
}
|
|
@@ -364,11 +602,13 @@ export function main({
|
|
|
364
602
|
title: titleFromMarkdown(header),
|
|
365
603
|
});
|
|
366
604
|
} catch (err) {
|
|
367
|
-
skipped.push({ source: rel, reason: `archive-failed: ${err.message}`, iid });
|
|
605
|
+
skipped.push({ source: rel, reason: `archive-failed: ${err.message}`, iid, via, ownership });
|
|
368
606
|
warn(`archive-closed-prds: failed to archive ${rel}: ${err.message}`);
|
|
369
607
|
continue;
|
|
370
608
|
}
|
|
371
609
|
entry.iid = iid;
|
|
610
|
+
entry.via = via;
|
|
611
|
+
entry.ownership = ownership;
|
|
372
612
|
|
|
373
613
|
if (!isDryRun) {
|
|
374
614
|
const rm = gitRunFn(['-C', root, 'rm', '--', rel]);
|
package/scripts/autopilot.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import { selectMode } from './lib/mode-selector.mjs';
|
|
|
47
47
|
import { probe, evaluate, DEFAULT_RESOURCE_THRESHOLDS } from './lib/resource-probe.mjs';
|
|
48
48
|
import { detectPeers } from './lib/session-registry.mjs';
|
|
49
49
|
import { normalizeSession } from './lib/session-schema.mjs';
|
|
50
|
+
import { readCanonicalSessions } from './lib/sessions-canonical.mjs';
|
|
50
51
|
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
// CLI-level flag extraction
|
|
@@ -174,17 +175,29 @@ const branch = detectBranch();
|
|
|
174
175
|
const SESSIONS_JSONL_PATH = resolve('.orchestrator/metrics/sessions.jsonl');
|
|
175
176
|
|
|
176
177
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
178
|
+
* Set of the DISTINCT session identities currently recorded in sessions.jsonl.
|
|
179
|
+
* Empty set when the file is missing or unreadable.
|
|
180
|
+
*
|
|
181
|
+
* Identities, not lines (#1167): the ledger is append-only, so one physical
|
|
182
|
+
* session can occupy two lines (an abandoned stub plus the authoritative
|
|
183
|
+
* record that supersedes it, or the systemic double-stub pair). The
|
|
184
|
+
* pre/post comparison below asks "did the session I just spawned record
|
|
185
|
+
* itself?", and a raw line count answers that question wrong in BOTH
|
|
186
|
+
* directions — a duplicate pair looks like two sessions, while a supersede
|
|
187
|
+
* append (+1 line, −1 stub) leaves a canonical COUNT unchanged. Comparing the
|
|
188
|
+
* id SETS is immune to both.
|
|
189
|
+
*
|
|
190
|
+
* @returns {Set<string>}
|
|
179
191
|
*/
|
|
180
|
-
function
|
|
181
|
-
if (!existsSync(SESSIONS_JSONL_PATH)) return
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
function readSessionIds() {
|
|
193
|
+
if (!existsSync(SESSIONS_JSONL_PATH)) return new Set();
|
|
194
|
+
const ids = new Set();
|
|
195
|
+
for (const rec of readCanonicalSessions({ filePath: SESSIONS_JSONL_PATH })) {
|
|
196
|
+
if (typeof rec.session_id === 'string' && rec.session_id.length > 0) {
|
|
197
|
+
ids.add(rec.session_id);
|
|
198
|
+
}
|
|
187
199
|
}
|
|
200
|
+
return ids;
|
|
188
201
|
}
|
|
189
202
|
|
|
190
203
|
/**
|
|
@@ -230,7 +243,12 @@ async function modeSelector() {
|
|
|
230
243
|
// `backlogLimit` is intentionally NOT passed: the window default lives once,
|
|
231
244
|
// in backlog-scan.mjs (`DEFAULT_BACKLOG_LIMIT`). The hand-written 50 that
|
|
232
245
|
// stood here under-read this repo's own backlog and hid its critical issues.
|
|
246
|
+
// `repoRoot` is passed EXPLICITLY (#1071) rather than left to the helper's
|
|
247
|
+
// cwd default: this driver already reads learnings.jsonl and sessions.jsonl
|
|
248
|
+
// relative to the invocation directory, and an implicit default is the seam
|
|
249
|
+
// where those two roots silently drift apart.
|
|
233
250
|
const signals = await buildLiveSignals({
|
|
251
|
+
repoRoot: process.cwd(),
|
|
234
252
|
learnings: surfacedLearnings,
|
|
235
253
|
});
|
|
236
254
|
return selectMode(signals);
|
|
@@ -244,7 +262,7 @@ async function modeSelector() {
|
|
|
244
262
|
* @returns {Promise<{session_id: string, agent_summary?: object, effectiveness?: object}>}
|
|
245
263
|
*/
|
|
246
264
|
async function sessionRunner({ mode, autopilotRunId }) {
|
|
247
|
-
const
|
|
265
|
+
const preIds = readSessionIds();
|
|
248
266
|
|
|
249
267
|
await new Promise((res, rej) => {
|
|
250
268
|
const childStdio = hasVerbose
|
|
@@ -276,8 +294,9 @@ async function sessionRunner({ mode, autopilotRunId }) {
|
|
|
276
294
|
});
|
|
277
295
|
});
|
|
278
296
|
|
|
279
|
-
const
|
|
280
|
-
|
|
297
|
+
const postIds = readSessionIds();
|
|
298
|
+
const appeared = [...postIds].some((id) => !preIds.has(id));
|
|
299
|
+
if (!appeared) {
|
|
281
300
|
throw new Error('no session record appended');
|
|
282
301
|
}
|
|
283
302
|
|
|
@@ -42,10 +42,12 @@ import { parseArgs } from 'node:util';
|
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
43
|
|
|
44
44
|
import { backfillAbandonedSession, isUuid } from './lib/session-close-backfill.mjs';
|
|
45
|
+
import { emitEvent } from './lib/events.mjs';
|
|
45
46
|
import { SO_PROJECT_DIR } from './lib/platform.mjs';
|
|
46
47
|
|
|
47
48
|
const LOCK_ACQUIRED = 'orchestrator.session.lock.acquired';
|
|
48
49
|
const SESSION_STARTED = 'orchestrator.session.started';
|
|
50
|
+
const SESSION_ENDED = 'orchestrator.session.ended';
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
53
|
* Default cap on how many candidates may reach the (expensive) shared core in a
|
|
@@ -54,12 +56,31 @@ const SESSION_STARTED = 'orchestrator.session.started';
|
|
|
54
56
|
*/
|
|
55
57
|
export const SESSION_START_LIMIT = 25;
|
|
56
58
|
|
|
57
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Read a JSONL file into parsed objects; MISSING (ENOENT) → `[]` silently,
|
|
61
|
+
* UNREADABLE (EACCES/EISDIR/…) → `[]` with a stderr WARN (#1188); malformed
|
|
62
|
+
* lines skipped.
|
|
63
|
+
*/
|
|
58
64
|
function readJsonl(filePath) {
|
|
59
65
|
let raw;
|
|
60
66
|
try {
|
|
61
67
|
raw = fs.readFileSync(filePath, 'utf8');
|
|
62
|
-
} catch {
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// #1188 — ENOENT and EACCES/EISDIR are different facts: a missing ledger is
|
|
70
|
+
// the ordinary fresh-repo case; an UNREADABLE one previously read as "no
|
|
71
|
+
// records" and made every downstream count silently wrong. Same split as
|
|
72
|
+
// readLockDetailed (session-lock.mjs § absent vs unreadable).
|
|
73
|
+
if (!err || err.code !== 'ENOENT') {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`⚠ backfill-abandoned-sessions: cannot read ${filePath} ` +
|
|
76
|
+
`(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — ` +
|
|
77
|
+
'treating as EMPTY, counts below are floors\n',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
// CEILING (BV-004): still [] rather than throw — this runs on the
|
|
81
|
+
// SessionStart path (SESSION_START_LIMIT), where a permissions fault must
|
|
82
|
+
// not crash the session start. REVISIT if the warn rate in events.jsonl
|
|
83
|
+
// shows masked corruption.
|
|
63
84
|
return [];
|
|
64
85
|
}
|
|
65
86
|
const out = [];
|
|
@@ -86,16 +107,22 @@ function readJsonl(filePath) {
|
|
|
86
107
|
export function planSessions({ repoRoot }) {
|
|
87
108
|
const events = readJsonl(path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl'));
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
// Two independent UUID -> semantic bridges (#1167). `lock.acquired` is the
|
|
111
|
+
// original one, but a session that LOST the lock-acquire race never emits it;
|
|
112
|
+
// its `session.ended` event carries `semantic_session_id` all the same (since
|
|
113
|
+
// #1068 AC1) and was never read here. Without that second bridge the
|
|
114
|
+
// candidate resolved semantic=null and the shared core minted a SYNTHETIC id,
|
|
115
|
+
// writing a duplicate stub beside the record the SessionEnd hook had already
|
|
116
|
+
// written under the real semantic id (measured 2026-09-02 @ c3ab480: 8 pairs).
|
|
117
|
+
// lock.acquired keeps precedence — it is the older, mode-carrying attestation.
|
|
118
|
+
const semanticFromLock = new Map();
|
|
119
|
+
const semanticFromEnded = new Map();
|
|
90
120
|
for (const ev of events) {
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
typeof ev.semantic_session_id === 'string'
|
|
95
|
-
) {
|
|
96
|
-
semanticByUuid.set(ev.session_id, ev.semantic_session_id);
|
|
97
|
-
}
|
|
121
|
+
if (typeof ev.session_id !== 'string' || typeof ev.semantic_session_id !== 'string') continue;
|
|
122
|
+
if (ev.event === LOCK_ACQUIRED) semanticFromLock.set(ev.session_id, ev.semantic_session_id);
|
|
123
|
+
else if (ev.event === SESSION_ENDED) semanticFromEnded.set(ev.session_id, ev.semantic_session_id);
|
|
98
124
|
}
|
|
125
|
+
const semanticByUuid = new Map([...semanticFromEnded, ...semanticFromLock]);
|
|
99
126
|
|
|
100
127
|
const seen = new Set();
|
|
101
128
|
const plan = [];
|
|
@@ -133,7 +160,14 @@ function readRecordedIds(repoRoot) {
|
|
|
133
160
|
const records = readJsonl(path.join(repoRoot, '.orchestrator', 'metrics', 'sessions.jsonl'));
|
|
134
161
|
const ids = new Set();
|
|
135
162
|
for (const r of records) {
|
|
136
|
-
if (r
|
|
163
|
+
if (!r) continue;
|
|
164
|
+
if (typeof r.session_id === 'string') ids.add(r.session_id);
|
|
165
|
+
// #1167 — a backfilled record now stamps the harness UUID it was
|
|
166
|
+
// reconstructed from. Indexing it lets a bare-UUID candidate be recognised
|
|
167
|
+
// as already-recorded even though the record itself is keyed semantically —
|
|
168
|
+
// the join that was structurally impossible while the field was absent
|
|
169
|
+
// (0 of 286 records carried it, measured 2026-09-02 @ c3ab480).
|
|
170
|
+
if (typeof r.raw_session_id === 'string') ids.add(r.raw_session_id);
|
|
137
171
|
}
|
|
138
172
|
return ids;
|
|
139
173
|
}
|
|
@@ -238,6 +272,7 @@ export async function runMigration({
|
|
|
238
272
|
case 'backfilled':
|
|
239
273
|
summary.backfilled += 1;
|
|
240
274
|
if (res.deadByAge) summary.dead_by_age += 1;
|
|
275
|
+
await emitBackfillCompleted(repoRoot, item, res);
|
|
241
276
|
// Keep the pre-filter snapshot in step with what we just wrote, so a
|
|
242
277
|
// second candidate bridging to the SAME semantic id is skipped cheaply
|
|
243
278
|
// instead of re-entering the core (which would reach the same verdict).
|
|
@@ -263,6 +298,40 @@ export async function runMigration({
|
|
|
263
298
|
return summary;
|
|
264
299
|
}
|
|
265
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Emit `orchestrator.session.backfill_completed` for a record THIS path wrote
|
|
303
|
+
* (#1167). Mirrors the payload of `hooks/on-session-end.mjs::emitBackfillOutcome`
|
|
304
|
+
* so both writers are queryable with one filter.
|
|
305
|
+
*
|
|
306
|
+
* Until now the startup/CLI path wrote records SILENTLY: only the SessionEnd
|
|
307
|
+
* hook emitted the event, so nothing in the event stream distinguished "the
|
|
308
|
+
* backfill never ran" from "the backfill ran here". Best-effort by contract —
|
|
309
|
+
* observability must never fail a migration run.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} repoRoot
|
|
312
|
+
* @param {{sessionId: string, semanticSessionId: string|null}} item
|
|
313
|
+
* @param {{action: string, sessionId?: string, supersedes?: string, error?: string}} res
|
|
314
|
+
*/
|
|
315
|
+
async function emitBackfillCompleted(repoRoot, item, res) {
|
|
316
|
+
try {
|
|
317
|
+
await emitEvent(
|
|
318
|
+
'orchestrator.session.backfill_completed',
|
|
319
|
+
{
|
|
320
|
+
kind: 'abandoned',
|
|
321
|
+
action: typeof res?.action === 'string' ? res.action : 'unknown',
|
|
322
|
+
...(typeof item?.sessionId === 'string' ? { session_id: item.sessionId } : {}),
|
|
323
|
+
...(typeof item?.semanticSessionId === 'string'
|
|
324
|
+
? { semantic_session_id: item.semanticSessionId }
|
|
325
|
+
: {}),
|
|
326
|
+
...(typeof res?.sessionId === 'string' ? { record_id: res.sessionId } : {}),
|
|
327
|
+
...(typeof res?.supersedes === 'string' ? { supersedes: res.supersedes } : {}),
|
|
328
|
+
...(typeof res?.error === 'string' ? { reason: res.error } : {}),
|
|
329
|
+
},
|
|
330
|
+
{ repoRoot },
|
|
331
|
+
);
|
|
332
|
+
} catch { /* observability is best-effort */ }
|
|
333
|
+
}
|
|
334
|
+
|
|
266
335
|
/**
|
|
267
336
|
* SessionStart entry point (#926) — decouple the backfill from the /close path.
|
|
268
337
|
*
|