session-orchestrator 5.3.0 → 5.4.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 (206) hide show
  1. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
  2. package/.agents/skills/navigator/SKILL.md +22 -0
  3. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  4. package/.agents/skills/test-audit/SKILL.md +21 -0
  5. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  6. package/.claude-plugin/marketplace.json +1 -1
  7. package/.claude-plugin/plugin.json +1 -1
  8. package/.codex-plugin/plugin.json +1 -1
  9. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  10. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  11. package/.codex-plugin/skills/navigator/agents/openai.yaml +5 -0
  12. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  13. package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
  14. package/.cursor/commands/navigator.md +15 -0
  15. package/.cursor/commands/test-audit.md +14 -0
  16. package/.cursor/rules/060-evolve.mdc +27 -12
  17. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  18. package/.cursor/skills/navigator/SKILL.md +13 -0
  19. package/.cursor/skills/test-audit/SKILL.md +12 -0
  20. package/.cursor-plugin/plugin.json +1 -1
  21. package/.orchestrator/policy/blocked-commands.json +1 -1
  22. package/AGENTS.md +9 -31
  23. package/CHANGELOG.md +70 -0
  24. package/README.md +14 -10
  25. package/docs/agent-authoring.md +2 -2
  26. package/docs/baseline.md +10 -1
  27. package/docs/ci-setup.md +57 -4
  28. package/docs/components.md +12 -12
  29. package/docs/events-schema.md +11 -8
  30. package/docs/persona-panel.md +5 -4
  31. package/docs/rule-authoring.md +27 -12
  32. package/docs/scope-collision-guard.md +26 -4
  33. package/docs/session-config-reference.md +55 -6
  34. package/docs/session-config-template.md +11 -2
  35. package/hooks/_lib/hook-import-set.json +27 -23
  36. package/hooks/hooks-codex.json +1 -1
  37. package/hooks/hooks-cursor.json +5 -0
  38. package/hooks/hooks-pi.json +5 -0
  39. package/hooks/hooks.json +6 -1
  40. package/hooks/on-session-start.mjs +53 -0
  41. package/hooks/on-stop.mjs +3 -1
  42. package/hooks/post-tool-batch-wave-signal.mjs +3 -1
  43. package/hooks/pre-bash-memory-propose-audit.mjs +52 -3
  44. package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
  45. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  46. package/hooks/pre-task-scope-disjoint.mjs +559 -72
  47. package/hooks/subagent-telemetry.mjs +96 -19
  48. package/package.json +3 -3
  49. package/pi/prompts/navigator.md +12 -0
  50. package/pi/prompts/test-audit.md +12 -0
  51. package/rules/README.md +6 -1
  52. package/rules/_index.md +3 -3
  53. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  54. package/rules/opt-in-domain/prompt-caching.md +5 -1
  55. package/rules/opt-in-stack/backend-data.md +9 -1
  56. package/rules/opt-in-stack/backend.md +7 -1
  57. package/rules/opt-in-stack/frontend.md +6 -1
  58. package/rules/opt-in-stack/security-web.md +7 -1
  59. package/rules/opt-in-stack/swift.md +5 -1
  60. package/scripts/apply-session-learnings.mjs +492 -0
  61. package/scripts/archive-closed-prds.mjs +2 -1
  62. package/scripts/autopilot.mjs +49 -53
  63. package/scripts/backfill-abandoned-sessions.mjs +1 -1
  64. package/scripts/baseline-archetypes.mjs +1 -1
  65. package/scripts/check-package-manager.mjs +62 -5
  66. package/scripts/cursor-install.mjs +1 -4
  67. package/scripts/emit-session.mjs +296 -2
  68. package/scripts/export-hw-learnings.mjs +22 -3
  69. package/scripts/fleet-checkin.mjs +147 -0
  70. package/scripts/lib/agent-frontmatter.mjs +3 -3
  71. package/scripts/lib/auto-dream.mjs +7 -6
  72. package/scripts/lib/autopilot/flags.mjs +6 -1
  73. package/scripts/lib/autopilot/loop.mjs +10 -6
  74. package/scripts/lib/baseline-archetypes.mjs +41 -22
  75. package/scripts/lib/config/host-paths.mjs +79 -0
  76. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  77. package/scripts/lib/config/reaper.mjs +7 -6
  78. package/scripts/lib/config-schema.mjs +13 -0
  79. package/scripts/lib/config.mjs +71 -10
  80. package/scripts/lib/events.mjs +21 -2
  81. package/scripts/lib/fleet-protocol.mjs +270 -0
  82. package/scripts/lib/gates/gate-baseline.mjs +13 -3
  83. package/scripts/lib/gates/gate-full.mjs +7 -2
  84. package/scripts/lib/gates/gate-helpers.mjs +93 -2
  85. package/scripts/lib/gates/gate-incremental.mjs +23 -22
  86. package/scripts/lib/gates/gate-per-file.mjs +16 -7
  87. package/scripts/lib/instruction-budget-guard.mjs +69 -11
  88. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  89. package/scripts/lib/learnings/io.mjs +154 -26
  90. package/scripts/lib/learnings/schema.mjs +163 -15
  91. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  92. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  93. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  94. package/scripts/lib/named-vault-resolver.mjs +104 -5
  95. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  96. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  97. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  98. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  99. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  100. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  101. package/scripts/lib/orphan-reaper.mjs +43 -1315
  102. package/scripts/lib/owner-yaml.mjs +104 -6
  103. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  104. package/scripts/lib/process-group.mjs +62 -11
  105. package/scripts/lib/project-hygiene.mjs +21 -1
  106. package/scripts/lib/quality-gate.mjs +45 -5
  107. package/scripts/lib/resource-probe.mjs +1 -1
  108. package/scripts/lib/rule-loader.mjs +100 -62
  109. package/scripts/lib/rules-sync.mjs +27 -12
  110. package/scripts/lib/scope-baseline.mjs +3 -4
  111. package/scripts/lib/session-record-repair.mjs +183 -26
  112. package/scripts/lib/session-registry.mjs +27 -2
  113. package/scripts/lib/session-schema/constants.mjs +30 -4
  114. package/scripts/lib/session-schema/validator.mjs +28 -1
  115. package/scripts/lib/session-token-rollup.mjs +245 -52
  116. package/scripts/lib/sessions-canonical.mjs +100 -9
  117. package/scripts/lib/sh-quote.mjs +29 -0
  118. package/scripts/lib/subagents-schema.mjs +30 -3
  119. package/scripts/lib/telemetry/pricing.mjs +118 -71
  120. package/scripts/lib/tests-src-ratio.mjs +140 -23
  121. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  122. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  123. package/scripts/lib/ux-grill/reconcile.mjs +1 -1
  124. package/scripts/lib/validate/check-agents.mjs +1 -1
  125. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  126. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  127. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  128. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  129. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  130. package/scripts/lib/validate-vendored-rules.mjs +206 -81
  131. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  132. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  133. package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
  134. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  135. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  136. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  137. package/scripts/lib/wave-resource-gate.mjs +34 -8
  138. package/scripts/memory-propose.mjs +18 -5
  139. package/scripts/release-session-lock.mjs +2 -1
  140. package/scripts/release.mjs +10 -3
  141. package/scripts/repair-invalid-sessions.mjs +37 -4
  142. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  143. package/scripts/run-quality-gate.mjs +323 -302
  144. package/scripts/site-numbers.mjs +163 -5
  145. package/scripts/sweep-expired-learnings.mjs +56 -5
  146. package/scripts/sweep-expired-rules.mjs +2 -1
  147. package/scripts/validate-plugin.mjs +7 -0
  148. package/scripts/vault-integration-watcher.mjs +2 -1
  149. package/scripts/vault-mirror.mjs +72 -46
  150. package/scripts/wave-scope-binding.mjs +2 -1
  151. package/skills/_shared/config-reading.md +3 -3
  152. package/skills/_shared/fleet-protocol.md +131 -0
  153. package/skills/_shared/state-ownership.md +5 -0
  154. package/skills/autopilot/SKILL.md +25 -11
  155. package/skills/bootstrap/SKILL.md +1 -1
  156. package/skills/bootstrap/_shared-template.md +1 -1
  157. package/skills/bootstrap/fast-template.md +1 -1
  158. package/skills/bootstrap/private-contract.md +2 -1
  159. package/skills/bootstrap/public-fallback.md +6 -3
  160. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  161. package/skills/claude-md-drift-check/SKILL.md +10 -10
  162. package/skills/claude-md-drift-check/checker.mjs +26 -18
  163. package/skills/evolve/SKILL.md +2 -2
  164. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  165. package/skills/memory-cleanup/SKILL.md +1 -1
  166. package/skills/navigator/SKILL.md +193 -0
  167. package/skills/navigator/references/account-check.sh +87 -0
  168. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  169. package/skills/navigator/references/slot-check.sh +114 -0
  170. package/skills/persona-panel/SKILL.md +2 -2
  171. package/skills/persona-panel/persona-format.md +2 -2
  172. package/skills/persona-panel/presets/designer-lens.md +1 -1
  173. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  174. package/skills/persona-panel/presets/pm-lens.md +1 -1
  175. package/skills/plan/mode-retro.md +19 -4
  176. package/skills/quality-gates/SKILL.md +6 -4
  177. package/skills/remote-offload/SKILL.md +4 -2
  178. package/skills/session-end/learning-patterns.md +22 -12
  179. package/skills/session-end/metrics-collection.md +5 -6
  180. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  181. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  182. package/skills/session-end/references/session-summary-template.md +1 -1
  183. package/skills/session-end/session-metrics-write.md +22 -60
  184. package/skills/session-plan/SKILL.md +0 -3
  185. package/skills/session-start/SKILL.md +6 -0
  186. package/skills/session-start/references/operations-contract.md +4 -0
  187. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  188. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -2
  189. package/skills/test-audit/SKILL.md +152 -0
  190. package/skills/test-audit/references/campaign.md +111 -0
  191. package/skills/test-audit/references/mutate.sh +50 -0
  192. package/skills/wave-executor/SKILL.md +1 -1
  193. package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
  194. package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
  195. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  196. package/templates/personas/README.md +5 -5
  197. package/templates/personas/accounting-compliance.v1.md +1 -1
  198. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  199. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  200. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  201. package/templates/personas/buyer-p3-build.v1.md +1 -1
  202. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  203. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  204. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  205. package/templates/personas/klima-ai-expert.v1.md +1 -1
  206. package/templates/personas/klima-physicist.v1.md +1 -1
