session-orchestrator 3.21.0 → 3.22.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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/040-discovery.mdc +6 -6
  6. package/.cursor/rules/050-plan.mdc +8 -8
  7. package/CHANGELOG.md +101 -0
  8. package/README.md +10 -10
  9. package/agents/memory-proposal-collector.md +6 -4
  10. package/commands/eli5.md +33 -0
  11. package/commands/release.md +5 -3
  12. package/commands/test.md +2 -2
  13. package/docs/components.md +6 -5
  14. package/docs/scope-collision-guard.md +3 -3
  15. package/docs/session-config-reference.md +31 -8
  16. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  17. package/hooks/hooks-codex.json +1 -1
  18. package/hooks/hooks.json +11 -1
  19. package/hooks/on-session-end.mjs +24 -92
  20. package/hooks/on-session-start.mjs +195 -104
  21. package/hooks/pre-auq-clarity.mjs +787 -0
  22. package/hooks/pre-bash-issue-budget.mjs +17 -18
  23. package/package.json +3 -1
  24. package/pi/prompts/eli5.md +12 -0
  25. package/scripts/auq-audit.mjs +825 -0
  26. package/scripts/autopilot.mjs +7 -8
  27. package/scripts/lib/auq/clarity.mjs +1314 -0
  28. package/scripts/lib/auq/parse.mjs +1006 -0
  29. package/scripts/lib/auq/schema.mjs +1457 -0
  30. package/scripts/lib/ci-status-banner.mjs +63 -57
  31. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  32. package/scripts/lib/config/vault-integration.mjs +12 -1
  33. package/scripts/lib/dispatcher/rank.mjs +4 -7
  34. package/scripts/lib/gates/gate-full.mjs +3 -3
  35. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  36. package/scripts/lib/io.mjs +239 -0
  37. package/scripts/lib/issue-budget.mjs +63 -9
  38. package/scripts/lib/owner-interview.mjs +78 -32
  39. package/scripts/lib/peer-discovery.mjs +73 -22
  40. package/scripts/lib/project-hygiene.mjs +64 -4
  41. package/scripts/lib/reconcile/renderer.mjs +17 -4
  42. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  43. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  44. package/scripts/lib/resource-probe.mjs +18 -2
  45. package/scripts/lib/spiral-carryover.mjs +23 -2
  46. package/scripts/lib/state-md/mission-status.mjs +147 -50
  47. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  48. package/scripts/lib/validate/check-hooks-symmetry.mjs +30 -0
  49. package/scripts/lib/validate/check-rules.mjs +153 -9
  50. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  51. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  52. package/scripts/lib/vcs-repo-spec.mjs +182 -13
  53. package/scripts/lib/wave-resource-gate.mjs +67 -73
  54. package/scripts/materialize-wave-scope.mjs +281 -0
  55. package/scripts/release.mjs +443 -122
  56. package/scripts/run-quality-gate.mjs +14 -0
  57. package/scripts/validate-plugin.mjs +3 -0
  58. package/scripts/validate-wave-scope.mjs +6 -1
  59. package/scripts/vault-backfill.mjs +32 -5
  60. package/skills/_shared/parallel-aware-auq.md +30 -24
  61. package/skills/_shared/parallel-aware-preamble.md +31 -2
  62. package/skills/_shared/state-ownership.md +32 -6
  63. package/skills/bootstrap/SKILL.md +2 -1
  64. package/skills/brainstorm/SKILL.md +18 -18
  65. package/skills/brainstorm/soul.md +12 -0
  66. package/skills/discovery/SKILL.md +28 -24
  67. package/skills/eli5/SKILL.md +43 -0
  68. package/skills/evolve/SKILL.md +8 -9
  69. package/skills/gitlab-ops/SKILL.md +30 -26
  70. package/skills/grill/SKILL.md +6 -6
  71. package/skills/grill/soul.md +16 -0
  72. package/skills/memory-cleanup/SKILL.md +2 -2
  73. package/skills/npm-publish/SKILL.md +4 -4
  74. package/skills/peekaboo-driver/SKILL.md +3 -3
  75. package/skills/plan/SKILL.md +18 -16
  76. package/skills/plan/mode-feature.md +1 -1
  77. package/skills/plan/mode-new.md +35 -23
  78. package/skills/plan/soul.md +12 -0
  79. package/skills/reconcile/SKILL.md +3 -3
  80. package/skills/session-end/SKILL.md +53 -20
  81. package/skills/session-end/phase-3-6-tail.md +37 -2
  82. package/skills/session-start/SKILL.md +69 -35
  83. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  84. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  85. package/skills/session-start/soul.md +110 -0
  86. package/skills/test-runner/SKILL.md +2 -2
  87. package/skills/using-orchestrator/SKILL.md +1 -1
  88. package/skills/wave-executor/wave-loop.md +27 -5
  89. package/skills/write-executable-plan/SKILL.md +6 -6
  90. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  91. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  92. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  93. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  94. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  95. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  96. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  97. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  98. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  99. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  100. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  101. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  102. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  103. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  104. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  105. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  106. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  107. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  108. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  109. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  110. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  111. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  112. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  113. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  114. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  115. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  116. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  117. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -67,6 +67,7 @@ import { discoverActiveSessions } from './session-discovery.mjs';
