session-orchestrator 5.7.0 → 5.8.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 (190) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -0
  2. package/.agents/skills/architecture/agents/openai.yaml +5 -0
  3. package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
  4. package/.agents/skills/eval/SKILL.md +1 -1
  5. package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
  6. package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
  7. package/.agents/skills/hook-development/SKILL.md +1 -0
  8. package/.agents/skills/mcp-builder/SKILL.md +1 -0
  9. package/.agents/skills/tmux-layout/SKILL.md +3 -0
  10. package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
  11. package/.agents/skills/vault-mirror/SKILL.md +1 -0
  12. package/.agents/skills/vault-sync/SKILL.md +1 -0
  13. package/.agents/skills/write-executable-plan/SKILL.md +3 -0
  14. package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
  15. package/.claude-plugin/marketplace.json +1 -1
  16. package/.claude-plugin/plugin.json +1 -1
  17. package/.codex-plugin/plugin.json +1 -1
  18. package/.codex-plugin/skills/architecture/SKILL.md +3 -0
  19. package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
  20. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
  21. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  22. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
  23. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
  24. package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
  25. package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
  26. package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
  27. package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
  28. package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
  29. package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
  30. package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
  31. package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
  32. package/.cursor/commands/architecture.md +13 -0
  33. package/.cursor/commands/eval.md +1 -1
  34. package/.cursor/commands/tmux-layout.md +13 -0
  35. package/.cursor/commands/write-executable-plan.md +13 -0
  36. package/.cursor/skills/architecture/SKILL.md +0 -1
  37. package/.cursor/skills/eval/SKILL.md +1 -1
  38. package/.cursor/skills/tmux-layout/SKILL.md +0 -1
  39. package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
  40. package/.cursor-plugin/plugin.json +1 -1
  41. package/CHANGELOG.md +49 -0
  42. package/README.md +10 -9
  43. package/agents/eval-judge.md +5 -5
  44. package/docs/baseline.md +4 -3
  45. package/docs/codex-setup.md +6 -4
  46. package/docs/components.md +5 -4
  47. package/docs/events-schema.md +4 -3
  48. package/docs/github-mirror-protection.md +3 -2
  49. package/docs/install.md +39 -3
  50. package/docs/pi-setup.md +1 -1
  51. package/docs/rule-authoring.md +2 -2
  52. package/docs/session-config-reference.md +7 -5
  53. package/docs/session-config-template.md +3 -2
  54. package/evals/README.md +60 -0
  55. package/evals/instruction-ablation/README.md +147 -0
  56. package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
  57. package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
  58. package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
  59. package/evals/instruction-ablation/run.mjs +336 -0
  60. package/evals/psa-003-foreign-work/case.yaml +16 -0
  61. package/evals/psa-003-foreign-work/fixture.sh +12 -0
  62. package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
  63. package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
  64. package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
  65. package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
  66. package/evals/psa-003-foreign-work/prompt.md +6 -0
  67. package/hooks/_lib/hook-import-set.json +10 -2
  68. package/hooks/hooks-codex.json +1 -1
  69. package/hooks/hooks.json +1 -1
  70. package/hooks/post-tool-batch-wave-signal.mjs +1 -0
  71. package/hooks/post-tooluse-frontend-slop.mjs +11 -1
  72. package/hooks/pre-task-scope-disjoint.mjs +8 -6
  73. package/monitors/monitors.json +1 -1
  74. package/package.json +3 -1
  75. package/pi/prompts/architecture.md +11 -0
  76. package/pi/prompts/eval.md +1 -1
  77. package/pi/prompts/tmux-layout.md +11 -0
  78. package/pi/prompts/write-executable-plan.md +11 -0
  79. package/scripts/backfill-abandoned-sessions.mjs +2 -2
  80. package/scripts/backfill-sessions.mjs +9 -7
  81. package/scripts/codex-install.mjs +108 -23
  82. package/scripts/emit-session.mjs +15 -2
  83. package/scripts/fleet-auflagen.mjs +54 -0
  84. package/scripts/generate-agents-skills.mjs +9 -16
  85. package/scripts/generate-codex-skills.mjs +8 -6
  86. package/scripts/generate-cursor-adapter.mjs +9 -74
  87. package/scripts/generate-pi-prompts.mjs +8 -74
  88. package/scripts/github-protection-audit.mjs +8 -0
  89. package/scripts/lib/auto-dialectic.mjs +10 -9
  90. package/scripts/lib/auto-dream.mjs +8 -10
  91. package/scripts/lib/autopilot/telemetry.mjs +12 -6
  92. package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
  93. package/scripts/lib/config/drift-check.mjs +1 -1
  94. package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
  95. package/scripts/lib/config.mjs +5 -1
  96. package/scripts/lib/convergence-monitor.mjs +5 -3
  97. package/scripts/lib/ecosystem-health.mjs +5 -3
  98. package/scripts/lib/eval/engine.mjs +61 -27
  99. package/scripts/lib/eval/judge.mjs +14 -8
  100. package/scripts/lib/eval/session-resolve.mjs +7 -2
  101. package/scripts/lib/events-retention-banner.mjs +5 -2
  102. package/scripts/lib/events.mjs +19 -5
  103. package/scripts/lib/fleet-protocol.mjs +98 -1
  104. package/scripts/lib/instruction-budget-guard.mjs +25 -18
  105. package/scripts/lib/io.mjs +172 -35
  106. package/scripts/lib/learnings/candidates.mjs +7 -28
  107. package/scripts/lib/learnings/io.mjs +7 -2
  108. package/scripts/lib/learnings/judgment.mjs +5 -8
  109. package/scripts/lib/learnings/select.mjs +3 -3
  110. package/scripts/lib/maintenance-due-banner.mjs +2 -2
  111. package/scripts/lib/memory-proposals/sink.mjs +5 -6
  112. package/scripts/lib/memory-proposals/store.mjs +3 -5
  113. package/scripts/lib/mirror-issues-banner.mjs +11 -1
  114. package/scripts/lib/native-command.mjs +48 -0
  115. package/scripts/lib/orphan-reaper/scan.mjs +29 -3
  116. package/scripts/lib/peer-cards/writer.mjs +6 -18
  117. package/scripts/lib/platform.mjs +113 -4
  118. package/scripts/lib/plugin-package-stage.mjs +169 -0
  119. package/scripts/lib/plugin-surface.mjs +121 -0
  120. package/scripts/lib/quality-gate.mjs +30 -25
  121. package/scripts/lib/reconcile/idempotency.mjs +9 -10
  122. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
  123. package/scripts/lib/reconcile/writer.mjs +11 -17
  124. package/scripts/lib/rule-loader.mjs +32 -6
  125. package/scripts/lib/scope-echo.mjs +12 -2
  126. package/scripts/lib/scope-gate.mjs +2 -1
  127. package/scripts/lib/session-close-backfill.mjs +3 -4
  128. package/scripts/lib/session-id.mjs +2 -1
  129. package/scripts/lib/session-record-repair.mjs +9 -13
  130. package/scripts/lib/session-schema/constants.mjs +20 -5
  131. package/scripts/lib/session-start-probes.mjs +2 -2
  132. package/scripts/lib/sessions-staleness-banner.mjs +2 -3
  133. package/scripts/lib/skill-evolution/engine.mjs +6 -9
  134. package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
  135. package/scripts/lib/slopcheck.mjs +9 -7
  136. package/scripts/lib/sunset/walker.mjs +3 -3
  137. package/scripts/lib/telemetry/sync.mjs +3 -1
  138. package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
  139. package/scripts/lib/test-sibling-coverage.mjs +167 -0
  140. package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
  141. package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
  142. package/scripts/lib/user-invocable-skills.mjs +37 -10
  143. package/scripts/lib/ux-grill/run-record.mjs +5 -3
  144. package/scripts/lib/validate/check-banner-parity.mjs +1 -0
  145. package/scripts/lib/validate/check-skills.mjs +12 -2
  146. package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
  147. package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
  148. package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
  149. package/scripts/lib/validate/check-unwired-features.mjs +1 -1
  150. package/scripts/lib/vault-archive.mjs +2 -4
  151. package/scripts/lib/vault-mirror/process.mjs +207 -26
  152. package/scripts/lib/vault-mirror/utils.mjs +16 -1
  153. package/scripts/lib/vault-sync-baseline.mjs +9 -8
  154. package/scripts/lib/wave-transcript-tail.mjs +8 -4
  155. package/scripts/lib/worktree/meta.mjs +7 -3
  156. package/scripts/mcp-server.mjs +158 -0
  157. package/scripts/migrate-sessions-jsonl.mjs +7 -5
  158. package/scripts/migrate-vault-paths.mjs +7 -4
  159. package/scripts/print-learnings-index.mjs +2 -1
  160. package/scripts/relocate-vault-corpus.mjs +5 -3
  161. package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
  162. package/scripts/self-update.mjs +179 -39
  163. package/scripts/site-numbers.mjs +4 -3
  164. package/scripts/sweep-expired-learnings.mjs +6 -4
  165. package/scripts/sync-vault-schema.mjs +3 -7
  166. package/scripts/windows-setup.mjs +45 -0
  167. package/skills/_shared/fleet-protocol.md +8 -0
  168. package/skills/architecture/SKILL.md +1 -0
  169. package/skills/bootstrap/_shared-template.md +5 -2
  170. package/skills/claude-md-drift-check/SKILL.md +4 -1
  171. package/skills/claude-md-drift-check/checker.mjs +83 -10
  172. package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
  173. package/skills/eval/SKILL.md +12 -13
  174. package/skills/eval/rubric-v3.md +521 -0
  175. package/skills/frontmatter-guard/SKILL.md +1 -0
  176. package/skills/gitlab-portfolio/SKILL.md +1 -0
  177. package/skills/hook-development/SKILL.md +1 -0
  178. package/skills/mcp-builder/SKILL.md +1 -0
  179. package/skills/repo-audit/SKILL.md +2 -2
  180. package/skills/session-end/SKILL.md +32 -1
  181. package/skills/session-end/vault-operations.md +12 -2
  182. package/skills/session-start/SKILL.md +2 -2
  183. package/skills/session-start/phase-7-1-premise-check.md +2 -1
  184. package/skills/session-start/references/operations-contract.md +4 -1
  185. package/skills/tmux-layout/SKILL.md +1 -0
  186. package/skills/vault-mirror/SKILL.md +1 -0
  187. package/skills/vault-sync/SKILL.md +6 -6
  188. package/skills/vault-sync/validator.sh +29 -10
  189. package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
  190. package/skills/write-executable-plan/SKILL.md +1 -0