@@ -20,16 +20,17 @@
20
20
  * `globs:`. Precedence when BOTH keys are present on the same rule: `globs:`
21
21
  * wins (silently — no merge, no warning) and `paths:` is ignored entirely.
22
22
  *
23
- * A rule file MAY carry a leading single-line provenance header before its
24
- * frontmatter block — the vendoring pipeline (`scripts/rules-sync.mjs`)
25
- * requires a first-line `<!-- source: session-orchestrator plugin
26
- * (canonical: ...) -->` comment on every vendored rule. The frontmatter
27
- * parser tolerates any leading run of blank lines and/or single-line HTML
28
- * comments before the opening `---`, so a vendored rule keeps its `globs:`
29
- * scoping and scalar meta instead of silently falling back to always-on.
30
- * Only single-line comments are tolerated (no multi-line comment blocks);
31
- * files with no header, or starting directly with `---`, parse exactly as
32
- * before.
23
+ * Frontmatter counts ONLY when its opening `---` is the very first line of the
24
+ * file (#1449) — the same rule Claude Code applies when it reads `paths:`.
25
+ * Until #1449 this parser skipped leading blank lines and single-line HTML
26
+ * comments (the vendoring pipeline's `<!-- source: … -->` provenance header),
27
+ * so a rule Claude Code loads ALWAYS-ON was classified path-scoped here and
28
+ * `instruction-budget-guard.mjs` under-counted the always-on surface. A
29
+ * provenance comment now belongs directly AFTER the closing `---` (body text,
30
+ * inert). A frontmatter-shaped block that is NOT on line 1 — any text,
31
+ * comment or blank line before it — yields no globs and no meta at all, and
32
+ * the entry carries `defect: 'frontmatter-not-at-top'` so callers can report
33
+ * the file instead of silently loading it always-on.
33
34
  *
34
35
  * Beyond the `globs:` key (issue #336), the frontmatter parser also captures
35
36
  * these scalar activation keys (issue #694), surfaced on each RuleEntry:
@@ -150,34 +151,15 @@ function matchGlob(filePath, globPattern) {
150
151
  // Minimal YAML frontmatter parser (matches state-md.mjs style)
151
152
  // ---------------------------------------------------------------------------
152
153
 
154
+ // Anchored at offset 0: frontmatter exists only when `---` is line 1 (#1449).
153
155
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
154
156
 
155
- // A single header line is either blank or a complete single-line HTML
156
- // comment (`<!-- ... -->` on one line). Multi-line comment blocks are
157
- // deliberately NOT supported — the vendoring convention is a single first
158
- // line, and tolerating a few stacked comment lines (each matching this RE)
159
- // is sufficient.
160
- const HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
161
-
162
157
  /**
163
- * Skips a leading run of blank lines and/or single-line HTML comments
164
- * (`<!-- ... -->`) at the very start of a rule file's contents, so the
165
- * frontmatter opener (`---`) can be found even when the vendoring pipeline's
166
- * mandatory provenance header precedes it. Stops at the first line that is
167
- * neither blank nor a complete single-line comment.
168
- *
169
- * @param {string} contents - raw file contents
170
- * @returns {string} contents starting at the first non-header line (empty
171
- * string when the entire content is header lines)
158
+ * Defect code for a frontmatter block that does not start on line 1 (#1449).
159
+ * Claude Code ignores such a block, so the rule loads always-on there; the
160
+ * loader mirrors that and tags the entry with this code.
172
161
  */
173
- function stripLeadingProvenanceHeader(contents) {
174
- const lines = contents.split(/\r?\n/);
175
- let idx = 0;
176
- while (idx < lines.length && HEADER_LINE_RE.test(lines[idx])) {
177
- idx++;
178
- }
179
- return lines.slice(idx).join('\n');
180
- }
162
+ export const FRONTMATTER_NOT_AT_TOP = 'frontmatter-not-at-top';
181
163
 
182
164
  // Scalar activation keys captured (issue #694 + #692). The `globs` key keeps
183
165
  // its dedicated sequence/flow handling and is NOT routed through this table.
@@ -193,12 +175,36 @@ const SCALAR_META_KEYS = new Set([
193
175
  'tier',
194
176
  ]);
195
177
 
178
+ /**
179
+ * Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
180
+ *
181
+ * `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
182
+ * js-yaml, which picks SINGLE-quote style whenever the plain form would be
183
+ * ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
184
+ * leading/trailing-quote strip then returns `it''s` for `it's` — measured
185
+ * 2026-08-23 on 40 of 146 live learning insights carrying both characters.
186
+ * Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
187
+ * `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
188
+ */
189
+ function unquoteYamlScalar(valuePart) {
190
+ const v = valuePart.trim();
191
+ if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
192
+ return v.slice(1, -1).replace(/''/g, "'");
193
+ }
194
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
195
+ return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
196
+ }
197
+ return v.replace(/^["']|["']$/g, '');
198
+ }
199
+
196
200
  /**
197
201
  * Parses the YAML frontmatter block for the `globs:` field (issue #336),
198
202
  * its `paths:` alias (issue #795), and the scalar activation keys (issue #694).
199
203
  *
200
204
  * Returns:
201
205
  * - `{ globs: string[] | null, meta: object }` on success
206
+ * - `{ globs: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
207
+ * frontmatter block exists but does not start on line 1 (#1449)
202
208
  * - throws `Error` on malformed frontmatter so the caller can fall back
203
209
  *
204
210
  * `globs` is `null` when no frontmatter, and neither `globs:` nor `paths:` is
@@ -213,41 +219,63 @@ const SCALAR_META_KEYS = new Set([
213
219
  * - all other recognised keys → quote-stripped strings
214
220
  * Unknown keys are ignored without error.
215
221
  *
216
- * A leading run of blank lines and/or single-line HTML comments (e.g. the
217
- * vendoring pipeline's provenance header) is tolerated before the opening
218
- * `---` — see `stripLeadingProvenanceHeader`. Files with no such header parse
219
- * identically to before this tolerance was added.
222
+ * Only a block whose opening `---` is line 1 is frontmatter (#1449), exactly
223
+ * as Claude Code reads it. Anything before the opener — a provenance comment,
224
+ * a blank line, any text — makes the block body text: no globs, no meta, and
225
+ * the `defect` field names the misplacement (see {@link FRONTMATTER_NOT_AT_TOP}).
220
226
  *
221
227
  * @param {string} contents - raw file contents
222
- * @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
228
+ * @returns {{ globs: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
223
229
  */
230
+ export function parseGlobsFrontmatter(contents) {
231
+ const match = FRONTMATTER_RE.exec(contents);
232
+ if (match) return parseFrontmatterBlock(match[1]);
233
+ if (hasDisplacedFrontmatter(contents)) {
234
+ return { globs: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
235
+ }
236
+ return { globs: null, meta: {} };
237
+ }
238
+
224
239
  /**
225
- * Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
240
+ * Does `contents` carry a frontmatter block that is NOT on line 1 (#1449)?
226
241
  *
227
- * `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
228
- * js-yaml, which picks SINGLE-quote style whenever the plain form would be
229
- * ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
230
- * leading/trailing-quote strip then returns `it''s` for `it's` — measured
231
- * 2026-08-23 on 40 of 146 live learning insights carrying both characters.
232
- * Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
233
- * `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
242
+ * Only the FIRST bare `---` line is considered, and its block must parse
243
+ * cleanly AND carry at least one key this loader honours (`globs`, `paths`,
244
+ * or a scalar activation key). That second condition keeps a horizontal rule
245
+ * pair around ordinary prose from being reported: prose between two `---`
246
+ * lines almost always holds a colon-less line (a heading, a sentence), which
247
+ * throws, and a recognised key at column 0 is what makes the misplacement
248
+ * change behaviour at all. The known blind spot is a displaced block that is
249
+ * itself malformed — it is treated as body text, which is what Claude Code
250
+ * does with it too.
251
+ *
252
+ * @param {string} contents - raw file contents
253
+ * @returns {boolean}
234
254
  */
235
- function unquoteYamlScalar(valuePart) {
236
- const v = valuePart.trim();
237
- if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
238
- return v.slice(1, -1).replace(/''/g, "'");
239
- }
240
- if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
241
- return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
255
+ function hasDisplacedFrontmatter(contents) {
256
+ const lines = contents.split(/\r?\n/);
257
+ const opener = lines.indexOf('---');
258
+ // -1: no opener at all. 0: the opener IS line 1, so FRONTMATTER_RE already
259
+ // had its chance (an unclosed block) — not a displacement.
260
+ if (opener <= 0) return false;
261
+ const block = FRONTMATTER_RE.exec(lines.slice(opener).join('\n'));
262
+ if (!block) return false;
263
+ try {
264
+ const { globs, meta } = parseFrontmatterBlock(block[1]);
265
+ return globs !== null || Object.keys(meta).length > 0;
266
+ } catch {
267
+ return false;
242
268
  }
243
- return v.replace(/^["']|["']$/g, '');
244
269
  }
245
270
 
246
- export function parseGlobsFrontmatter(contents) {
247
- const match = FRONTMATTER_RE.exec(stripLeadingProvenanceHeader(contents));
248
- if (!match) return { globs: null, meta: {} };
249
-
250
- const fmText = match[1];
271
+ /**
272
+ * Parses the text BETWEEN the two `---` fences (the frontmatter body).
273
+ * Throws on a malformed top-level line — see {@link parseGlobsFrontmatter}.
274
+ *
275
+ * @param {string} fmText - frontmatter body without its fences
276
+ * @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
277
+ */
278
+ function parseFrontmatterBlock(fmText) {
251
279
  const lines = fmText.split(/\r?\n/);
252
280
 
253
281
  let globsValue = null;
@@ -466,6 +494,9 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
466
494
  * from the `alwaysApply` frontmatter key)
467
495
  * @property {string[]} matchedGlobs - globs that matched; empty when alwaysOn
468
496
  * @property {true} [_parseError] - present only on frontmatter parse-error entries
497
+ * @property {'frontmatter-not-at-top'} [defect] - present only when a frontmatter
498
+ * block exists but does not start on line 1 (#1449); such an entry is always
499
+ * `alwaysOn: true` and carries no meta fields, mirroring Claude Code
469
500
  * @property {string} [description] - frontmatter `description` (issue #694), when present
470
501
  * @property {string} [mode] - frontmatter `mode` (issue #694), when present
471
502
  * @property {string} [hostClass] - frontmatter `host-class` (issue #694), when present
@@ -484,6 +515,10 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
484
515
  * Rules without `globs:` (or its `paths:` alias — issue #795) frontmatter are
485
516
  * always included (alwaysOn: true). Rules with `globs:`/`paths:` are included
486
517
  * only when at least one scopePath matches at least one glob pattern.
518
+ * A rule whose frontmatter does not start on line 1 is always-on with no meta
519
+ * (so no gate excludes it) and carries `defect: 'frontmatter-not-at-top'`
520
+ * (#1449) — no stderr line, because callers such as the instruction-budget
521
+ * guard load the directory several times per run and report the defect themselves.
487
522
  *
488
523
  * After a successful frontmatter parse, deterministic gates (issue #694) are
489
524
  * applied to BOTH always-on and glob-matched rules — a rule must pass ALL
@@ -557,10 +592,11 @@ export function loadApplicableRules({
557
592
 
558
593
  let globs;
559
594
  let meta;
595
+ let defect;
560
596
  let parseError = false;
561
597
 
562
598
  try {
563
- ({ globs, meta } = parseGlobsFrontmatter(content));
599
+ ({ globs, meta, defect } = parseGlobsFrontmatter(content));
564
600
  } catch (err) {
565
601
  process.stderr.write(
566
602
  `[rule-loader] Frontmatter parse error in ${filePath}: ${err.message} — treating as always-on\n`,
@@ -581,7 +617,8 @@ export function loadApplicableRules({
581
617
  const metaFields = metaToEntryFields(meta);
582
618
 
583
619
  if (globs === null) {
584
- // No globs frontmatter (or parse error) → always-on
620
+ // No globs frontmatter (or parse error, or frontmatter not on line 1 —
621
+ // #1449) → always-on
585
622
  results.push({
586
623
  path: filePath,
587
624
  content,
@@ -589,6 +626,7 @@ export function loadApplicableRules({
589
626
  matchedGlobs: [],
590
627
  ...metaFields,
591
628
  ...(parseError ? { _parseError: true } : {}),
629
+ ...(defect ? { defect } : {}),
592
630
  });
593
631
  continue;
594
632
  }
@@ -3,26 +3,37 @@
3
3
  * Syncs canonical rules from the plugin's rules/ library into a consumer repo's .claude/rules/.
4
4
  *
5
5
  * Stdlib-only, cross-platform, no Zod, no third-party parsers. ESM module.
6
- * Preserves local rules (files without the plugin source header).
6
+ * Preserves local rules — files carrying the plugin provenance header in
7
+ * neither accepted position (see `isPluginOwnedContent`).
7
8
  */
8
9
 
9
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
11
  import { join, basename, dirname, resolve } from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
12
- import { validateRuleContent, scanVendoringLeaks } from './validate-vendored-rules.mjs';
13
+ import {
14
+ validateRuleContent,
15
+ scanVendoringLeaks,
16
+ isPluginOwnedContent,
17
+ } from './validate-vendored-rules.mjs';
13
18
  import { isMainModule } from './is-main-module.mjs';
14
19
 
15
20
  // The vendoring sanitizer (issue #1098) lives in validate-vendored-rules.mjs —
16
21
  // it has that module's shape ("judge one rule file → findings") and its
17
22
  // standalone CLI needs it too. Re-exported here so importers that predate the
18
23
  // move (and this file's own tests) keep resolving it from rules-sync.mjs.
19
- export { scanVendoringLeaks };
24
+ //
25
+ // isPluginOwnedContent (#1449) — the ownership predicate below — lives there
26
+ // for the same reason as PLUGIN_HEADER_PREFIX's copy: this module imports
27
+ // validate-vendored-rules.mjs, so defining it here and importing it back into
28
+ // the validator's provenance probe would make the two modules import each
29
+ // other. Re-exported so the ownership rule is reachable from the sync module.
30
+ export { scanVendoringLeaks, isPluginOwnedContent };
20
31
 
21
32
  // Exported (issue #722 Epic A Wave 2) for external consumers (e.g. tests).
22
- // NOT imported by validate-vendored-rules.mjs — that module imports
23
- // validateRuleContent FROM this file (see the pre-write gate below), so
24
- // importing this constant back would create a module-load cycle; it keeps
25
- // its own textually-identical copy instead.
33
+ // NOT imported by validate-vendored-rules.mjs — this module imports
34
+ // validateRuleContent FROM that file (see the pre-write gate below), so
35
+ // importing this constant back there would create a module-load cycle; it
36
+ // keeps its own textually-identical copy instead.
26
37
  export const PLUGIN_HEADER_PREFIX = '<!-- source: session-orchestrator plugin';
27
38
 
28
39
  /**
@@ -170,8 +181,9 @@ function escapeRegex(s) {
170
181
  *
171
182
  * When `validate` is true (the default — issue #722 Epic A Wave 2),
172
183
  * validateRuleContent() runs as a pre-write gate on every source file before
173
- * it is copied. A file with any error-severity violation (paths-frontmatter,
174
- * missing provenance header, unfilled placeholder) is recorded in `errors[]`
184
+ * it is copied. A file with any error-severity violation (frontmatter-not-at-top,
185
+ * globs-paths-mismatch, missing provenance header, unfilled placeholder) is
186
+ * recorded in `errors[]`
175
187
  * and its write is skipped entirely — it is never added to
176
188
  * written/skipped/preserved. Warn-severity violations (zero-match-globs,
177
189
  * foreign-glob) do NOT block the write; they are collected into the
@@ -405,9 +417,12 @@ export function syncRules({
405
417
  continue;
406
418
  }
407
419
 
408
- const firstLine = targetContent.split('\n')[0] ?? '';
409
- if (!firstLine.startsWith(PLUGIN_HEADER_PREFIX)) {
410
- // Local file — preserve it
420
+ // Ownership accepts BOTH header positions (#1449): line 1 (the format
421
+ // every copy vendored before #1449 carries, frontmatter or not) and the
422
+ // first non-blank line after a line-1 frontmatter (the current format).
423
+ // So an old-format copy is rewritten into the new format below, never
424
+ // preserved; only a file with no header in either position is local.
425
+ if (!isPluginOwnedContent(targetContent)) {
411
426
  preserved.push(fileName);
412
427
  continue;
413
428
  }
@@ -604,10 +604,9 @@ export function computeDrift({ repoRoot, threshold = 2.0 } = {}) {
604
604
  ? plannedFilesRaw
605
605
  : 0;
606
606
 
607
- // Mirrors the existing over_delivery_ratio div-by-zero guard in
608
- // skills/wave-executor/wave-loop.md (`files_changed / max(planned_files_count, 1)`,
609
- // in the per-wave metrics section). Deliberately un-line-pinned: that file is
610
- // edited often enough that a pinned number goes stale, as this very comment did.
607
+ // Division-by-zero guard: a baseline that froze zero planned files (no declared
608
+ // file scope, or a non-numeric count coerced to 0 above) would otherwise yield
609
+ // Infinity or NaN; `max(plannedFiles, 1)` keeps the ratio finite and comparable.
611
610
  const filesRatio = round2(actualFiles / Math.max(plannedFiles, 1));
612
611
  const breached = filesRatio >= threshold;
613
612