67
67
  import { readLock, isLockLive, LOCK_PATH } from './session-lock.mjs';
68
68
  import { checkPeerStateMd } from './state-md-peer-guard.mjs';
69
69
  import { listWorktreesChecked } from './worktree/listing.mjs';
70
+ import { parseSessionId } from './session-id.mjs';
70
71
 
71
72
  /** Closed enum of provenance sources. */
72
73
  const SOURCE_DISCOVERED = 'discovered'; // lock + registry unified (irreversibly merged upstream)
@@ -120,24 +121,64 @@ function _peerFromDiscovered(s, nowMs) {
120
121
  return peer;
121
122
  }
122
123
 
124
+ /**
125
+ * Resolve a semantic hint to the concrete local raw ID for discovered surfaces.
126
+ *
127
+ * Semantic labels are attribution, not ownership. A mapping is usable only when
128
+ * current-session.json names the hint and its raw id exactly matches the local
129
+ * readable session.lock. Otherwise return null so a potentially foreign record
130
+ * stays visible. Raw caller ids retain the legacy direct comparison unchanged.
131
+ *
132
+ * This is a consistency check on two repo-global files, NOT a proof of ownership;
133
+ * a colliding semantic label from a foreign live session satisfies it. See the
134
+ * call site in findPeers for the measured case and why it is not closed here.
135
+ *
136
+ * @param {string|null} mySessionId
137
+ * @param {string} repoRoot
138
+ * @returns {string|null}
139
+ */
140
+ function _discoveredSelfSessionId(mySessionId, repoRoot) {
141
+ if (typeof mySessionId !== 'string' || mySessionId.length === 0) return null;
142
+ if (parseSessionId(mySessionId)?.format !== 'semantic') return mySessionId;
143
+
144
+ try {
145
+ const raw = fs.readFileSync(
146
+ path.join(repoRoot, '.orchestrator', 'current-session.json'),
147
+ 'utf8',
148
+ );
149
+ const current = JSON.parse(raw);
150
+ const lock = readLock({ repoRoot });
151
+ if (
152
+ current &&
153
+ typeof current === 'object' &&
154
+ !Array.isArray(current) &&
155
+ typeof current.session_id === 'string' &&
156
+ current.session_id.length > 0 &&
157
+ current.semantic_session_id === mySessionId &&
158
+ lock !== null &&
159
+ typeof lock.session_id === 'string' &&
160
+ lock.session_id === current.session_id
161
+ ) {
162
+ return lock.session_id;
163
+ }
164
+ } catch {
165
+ // Missing or malformed local state cannot prove a semantic alias.
166
+ }
167
+ return null;
168
+ }
169
+
123
170
  /**
124
171
  * findPeers — union of all 3 peer-discovery surfaces, fail-open per surface.
125
172
  *
126
173
  * @param {string} repoRoot Absolute path to the repository root.
127
174
  * @param {object} [opts] passthrough seams shared with the underlying surfaces.
128
- * @param {string|null} [opts.mySessionId] Current session id, used for
129
- * self-exclusion on BOTH surfaces but the two surfaces read different
130
- * id-spaces. Surface A+B (discoverActiveSessions) compares against
131
- * `session_id` from session.lock / the host registry, which is ALWAYS the
132
- * UUID (never `semantic_session_id` see session-lock.mjs). Surface C
133
- * (checkPeerStateMd) compares against STATE.md's `session:` frontmatter
134
- * field, which callers may populate with either id-space as long as it is
135
- * the SAME id-space `mySessionId` was derived from (see the PRECONDITION
136
- * note in state-md-peer-guard.mjs). Passing a semantic id here self-excludes
137
- * correctly on Surface C but NOT on Surface A+B (the UUID lock/registry
138
- * entry for the same session will still surface as a 'discovered' peer of
139
- * itself) — callers that need both surfaces to self-exclude MUST pass the
140
- * UUID.
175
+ * @param {string|null} [opts.mySessionId] Current session id hint. Raw
176
+ * ids self-exclude directly on Surface A+B. A semantic hint maps to a raw
177
+ * discovered-surface id only after the local current-session.json semantic
178
+ * label matches it AND that file's raw id exactly equals local
179
+ * session.lock.session_id. Missing, malformed, or mismatched local state
180
+ * maps nothing, leaving discovered entries visible. Surface C receives this
181
+ * original hint unchanged and compares it to STATE.md's `session:` field.
141
182
  * @param {number} [opts.now] ms-since-epoch (test seam for freshness/age).
142
183
  * @param {number} [opts.freshnessMin] Registry-entry freshness threshold (minutes).
143
184
  * @param {number} [opts.maxAgeHours] STATE.md abandonment threshold (hours).
@@ -155,6 +196,7 @@ function _peerFromDiscovered(s, nowMs) {
155
196
  export async function findPeers(repoRoot, opts = {}) {
156
197
  const nowMs = typeof opts.now === 'number' ? opts.now : Date.now();
157
198
  const mySessionId = opts.mySessionId ?? null;
199
+ const discoveredSelfSessionId = _discoveredSelfSessionId(mySessionId, repoRoot);
158
200
 
159
201
  const peers = [];
160
202
 
@@ -172,15 +214,24 @@ export async function findPeers(repoRoot, opts = {}) {
172
214
  });
173
215
  if (Array.isArray(discovered)) {
174
216
  for (const s of discovered) {
175
- // Self-exclusion (#798): discoverActiveSessions has no notion of "my
176
- // session" it returns every live lock/registry entry, including the
177
- // caller's own SessionStart-hook heartbeat. Exclude it here so it
178
- // never surfaces as a source:'discovered' peer of itself. Mirrors the
179
- // same guard in session-registry.mjs detectPeers() and
180
- // hooks/on-session-start.mjs. `mySessionId === null` needs no special
181
- // case: `!==` against a string sessionId is always true when
182
- // mySessionId is null, so a foreign entry is never filtered.
183
- if (s && typeof s.sessionId === 'string' && s.sessionId !== mySessionId) {
217
+ // Self-exclusion (#798, #1085): discovered entries carry raw ids. A
218
+ // semantic caller hint filters one such id only after the local raw
219
+ // binding was verified above.
220
+ //
221
+ // The binding proves CONSISTENCY of the two local files, not OWNERSHIP.
222
+ // Both are repo-global, so if a foreign live session wrote them last and
223
+ // its semantic label collides with this hint (labels like
224
+ // `main-2026-08-21-session-2` are routinely shared see
225
+ // hooks/on-session-start.mjs), that foreign raw id is filtered and the
226
+ // peer PSA-002 exists to surface goes missing. Measured 2026-08-21:
227
+ // null hint -> peer visible, colliding semantic hint -> peers: [].
228
+ // Closing it needs a per-process ownership proof, not a stronger read
229
+ // of the same two files.
230
+ if (
231
+ s &&
232
+ typeof s.sessionId === 'string' &&
233
+ s.sessionId !== discoveredSelfSessionId
234
+ ) {
184
235
  peers.push(_peerFromDiscovered(s, nowMs));
185
236
  }
186
237
  }
@@ -212,6 +212,21 @@ function duBytes(absPath) {
212
212
  }
213
213
  }
214
214
 
215
+ /**
216
+ * Human-readable byte size. Sub-megabyte totals are the common case once the
217
+ * aged subset is sized correctly, and `Math.round(bytes / MB)` renders every
218
+ * one of them as "0 MB" — which is why the unit is chosen, not fixed.
219
+ * @param {number} bytes
220
+ * @returns {string}
221
+ */
222
+ function formatBytes(bytes) {
223
+ const mb = bytes / (1024 * 1024);
224
+ if (mb >= 10) return `${Math.round(mb)} MB`;
225
+ if (mb >= 1) return `${mb.toFixed(2)} MB`;
226
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
227
+ return `${bytes} B`;
228
+ }
229
+
215
230
  /**
216
231
  * H3 — Aged orchestrator artifacts.
217
232
  *
@@ -219,17 +234,48 @@ function duBytes(absPath) {
219
234
  * 147 MB were Playwright test-run captures dating back seven weeks, plus
220
235
  * 592 files older than 30 days.
221
236
  *
237
+ * Counts and sizes ONLY untracked aged files. Two properties are load-bearing
238
+ * and were both defects until 2026-08-22:
239
+ *
240
+ * 1. The byte total describes the set the sentence names — the aged files —
241
+ * not the whole directory. Sizing the directory overstated the reachable
242
+ * win by a factor of ~18 in this repo (11 MB claimed, 0.68 MB real).
243
+ * 2. Version-tracked paths are excluded. `.orchestrator/policy/*.json` and
244
+ * `.orchestrator/steering/*.md` are read at runtime by hooks and skills;
245
+ * their age is a sign of stability, not decay. Proposing them for
246
+ * "pruning" is proposing to delete source.
247
+ *
222
248
  * @param {string} repoRoot
223
249
  * @param {number} ageDays
224
250
  * @param {number} now
225
- * @returns {object|null}
251
+ * @returns {{check: string, fixable: boolean, agedFiles: number, agedBytes: number, message: string}|null}
226
252
  */