@@ -1,6 +1,6 @@
1
1
  {
2
- "generated_at": "2026-10-03T07:30:53.188Z",
3
- "head": "0cfbaa2c4fad16d825b94b8000ca8aea3dc9af68",
2
+ "generated_at": "2026-10-05T10:45:41.038Z",
3
+ "head": "94dc4a0cf182a06492feb217162d290e9fd0e921",
4
4
  "entries": [
5
5
  {
6
6
  "file": "hooks/_lib/atomic-json.mjs",
@@ -1264,6 +1264,14 @@
1264
1264
  "subagent-telemetry.mjs"
1265
1265
  ]
1266
1266
  },
1267
+ {
1268
+ "file": "scripts/lib/plugin-surface.mjs",
1269
+ "reachable_from": [
1270
+ "on-session-end.mjs",
1271
+ "on-session-start.mjs",
1272
+ "skill-invocation-telemetry.mjs"
1273
+ ]
1274
+ },
1267
1275
  {
1268
1276
  "file": "scripts/lib/plugin-update-banner.mjs",
1269
1277
  "reachable_from": [
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v5.7.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v5.8.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
11
11
  "async": false
12
12
  },
13
13
  {
package/hooks/hooks.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "echo '🎯 Session Orchestrator v5.7.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v5.8.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
@@ -265,6 +265,7 @@ const GIT_OPTS = (projectDir) => ({
265
265
  cwd: projectDir,
266
266
  encoding: 'utf8',
267
267
  timeout: 1_500,
268
+ maxBuffer: 64 * 1024 * 1024,
268
269
  stdio: ['ignore', 'pipe', 'ignore'],
269
270
  env: filteredGitEnv(),
270
271
  });
@@ -3,6 +3,10 @@
3
3
  * post-tooluse-frontend-slop.mjs — PostToolUse hook that runs the deterministic
4
4
  * frontend-slop detector on a UI file right after it is edited.
5
5
  *
6
+ * @deprecated since 5.8.0 (#1121) — removal in the next major. An opted-in
7
+ * repo (`frontend-slop-hook.enabled: true`) gets one stderr notice per run;
8
+ * behaviour is otherwise unchanged.
9
+ *
6
10
  * Issue #684 (Track 2 / item B). OPT-IN, warn-only, NON-BLOCKING. PostToolUse
7
11
  * fires AFTER the edit is already applied, so this hook can never block — it
8
12
  * only surfaces an `additionalContext` roll-up of detector findings so the
@@ -15,7 +19,7 @@
15
19
  * 4. file_path gate: exit 0 if absent / non-string.
16
20
  * 5. Extension gate: exit 0 unless path.extname(file) ∈ SCANNABLE_EXTS (SSOT).
17
21
  * 6. Session Config gate: exit 0 unless `frontend-slop-hook.enabled === true`
18
- * (OPT-IN — default off).
22
+ * (OPT-IN — default off). Enabled → one deprecation line on stderr (#1121).
19
23
  * 7. detectFiles([file]) (fail-soft). No findings → exit 0 silently.
20
24
  * 8. Output: additionalContext roll-up on stdout (NEVER blocks). Best-effort
21
25
  * orchestrator.frontend_slop.warning event.
@@ -149,6 +153,12 @@ async function main() {
149
153
  const config = await loadFrontendSlopHookConfig({ repoRoot: getProjectDir() });
150
154
  if (config.enabled !== true) return;
151
155
 
156
+ // #1121: deprecated, not removed. Only opted-in repos hear it, once per run,
157
+ // on stderr — stdout is the hook-JSON channel the harness parses.
158
+ process.stderr.write(
159
+ 'ℹ frontend-slop-hook is deprecated, removal in the next major — #1121\n',
160
+ );
161
+
152
162
  // Run the deterministic detector (fail-soft: unreadable files → []).
153
163
  const findings = detectFiles([filePath]);
154
164
  if (findings.length === 0) return;
@@ -172,12 +172,14 @@
172
172
  * ## Observability — the ledger half of #1092
173
173
  *
174
174
  * Every dispatch DECISION also appends one `orchestrator.wave_dispatch.scope_checked`
175
- * record to `<session root>/.orchestrator/metrics/events.jsonl` — the root
176
- * `resolveSessionRoot()` resolves (`scripts/lib/platform.mjs`: the git toplevel
177
- * of the payload `cwd` — its nearest `.git` ancestor when git cannot answer —
178
- * but never above a launch dir inside it; so neither the subdirectory a `cd`
179
- * moved to, #1489 Pkt 6, nor the launch dir a worktree session left), which is
180
- * also where the ledger and its lock live. The reason it
175
+ * record to the events ledger of the session root's repo —
176
+ * `eventsFilePath(sessionRoot)`, which maps a linked worktree to its main
177
+ * checkout (one ledger per repo, #1514 point 1). The DISPATCH ledger and its
178
+ * lock stay on the root `resolveSessionRoot()` resolves (`scripts/lib/platform.mjs`: the git
179
+ * toplevel of the payload `cwd` — its nearest `.git` ancestor when git cannot
180
+ * answer — but never above a launch dir inside it; so neither the subdirectory
181
+ * a `cd` moved to, #1489 Pkt 6, nor the launch dir a worktree session left),
182
+ * and session attribution is still read there. The reason the record
181
183
  * exists is matrix rows 5/6: the no-signal ALLOW used to be byte-identical to
182
184
  * "the guard never ran", and the in-ledger counter added first is a WAVE tally —
183
185
  * it cannot say WHICH dispatch carried a scope. Payload: `wave` (omitted, never
@@ -2,7 +2,7 @@
2
2
  {
3
3
  "name": "ecosystem-health",
4
4
  "command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/lib/ecosystem-health.mjs --watch",
5
- "description": "Local state-file watcher: every 900 s it stats and reads ONE file, .orchestrator/metrics/ecosystem-health.jsonl under the plugin root, and emits one NDJSON line per change (appeared / disappeared / changed). No network, no CI or branch probing, no repo enumeration, no Session Config read. Nothing writes that file yet (0 writers, measured 2026-09-18), so every run emits `no-state-yet` and idles until SIGTERM — ceiling and revisit trigger at DEFAULT_INTERVAL_S in scripts/lib/ecosystem-health.mjs. Hung on session-start because the previous trigger (on-skill-invoke:ecosystem-health) was self-referential: it could only start when the ecosystem-health skill ran, and that skill has 0 recorded invocations fleet-wide, so the watcher never started once.",
5
+ "description": "Local state-file watcher: every 900 s it stats and reads ONE file, .orchestrator/metrics/ecosystem-health.jsonl under the project dir (getProjectDir, #1517), and emits one NDJSON line per change (appeared / disappeared / changed). No network, no CI or branch probing, no repo enumeration, no Session Config read. Nothing writes that file yet (0 writers, measured 2026-09-18), so every run emits `no-state-yet` and idles until SIGTERM — ceiling and revisit trigger at DEFAULT_INTERVAL_S in scripts/lib/ecosystem-health.mjs. Hung on session-start because the previous trigger (on-skill-invoke:ecosystem-health) was self-referential: it could only start when the ecosystem-health skill ran, and that skill has 0 recorded invocations fleet-wide, so the watcher never started once.",
6
6
  "when": "on-skill-invoke:session-start"
7
7
  },
8
8
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "5.7.0",
3
+ "version": "5.8.0",
4
4
  "description": "Your coding agents get an agreed plan, a check after each round of changes and a handover to the next session. Plan, Go, Close: /session reads your repo and agrees the scope, /go runs the work in waves with your tests and typecheck after each implementation wave, /close runs the full gate, verifies, commits and files the rest as issues. Scope and destructive-command guards, crash-safe resume, learnings you approve. For Claude Code, Codex CLI, Cursor and Pi.",
5
5
  "type": "module",
6
6
  "homepage": "https://session-orchestrator.com",
@@ -41,6 +41,8 @@
41
41
  "output-styles/",
42
42
  "monitors/",
43
43
  "assets/",
44
+ "evals/",
45
+ "!evals/results/**",
44
46
  "!assets/marketing/**",
45
47
  ".claude-plugin/",
46
48
  ".codex-plugin/",
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Use when the user asks to improve architecture, find refactoring opportunities, surface deepening opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable. Surfaces shallow modules and hypothetical seams using a precise vocabulary (Module / Interface / Implementation / Depth / Seam / Adapter / Leverage / Locality from LANGUAGE.md).
3
+ ---
4
+
5
+ # /architecture
6
+
7
+ Use the Session Orchestrator skill definition at `skills/architecture/SKILL.md`.
8
+
9
+ Arguments: $@
10
+
11
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Use this skill to run an honest session-process evaluation (Standard v1, aiat-llm-eval/1.0) — score the last completed orchestrator session against the pre-registered rubric-v2 dimensions, run /eval, evaluate this session, produce an eval report, or re-verify a stored eval run for reproducibility. Deterministic-first with an optional advisory LLM judge; never produces a global score."
2
+ description: "Use this skill to run an honest session-process evaluation (Standard v1, aiat-llm-eval/1.0) — score the last completed orchestrator session against the pre-registered rubric-v3 dimensions, run /eval, evaluate this session, produce an eval report, or re-verify a stored eval run for reproducibility. Deterministic-first with an optional advisory LLM judge; never produces a global score."
3
3
  argument-hint: "[--session <id>] [--no-write] [--verify <run-id>]"
4
4
  ---
5
5
 
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: "Use this skill when the operator wants a prepared tmux visualization layout for the session's side-channels (STATE.md tail, CI-watch, events.jsonl tail). Renders a 4-pane default layout or debug layout. Read-only side-channel observability — the coordinator chat stays in the operator's original terminal. Trigger phrases: \"tmux layout\", \"split panes for ci watch\", \"visualize session side-channels\", \"show me state-md tail and ci\"."
3
+ ---
4
+
5
+ # /tmux-layout
6
+
7
+ Use the Session Orchestrator skill definition at `skills/tmux-layout/SKILL.md`.
8
+
9
+ Arguments: $@
10
+
11
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: "Use when you have a PRD or design spec and need a bite-sized, executable implementation plan that any agent can follow without re-deriving structure. Produces `docs/plans/YYYY-MM-DD-<feature>.md` with per-task Files block, complete code per step (no placeholders), and exact verification commands. Rejects \"TBD\", \"TODO\", \"add error handling\", \"similar to Task N\"."
3
+ ---
4
+
5
+ # /write-executable-plan
6
+
7
+ Use the Session Orchestrator skill definition at `skills/write-executable-plan/SKILL.md`.
8
+
9
+ Arguments: $@
10
+
11
+ Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
@@ -42,7 +42,7 @@ import path from 'node:path';
42
42
  import { parseArgs } from 'node:util';
43
43
 
44
44
  import { backfillAbandonedSession, isUuid } from './lib/session-close-backfill.mjs';
45
- import { SCAN_CHUNK_BYTES, emitEvent, listEventSourcesNewestFirst, tombstoneGap } from './lib/events.mjs';
45
+ import { SCAN_CHUNK_BYTES, emitEvent, eventsFilePath, listEventSourcesNewestFirst, tombstoneGap } from './lib/events.mjs';
46
46
  import { ARCHIVE_DIR_NAME } from './lib/events-schema.mjs';
47
47
  import { getProjectDir } from './lib/platform.mjs';
48
48
  import { isMainModule } from './lib/is-main-module.mjs';
@@ -173,7 +173,7 @@ export function planSessions({ repoRoot }) {
173
173
  * @returns {{ plan: Array<{ sessionId: string, semanticSessionId: string|null }>, historyGaps: number }}
174
174
  */
175
175
  function planWithGaps({ repoRoot }) {
176
- const eventsPath = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
176
+ const eventsPath = eventsFilePath(repoRoot); // #1514: the repo's one ledger
177
177
 
178
178
  // Two independent UUID -> semantic bridges (#1167). `lock.acquired` is the
179
179
  // original one, but a session that LOST the lock-acquire race never emits it;
@@ -28,8 +28,9 @@
28
28
  * {"mode":"dry-run|apply|mark-deprecated-only","total":N,"rewritten":N,"deprecated":N,"unchanged":N,"backup":"<path>|null"}
29
29
  */
30
30
 
31
- import { readFileSync, writeFileSync, renameSync, copyFileSync, existsSync } from 'node:fs';
32
- import { join, dirname, basename } from 'node:path';
31
+ import { readFileSync, copyFileSync, existsSync } from 'node:fs';
32
+ import { basename } from 'node:path';
33
+ import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
33
34
  import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
34
35
 
35
36
  const DEFAULT_FILE = '.orchestrator/metrics/sessions.jsonl';
@@ -253,17 +254,18 @@ async function main() {
253
254
  // by a pre-write copy-to-backup. This eliminates the crash window where the
254
255
  // canonical path could be briefly missing if the rename dance were split:
255
256
  // 1. copyFileSync: canonical → .bak-<ISO> (backup; canonical still present)
256
- // 2. writeFileSync: rewritten → tmp (new content; canonical still present)
257
- // 3. renameSync: tmp → canonical (atomic replace per POSIX)
257
+ // 2+3. atomicWriteWithBackup: rewritten → tmp → rename over canonical
258
+ // (atomic replace per POSIX; the helper unlinks its tmp on failure)
258
259
  // At every point between steps, the canonical file exists and holds either
259
260
  // the old content or the new content. Recovery artifact: .bak-<ISO>.
260
261
  const iso = new Date().toISOString().replace(/[:.]/g, '-');
261
262
  const backupPath = `${args.file}.bak-${iso}`;
262
- const tmpPath = join(dirname(args.file), `.${basename(args.file)}.tmp-${process.pid}`);
263
263
  try {
264
264
  copyFileSync(args.file, backupPath);
265
- writeFileSync(tmpPath, rewritten.join('\n') + '\n', 'utf8');
266
- renameSync(tmpPath, args.file);
265
+ const res = atomicWriteWithBackup(args.file, rewritten.join('\n') + '\n', {
266
+ tmpPrefix: `.${basename(args.file)}.tmp`,
267
+ });
268
+ if (!res.ok) throw envelopeToError(res);
267
269
  summary.backup = backupPath;
268
270
  } catch (err) {
269
271
  const recoveryHint = existsSync(backupPath)
@@ -2,22 +2,31 @@
2
2
  /**
3
3
  * Install Session Orchestrator through Codex's public plugin lifecycle.
4
4
  *
5
+ * The marketplace reads the host-local packed copy (`npm pack` of this clone,
6
+ * with runtime dependencies, under ~/.cache/session-orchestrator/plugin-package),
7
+ * never the clone itself: a marketplace on the working checkout copied all of
8
+ * it into the Codex plugin cache — 15742 files, `.env.local`, `.orchestrator/`
9
+ * and `tests/` included (#1518, measured 2026-10-03).
10
+ *
5
11
  * Exit codes:
6
12
  * 0 — installed, enabled, and verified
7
13
  * 1 — argument or user-correctable environment error
8
14
  * 2 — system, upstream command/JSON, or runtime postcondition failure
9
15
  */
10
16
 
11
- import { readFileSync } from 'node:fs';
17
+ import { readFileSync, realpathSync } from 'node:fs';
18
+ import os from 'node:os';
12
19
  import path from 'node:path';
13
20
  import { spawnSync } from 'node:child_process';
14
21
  import { fileURLToPath } from 'node:url';
15
22
 
16
23
  import { validateCodexPluginContract } from './lib/codex/plugin-contract.mjs';
24
+ import { resolveStageDir, stagePackage } from './lib/plugin-package-stage.mjs';
17
25
 
18
26
  const __filename = fileURLToPath(import.meta.url);
19
27
  const SCRIPT_DIR = path.dirname(__filename);
20
28
  const SO_ROOT = path.dirname(SCRIPT_DIR);
29
+ const STAGE_DIR = resolveStageDir();
21
30
 
22
31
  const MIN_CODEX_VERSION = [0, 144, 4];
23
32
  const MARKETPLACE_NAME = 'kanevry';
@@ -166,27 +175,28 @@ function requireStableFeature(features, name) {
166
175
  }
167
176
  }
168
177
 
169
- function readLocalJson(relativePath, label) {
178
+ function readLocalJson(relativePath, label, root = SO_ROOT) {
170
179
  try {
171
- return JSON.parse(readFileSync(path.join(SO_ROOT, relativePath), 'utf8'));
180
+ return JSON.parse(readFileSync(path.join(root, relativePath), 'utf8'));
172
181
  } catch (error) {
173
182
  throw new InstallerError(`Cannot read valid ${label}: ${error.message}`);
174
183
  }
175
184
  }
176
185
 
177
- function readBaseVersion() {
178
- const packageJson = readLocalJson('package.json', 'package.json');
186
+ function readBaseVersion(root = SO_ROOT) {
187
+ const packageJson = readLocalJson('package.json', 'package.json', root);
179
188
  if (typeof packageJson.version !== 'string' || packageJson.version.trim() === '') {
180
189
  throw new InstallerError('package.json must contain a non-empty version string.');
181
190
  }
182
191
  return packageJson.version;
183
192
  }
184
193
 
185
- function validateLocalContract() {
186
- const expectedBaseVersion = readBaseVersion();
187
- const manifest = readLocalJson(path.join('.codex-plugin', 'plugin.json'), 'Codex plugin manifest');
194
+ /** Validates the plugin Codex will actually install: the staged packed copy. */
195
+ function validateLocalContract(root) {
196
+ const expectedBaseVersion = readBaseVersion(root);
197
+ const manifest = readLocalJson(path.join('.codex-plugin', 'plugin.json'), 'Codex plugin manifest', root);
188
198
  const verdict = validateCodexPluginContract({
189
- pluginRoot: SO_ROOT,
199
+ pluginRoot: root,
190
200
  expectedBaseVersion,
191
201
  });
192
202
 
@@ -210,12 +220,34 @@ function requireArray(value, label) {
210
220
  return value;
211
221
  }
212
222
 
223
+ const realOrResolved = (p) => { try { return realpathSync(p); } catch { return path.resolve(p); } };
224
+
213
225
  function sameLocalPath(left, right) {
214
- return typeof left === 'string' && path.resolve(left) === path.resolve(right);
226
+ return typeof left === 'string' && realOrResolved(left) === realOrResolved(right);
227
+ }
228
+
229
+ function marketplaceSource(entry) {
230
+ return { sourceType: entry.marketplaceSource?.sourceType, source: entry.marketplaceSource?.source ?? entry.root };
215
231
  }
216
232
 
217
233
  function assertMarketplaceCompatible(payload) {
218
234
  const marketplaces = requireArray(payload.marketplaces, 'marketplaces');
235
+ // Any local marketplace on the working checkout copies all of it into the
236
+ // plugin cache (#1518) — refuse, naming the one-time re-registration.
237
+ const onCheckout = marketplaces.filter((entry) => {
238
+ if (!isRecord(entry)) return false;
239
+ const { sourceType, source } = marketplaceSource(entry);
240
+ return sourceType === 'local' && sameLocalPath(source, SO_ROOT);
241
+ });
242
+ if (onCheckout.length > 0) {
243
+ const removes = onCheckout.map((entry) => `codex plugin marketplace remove ${String(entry.name)}`).join(' && ');
244
+ throw new InstallerError(
245
+ `Marketplace ${onCheckout.map((entry) => `'${String(entry.name)}'`).join(', ')} reads this clone '${SO_ROOT}' — `
246
+ + `the Codex plugin cache becomes a full copy incl. .env.local (#1518). Re-register once: `
247
+ + `${removes} && node scripts/codex-install.mjs (it adds the packed copy at '${STAGE_DIR}').`,
248
+ ERROR_CATEGORIES.USER,
249
+ );
250
+ }
219
251
  const matches = marketplaces.filter((entry) => isRecord(entry) && entry.name === MARKETPLACE_NAME);
220
252
 
221
253
  if (matches.length > 1) {
@@ -227,28 +259,61 @@ function assertMarketplaceCompatible(payload) {
227
259
  }
228
260
  if (matches.length === 0) return;
229
261
 
230
- const existing = matches[0];
231
- const sourceType = existing.marketplaceSource?.sourceType;
232
- const source = existing.marketplaceSource?.source ?? existing.root;
233
- if (sourceType !== 'local' || !sameLocalPath(source, SO_ROOT)) {
262
+ const { sourceType, source } = marketplaceSource(matches[0]);
263
+ if (sourceType !== 'local' || !sameLocalPath(source, STAGE_DIR)) {
234
264
  throw new InstallerError(
235
265
  `Marketplace '${MARKETPLACE_NAME}' already points to '${String(source)}' (${String(sourceType)}), `
236
- + `not '${SO_ROOT}'. Resolve the conflict manually after reviewing `
266
+ + `not '${STAGE_DIR}'. Resolve the conflict manually after reviewing `
237
267
  + `'codex plugin marketplace list --json'; this installer will not remove or replace it.`,
238
268
  ERROR_CATEGORIES.USER,
239
269
  );
240
270
  }
241
271
  }
242
272
 
273
+ /**
274
+ * Codex's personal marketplace (`~/.agents/plugins/marketplace.json`, paths
275
+ * relative to the home dir) may list a plugin that resolves to this clone — on
276
+ * the reference host `~/plugins/session-orchestrator` is a symlink to the main
277
+ * checkout, and the resulting `session-orchestrator@local` cache held
278
+ * `.env.local` (#1518). That catalog belongs to the operator, so this is a
279
+ * read-only hint naming the cleanup, never a refusal or an edit.
280
+ */
281
+ function printPersonalMarketplaceHint(json) {
282
+ const home = os.homedir();
283
+ const catalog = path.join(home, '.agents', 'plugins', 'marketplace.json');
284
+ let plugins;
285
+ try {
286
+ plugins = JSON.parse(readFileSync(catalog, 'utf8')).plugins;
287
+ } catch {
288
+ return;
289
+ }
290
+ if (!Array.isArray(plugins)) return;
291
+ const hits = plugins.filter((plugin) => (
292
+ isRecord(plugin) && typeof plugin.source?.path === 'string'
293
+ && sameLocalPath(path.resolve(home, plugin.source.path), SO_ROOT)
294
+ ));
295
+ for (const plugin of hits) {
296
+ const linked = path.resolve(home, plugin.source.path);
297
+ const message = `Hint: ${catalog} lists '${String(plugin.name)}' at '${linked}', which resolves to this clone — `
298
+ + `its Codex cache copies the whole checkout incl. .env.local (#1518). Cleanup, once: `
299
+ + `(1) let this installer finish (it removes ${PLUGIN_NAME}@local after a healthy install); `
300
+ + `(2) remove that entry from ${catalog}; (3) delete the link '${linked}' if it is a symlink; `
301
+ + `(4) after restarting Codex, delete the old cache folders under ~/.codex/plugins/cache/local/ `
302
+ + `(${PLUGIN_NAME}/ and any plugin-install-*/ or plugin-backup-*/ holding ${PLUGIN_NAME}).`;
303
+ if (json) process.stderr.write(`${message}\n`);
304
+ else progress(json, message);
305
+ }
306
+ }
307
+
243
308
  function assertMarketplaceAddResult(payload) {
244
309
  if (payload.marketplaceName !== MARKETPLACE_NAME) {
245
310
  throw new InstallerError(
246
311
  `Marketplace add returned '${String(payload.marketplaceName)}', expected '${MARKETPLACE_NAME}'.`,
247
312
  );
248
313
  }
249
- if (!sameLocalPath(payload.installedRoot, SO_ROOT)) {
314
+ if (!sameLocalPath(payload.installedRoot, STAGE_DIR)) {
250
315
  throw new InstallerError(
251
- `Marketplace add returned root '${String(payload.installedRoot)}', expected '${SO_ROOT}'.`,
316
+ `Marketplace add returned root '${String(payload.installedRoot)}', expected '${STAGE_DIR}'.`,
252
317
  );
253
318
  }
254
319
  if (typeof payload.alreadyAdded !== 'boolean') {
@@ -336,18 +401,38 @@ function install({ json }) {
336
401
  requireStableFeature(features, 'hooks');
337
402
  progress(json, 'Features: plugins stable true; hooks stable true');
338
403
 
339
- const expectedVersion = validateLocalContract();
340
- progress(json, `Local contract: valid (${expectedVersion})`);
341
-
342
404
  const marketplaceList = runCodexJson(
343
405
  ['plugin', 'marketplace', 'list', '--json'],
344
406
  'Marketplace list',
345
407
  );
408
+ // Checked before staging: a refused install must not replace the stage dir.
346
409
  assertMarketplaceCompatible(marketplaceList);
347
410
 
348
- progress(json, `Marketplace: adding '${SO_ROOT}'`);
411
+ printPersonalMarketplaceHint(json);
412
+
413
+ // The contract is checked on the new copy BEFORE it replaces the shared
414
+ // stage: an invalid copy must never become what Claude Code or Codex reads.
415
+ let expectedVersion;
416
+ const staged = stagePackage({
417
+ soRoot: SO_ROOT,
418
+ stageDir: STAGE_DIR,
419
+ installDeps: true,
420
+ log: (line) => progress(json, `Package: ${line}`),
421
+ validate: (dir) => {
422
+ try {
423
+ expectedVersion = validateLocalContract(dir);
424
+ return { ok: true };
425
+ } catch (error) {
426
+ return { ok: false, detail: error.message };
427
+ }
428
+ },
429
+ });
430
+ if (!staged.ok) throw new InstallerError(`Cannot stage the packed copy: ${staged.detail}`);
431
+ progress(json, `Local contract: valid (${expectedVersion})`);
432
+
433
+ progress(json, `Marketplace: adding '${STAGE_DIR}'`);
349
434
  const marketplaceAdd = runCodexJson(
350
- ['plugin', 'marketplace', 'add', SO_ROOT, '--json'],
435
+ ['plugin', 'marketplace', 'add', STAGE_DIR, '--json'],
351
436
  'Marketplace add',
352
437
  );
353
438
  assertMarketplaceAddResult(marketplaceAdd);
@@ -399,7 +484,7 @@ function install({ json }) {
399
484
  ok: true,
400
485
  codexVersion: formatVersion(version),
401
486
  marketplace: MARKETPLACE_NAME,
402
- marketplaceSource: SO_ROOT,
487
+ marketplaceSource: STAGE_DIR,
403
488
  marketplaceAlreadyAdded: marketplaceAdd.alreadyAdded,
404
489
  pluginId: PLUGIN_ID,
405
490
  pluginVersion: expectedVersion,
@@ -29,6 +29,7 @@
29
29
 
30
30
  import { existsSync, readFileSync } from 'node:fs';
31
31
  import { dirname, join } from 'node:path';
32
+ import { resolveEventsLedgerRoot } from './lib/platform.mjs';
32
33
  import { appendJsonl } from './lib/common.mjs';
33
34
  import {
34
35
  MEMORY_CLEANUP_EVENT,
@@ -49,6 +50,18 @@ import {
49
50
  } from './lib/session-schema.mjs';
50
51
  import { isMainModule } from './lib/is-main-module.mjs';
51
52
 
53
+ /**
54
+ * The events ledger beside `sessionsFile` — mapped to the main checkout when the
55
+ * metrics dir sits in a LINKED git worktree (#1514 point 1: one ledger per repo,
56
+ * the one the hooks write). Identity everywhere else, including a tmp fixture.
57
+ *
58
+ * @param {string} sessionsFile
59
+ * @returns {string}
60
+ */
61
+ function siblingEventsFile(sessionsFile) {
62
+ return join(resolveEventsLedgerRoot(dirname(sessionsFile)), 'events.jsonl');
63
+ }
64
+
52
65
  export { serializeSessionLineChecked };
53
66
 
54
67
  // A FULL object name only (SHA-1 or SHA-256 repo). A short sha or a symbolic
@@ -314,7 +327,7 @@ async function main() {
314
327
  const alreadyStamped =
315
328
  typeof repaired.memory_cleanup_at === 'string' && repaired.memory_cleanup_at.length > 0;
316
329
  if (!alreadyStamped) {
317
- const eventsFile = join(dirname(args.file), 'events.jsonl');
330
+ const eventsFile = siblingEventsFile(args.file);
318
331
  const signal = deriveMemoryCleanupSignal({
319
332
  eventsFile,
320
333
  sessionId: repaired.session_id,
@@ -455,7 +468,7 @@ async function main() {
455
468
  // without one the fallback cannot run, which is exactly when silence hid it.
456
469
  if (!hasOwn('session_start_ref')) {
457
470
  const headSha = ownUuid !== null
458
- ? readOwnStartHeadSha(join(dirname(args.file), 'events.jsonl'), ownUuid)
471
+ ? readOwnStartHeadSha(siblingEventsFile(args.file), ownUuid)
459
472
  : null;
460
473
  if (headSha !== null) {
461
474
  repaired = { ...repaired, session_start_ref: headSha };
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fleet-auflagen.mjs — read this session's navigator conditions ("Auflagen")
4
+ * and accept them only when they belong to this repo (#1520, navigator#190).
5
+ *
6
+ * Thin CLI over `readAuflagen()` in `scripts/lib/fleet-protocol.mjs`, called by
7
+ * session-start Phase 7.6 after the check-in.
8
+ *
9
+ * Usage:
10
+ * node scripts/fleet-auflagen.mjs <session_id>
11
+ *
12
+ * The own repo is `CLAUDE_PROJECT_DIR` (trimmed) or the working directory.
13
+ *
14
+ * stdout: one JSON line — `{ state: 'absent' }`, `{ state: 'accepted', auflagen }`
15
+ * or `{ state: 'rejected', reason }`. A rejected file counts as absent: the
16
+ * Standard-Auflagen apply. On rejection a WARN line goes to stderr.
17
+ *
18
+ * Exit codes:
19
+ * 0 — any of the three states (rejection is a result, not an error)
20
+ * 2 — usage error (no session id)
21
+ */
22
+
23
+ import { readAuflagen } from './lib/fleet-protocol.mjs';
24
+ import { isMainModule } from './lib/is-main-module.mjs';
25
+
26
+ const PREFIX = 'fleet-auflagen:';
27
+
28
+ /** @returns {Promise<number>} exit code */
29
+ async function main() {
30
+ const sessionId = process.argv[2];
31
+ if (!sessionId) {
32
+ process.stderr.write(`${PREFIX} usage: fleet-auflagen.mjs <session_id>\n`);
33
+ return 2;
34
+ }
35
+ const repoRoot = (process.env.CLAUDE_PROJECT_DIR || '').trim() || process.cwd();
36
+ const result = await readAuflagen(sessionId, { repoRoot });
37
+ if (result.state === 'rejected') {
38
+ process.stderr.write(
39
+ `${PREFIX} WARN: auflagen file ignored (${result.reason}) — treated as absent, Standard-Auflagen apply\n`,
40
+ );
41
+ }
42
+ process.stdout.write(JSON.stringify(result) + '\n');
43
+ return 0;
44
+ }
45
+
46
+ if (isMainModule(import.meta.url)) {
47
+ main().then(
48
+ (code) => { process.exitCode = code; },
49
+ (err) => {
50
+ process.stderr.write(`${PREFIX} ${err instanceof Error ? err.message : String(err)}\n`);
51
+ process.exitCode = 1;
52
+ },
53
+ );
54
+ }
@@ -54,7 +54,8 @@ import path from 'node:path';
54
54
  import { fileURLToPath } from 'node:url';
55
55
  import { execFileSync } from 'node:child_process';
56
56
  import yaml from 'js-yaml';
57
- import { isUserInvocableValue } from './lib/user-invocable-skills.mjs';
57
+ import { isUserInvocableSkill } from './lib/user-invocable-skills.mjs';
58
+ import { listCommandFiles, listSkillDirs } from './lib/plugin-surface.mjs';
58
59
 
59
60
  const POLICY_MARKER = '# Generated by scripts/generate-agents-skills.mjs; do not edit.\n';
60
61
 
@@ -185,7 +186,9 @@ export function renderMirror(skillName, portableFm, canonical = `skills/${skillN
185
186
  '',
186
187
  'Read the linked document in full and follow its complete workflow, including prechecks and stop conditions. Resolve its link relative to this SKILL.md, not the project working directory. The plugin root is three directories above this file. Resolve package paths such as `skills/` and `scripts/` from that root and relative links in the canonical document from its own directory. Keep the user’s project as the target of project operations.',
187
188
  ];
188
- if (command || isUserInvocableValue(portableFm.metadata?.['user-invocable'])) lines.push(
189
+ // The source `user-invocable` travels as a string in `metadata`; absent there
190
+ // means absent in the source, which Claude Code reads as invocable (#1515).
191
+ if (command || isUserInvocableSkill(portableFm.metadata ?? {})) lines.push(
189
192
  '',
190
193
  '`$ARGUMENTS` means the trailing user input after the selected command skill, or an empty string when absent. Preserve flags, quoted text, and Unicode as data. Do not perform global substitution in the command document, shell expansion on the argument string, or execution of that string as shell code. Pass command arguments through structured tool parameters or safely quoted individual arguments.',
191
194
  );
@@ -211,24 +214,14 @@ function readFrontmatter(absPath) {
211
214
 
212
215
  /** List `skills/<name>` directories that carry a SKILL.md, sorted. */
213
216
  export function listSourceSkills(pluginRoot) {
214
- const dir = path.join(pluginRoot, 'skills');
215
- if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
216
- return readdirSync(dir, { withFileTypes: true })
217
- .filter((e) => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_'))
218
- .map((e) => e.name)
219
- .filter((n) => existsSync(path.join(dir, n, 'SKILL.md')))
220
- .sort();
217
+ return listSkillDirs(pluginRoot);
221
218
  }
222
219
 
223
220
  /** Union of canonical skills and commands, preserving command precedence. */
224
221
  function listSourceEntries(pluginRoot) {
225
222
  const sources = new Map(listSourceSkills(pluginRoot).map((name) => [name, `skills/${name}/SKILL.md`]));
226
- const commands = path.join(pluginRoot, 'commands');
227
- if (existsSync(commands) && statSync(commands).isDirectory()) {
228
- for (const entry of readdirSync(commands, { withFileTypes: true })) {
229
- if (!entry.isFile() || !entry.name.endsWith('.md') || /^[._]/.test(entry.name)) continue;
230
- sources.set(entry.name.slice(0, -3), `commands/${entry.name}`);
231
- }
223
+ for (const file of listCommandFiles(pluginRoot)) {
224
+ sources.set(file.slice(0, -3), `commands/${file}`);
232
225
  }
233
226
  return sources;
234
227
  }
@@ -250,7 +243,7 @@ function invocationPolicy(pluginRoot, name, canonical, fm) {
250
243
  return native.policy.allow_implicit_invocation;
251
244
  }
252
245
  }
253
- if (!isUserInvocableValue(fm['user-invocable'], canonical)) return undefined;
246
+ if (!isUserInvocableSkill(fm, canonical)) return undefined;
254
247
  }
255
248
  if (Object.hasOwn(fm, 'disable-model-invocation') && typeof fm['disable-model-invocation'] !== 'boolean') {
256
249
  throw new Error(`${canonical}: disable-model-invocation must be a boolean`);