227
253
  export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAYS, now = Date.now()) {
228
254
  const dir = join(repoRoot, '.orchestrator');
229
255
  if (!existsSync(dir)) return null;
230
256
 
257
+ // Resolve the tracked set ONCE per call — `git ls-files --error-unmatch` per
258
+ // file would cost one process per candidate for the same answer.
259
+ //
260
+ // FAIL-SAFE, NOT FAIL-OPEN: when git cannot answer (no repo, git absent,
261
+ // non-zero exit) source and artifact are indistinguishable, so the probe
262
+ // stays SILENT rather than falling back to "nothing is tracked" — that
263
+ // fallback IS the defect this exclusion closes, and it would return
264
+ // invisibly. Losing an advisory finding costs nothing; proposing to delete
265
+ // versioned files costs a restore. `-z` suppresses git's path quoting, so
266
+ // non-ASCII and space-bearing paths compare byte-exactly.
267
+ const trackedRaw = git(['ls-files', '-z', '--', '.orchestrator'], repoRoot);
268
+ if (trackedRaw === null) return null;
269
+ const tracked = new Set(
270
+ trackedRaw
271
+ .split('\0')
272
+ .filter(Boolean)
273
+ .map((p) => join(repoRoot, p)),
274
+ );
275
+
231
276
  const cutoff = now - ageDays * 24 * 60 * 60 * 1000;
232
277
  let aged = 0;
278
+ let agedBytes = 0;
233
279
  let scanned = 0;
234
280
 
235
281
  /** @param {string} d @param {number} depth */
@@ -248,9 +294,16 @@ export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAY
248
294
  if (e.isDirectory()) {
249
295
  walk(full, depth + 1);
250
296
  } else if (e.isFile()) {
297
+ // Counted before the tracked-skip so the 20k bound still measures the
298
+ // walk, not the reportable subset.
251
299
  scanned++;
300
+ if (tracked.has(full)) continue;
252
301
  try {
253
- if (statSync(full).mtimeMs < cutoff) aged++;
302
+ const st = statSync(full);
303
+ if (st.mtimeMs < cutoff) {
304
+ aged++;
305
+ agedBytes += st.size;
306
+ }
254
307
  } catch {
255
308
  /* vanished mid-scan — ignore */
256
309
  }
@@ -261,11 +314,18 @@ export function checkStaleArtifacts(repoRoot, ageDays = DEFAULT_ARTIFACT_AGE_DAY
261
314
 
262
315
  if (aged === 0) return null;
263
316
 
264
- const mb = Math.round((duBytes(dir) ?? 0) / (1024 * 1024));
317
+ // `fixable: true` stays. The only consumer is skills/session-start/SKILL.md
318
+ // (Phase 4), which routes fixable findings to "safe batch work" instead of
319
+ // the operator Q&A — prose guidance to the coordinator, not an automatic
320
+ // deletion run; `grep -rn "fixable" scripts/ skills/ hooks/` finds no other
321
+ // reader than the `mechanical` count below. With tracked paths excluded the
322
+ // claim is now true: every reported path is an untracked artifact.
265
323
  return {
266
324
  check: 'stale-artifacts',
267
325
  fixable: true,
268
- message: `${aged} file(s) under .orchestrator/ older than ${ageDays}d${mb > 0 ? ` (${mb} MB total)` : ''} — candidates for pruning`,
326
+ agedFiles: aged,
327
+ agedBytes,
328
+ message: `${aged} untracked file(s) under .orchestrator/ older than ${ageDays}d${agedBytes > 0 ? ` (${formatBytes(agedBytes)} total)` : ''} — candidates for pruning`,
269
329
  };
270
330
  }
271
331
 
@@ -262,10 +262,23 @@ export function renderRule(learning, metadata) {
262
262
  // purely by `host-class:` (loads only on a matching host) — the intended
263
263
  // behavior, and still never always-on (host-class IS an activation axis).
264
264
  if (globs.length > 0) {
265
- fm.push('globs:');
266
- for (const glob of globs) {
267
- // Always double-quote keeps `*`/`[`/`{` safe; loader strips quotes.
268
- fm.push(` - "${glob}"`);
265
+ // TWO keys, same list, on purpose (#1108). `globs:` is what THIS repo's
266
+ // `rule-loader.mjs` and Cursor read; `paths:` is the ONLY key Claude Code's
267
+ // own loader honours, and a rule without it is loaded UNCONDITIONALLY into
268
+ // every agent (https://code.claude.com/docs/en/memory § Path-specific
269
+ // rules). Emitting `globs:` alone therefore produced a rule that LOOKS
270
+ // scoped, IS scoped for us, and is always-on for the harness — measured
271
+ // 2026-08-22: 16 such rules put 72_195 bytes (~18_300 tokens) into every
272
+ // single dispatch. `check-rules.mjs` now hard-FAILs that shape, so a
273
+ // globs-only rule emitted here would be rejected by the gate the moment it
274
+ // lands. Keep the two lists IDENTICAL: divergence is its own FAIL class,
275
+ // because it would scope Claude Code and Cursor to different rule sets.
276
+ for (const key of ['globs', 'paths']) {
277
+ fm.push(`${key}:`);
278
+ for (const glob of globs) {
279
+ // Always double-quote — keeps `*`/`[`/`{` safe; loader strips quotes.
280
+ fm.push(` - "${glob}"`);
281
+ }
269
282
  }
270
283
  }
271
284
  if (hostClass !== undefined) {