session-orchestrator 4.0.0 → 4.0.1

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 +3 -2
  4. package/.codex-plugin/skills/architecture/SKILL.md +20 -0
  5. package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
  6. package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
  7. package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
  8. package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
  9. package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
  10. package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
  11. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
  12. package/.codex-plugin/skills/close/SKILL.md +21 -0
  13. package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
  14. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
  15. package/.codex-plugin/skills/debug/SKILL.md +21 -0
  16. package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
  17. package/.codex-plugin/skills/discovery/SKILL.md +21 -0
  18. package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
  19. package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
  20. package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
  21. package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
  22. package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
  23. package/.codex-plugin/skills/eli5/SKILL.md +21 -0
  24. package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
  25. package/.codex-plugin/skills/eval/SKILL.md +21 -0
  26. package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
  27. package/.codex-plugin/skills/evolve/SKILL.md +21 -0
  28. package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
  29. package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
  30. package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
  31. package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
  32. package/.codex-plugin/skills/go/SKILL.md +22 -0
  33. package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
  34. package/.codex-plugin/skills/grill/SKILL.md +21 -0
  35. package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
  36. package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
  37. package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
  38. package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
  39. package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
  40. package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
  41. package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
  42. package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
  43. package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
  44. package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
  45. package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
  46. package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
  47. package/.codex-plugin/skills/plan/SKILL.md +22 -0
  48. package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
  49. package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
  50. package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
  51. package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
  52. package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
  53. package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
  54. package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
  55. package/.codex-plugin/skills/release/SKILL.md +22 -0
  56. package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
  57. package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
  58. package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
  59. package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
  60. package/.codex-plugin/skills/session/SKILL.md +21 -0
  61. package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
  62. package/.codex-plugin/skills/session-end/SKILL.md +22 -0
  63. package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
  64. package/.codex-plugin/skills/session-start/SKILL.md +22 -0
  65. package/.codex-plugin/skills/spinout/SKILL.md +21 -0
  66. package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
  67. package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
  68. package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
  69. package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
  70. package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
  71. package/.codex-plugin/skills/test/SKILL.md +21 -0
  72. package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
  73. package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
  74. package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
  75. package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
  76. package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
  77. package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
  78. package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
  79. package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
  80. package/{plugin.json → .cursor-plugin/plugin.json} +5 -2
  81. package/CHANGELOG.md +190 -1
  82. package/README.md +26 -18
  83. package/docs/codex-setup.md +43 -9
  84. package/docs/components.md +3 -2
  85. package/docs/instruction-delivery.md +12 -5
  86. package/docs/migration-v4.md +33 -9
  87. package/hooks/_lib/hook-import-set.json +4 -3
  88. package/hooks/hooks-codex.json +1 -1
  89. package/hooks/hooks.json +1 -1
  90. package/hooks/on-stop.mjs +25 -4
  91. package/package.json +2 -2
  92. package/scripts/generate-codex-skills.mjs +246 -0
  93. package/scripts/generate-hook-import-set.mjs +51 -8
  94. package/scripts/lib/codex/plugin-contract.mjs +6 -0
  95. package/scripts/lib/config/host-paths.mjs +20 -4
  96. package/scripts/lib/gates/gate-full.mjs +7 -3
  97. package/scripts/lib/owner-config-banner.mjs +7 -9
  98. package/scripts/lib/owner-yaml.mjs +8 -1
  99. package/scripts/lib/plugin-update-banner.mjs +10 -2
  100. package/scripts/lib/reconcile/engine.mjs +38 -7
  101. package/scripts/lib/session-schema/constants.mjs +38 -11
  102. package/scripts/lib/session-start-probes.mjs +12 -0
  103. package/scripts/lib/telemetry/schema.mjs +39 -18
  104. package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
  105. package/scripts/lib/validate/check-codex-skills.mjs +191 -0
  106. package/scripts/lib/validate/check-owner-leakage.mjs +91 -55
  107. package/scripts/lib/validate/check-skill-links.mjs +37 -7
  108. package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
  109. package/scripts/lib/validate/check-unwired-features.mjs +163 -13
  110. package/scripts/lib/validate/confidential-names.mjs +95 -30
  111. package/scripts/lib/validate/repo-files.mjs +48 -14
  112. package/scripts/release.mjs +109 -18
  113. package/scripts/site-numbers.mjs +344 -8
  114. package/scripts/validate-plugin.mjs +3 -0
  115. package/skills/session-start/SKILL.md +2 -2
  116. package/skills/session-start/references/phase-4-ssot-environment-check.md +5 -0
  117. package/skills/vault-sync/SKILL.md +10 -0
@@ -52,9 +52,10 @@ Codex uses the curated six-event project subset `SessionStart`, `PreToolUse`, `P
52
52
 
53
53
  - **Output Styles (3):** `session-report`, `wave-summary`, `finding-report`.
54
54
  - **Policy & rules:** `.orchestrator/policy/blocked-commands.json` (destructive-command rules); `.claude/rules/parallel-sessions.md` (PSA-001..PSA-004).
55
- - **Codex:** `.codex-plugin/plugin.json` (tracked `+codex.<UTC timestamp>` version), compatibility config, agent role definitions, and the public marketplace/add/list lifecycle implemented by `scripts/codex-install.mjs`. Every run refreshes via `plugin add`; hook trust remains an operator decision in a fresh task through `/hooks`.
55
+ - **Codex:** `.codex-plugin/plugin.json` (tracked `+codex.<UTC timestamp>` version), generated command and skill entrypoints under `.codex-plugin/skills/`, compatibility config, agent role definitions, and the public marketplace/add/list lifecycle implemented by `scripts/codex-install.mjs`. `scripts/generate-codex-skills.mjs` produces the name union with commands taking precedence; [Codex usage](codex-setup.md#usage) explains selection. Every installer run refreshes via `plugin add`; hook trust remains an operator decision in a fresh task through `/hooks`.
56
+ - **Cursor:** `.cursor-plugin/plugin.json` registers canonical skills and `.mcp.json` using Cursor's native manifest format. Additional native component discovery is explicitly disabled; `scripts/cursor-install.mjs` supplies the existing command and hook adapters. The former standard root manifest was moved to prevent [Codex manifest interception](codex-setup.md#manifest-compatibility).
56
57
  - **Pi:** `package.json` `pi` manifest, `pi/extensions/session-orchestrator.ts` bridge, `hooks/hooks-pi.json`, `scripts/pi-install.mjs`.
57
- - **Portable cross-harness surface (generated, never hand-edited):** root `AGENTS.md` (byte-identical copy of `CLAUDE.md`), root `plugin.json` ([agent-plugins.org](https://agent-plugins.org) 1.0.0 schema), and `.agents/skills/<name>/SKILL.md` — a mirror of all 43 skills carrying only spec-legal frontmatter plus a pointer body (progressive disclosure; the mirror never duplicates the canonical instructions). Written by `scripts/generate-agents-skills.mjs`, drift-checked via its `--check` form inside `scripts/validate-plugin.mjs`.
58
+ - **Portable cross-harness surface (generated, never hand-edited):** root `AGENTS.md` (byte-identical copy of `CLAUDE.md`) and `.agents/skills/<name>/SKILL.md` — mirrors carrying only spec-legal frontmatter plus a pointer body. These two surfaces are written by `scripts/generate-agents-skills.mjs` and drift-checked via its `--check` form inside `scripts/validate-plugin.mjs`. Native plugin manifests are maintained separately, with versions updated by `scripts/release.mjs`.
58
59
  - **Scripts:** deterministic CLI tools (parse-config, run-quality-gate, validate-wave-scope, validate-plugin, token-audit, autopilot) plus shared lib under `scripts/lib/*.mjs`, all covered by the vitest suite.
59
60
 
60
61
  ## `/harness-audit` — Anthropic large-codebase rubric
@@ -688,11 +688,18 @@ agentskills.io permits outside Claude Code (`name`, `description`, `license`,
688
688
  `skills/<name>/SKILL.md`, never a copy of it — progressive disclosure keeps the
689
689
  whole mirror at 47,380 bytes across 43 files.
690
690
 
691
- Root `plugin.json` (agent-plugins.org 1.0.0) is the third artefact of the same
692
- surface. It is the **third** manifest carrying a version, alongside
693
- `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`, while the release
694
- script bumps only `package.json` `scripts/lib/validate/check-agents-skills.mjs`
695
- asserts all of them agree.
691
+ These are the two artifacts emitted by `scripts/generate-agents-skills.mjs`. Plugin
692
+ manifests are maintained separately under `.claude-plugin/`, `.codex-plugin/` and
693
+ `.cursor-plugin/`; `scripts/release.mjs` updates their versions with `package.json`.
694
+ The initial 4.0.0 standard root `plugin.json` was moved to the native Cursor location
695
+ because it intercepted Codex's skill path and version selection; see
696
+ [Codex manifest compatibility](codex-setup.md#manifest-compatibility).
697
+
698
+ Codex has a separate generated surface: `scripts/generate-codex-skills.mjs` writes
699
+ `.codex-plugin/skills/` from the union of canonical command and skill names. Commands
700
+ win same-name overlaps, and each adapter reads the canonical document before dispatch.
701
+ The portable `.agents/skills/` mirrors continue to reference canonical skills directly;
702
+ they do not replace the installed Codex command entrypoints.
696
703
 
697
704
  ## Learnings Index (selected for your file scope)
698
705
 
@@ -24,7 +24,7 @@ change list: [CHANGELOG.md](../CHANGELOG.md).
24
24
  | `.claude/rules/` | 61 files (43 generated) | 26 files (43 generated → 8 thematic) |
25
25
  | Turn-stop event | `orchestrator.session.stopped` | `orchestrator.turn.stopped` (both emitted until 2027-03-06) |
26
26
  | Telemetry field | `fleet` | `fleet_self_declared` (both sent until 2027-03-06) |
27
- | Cross-harness manifest | none | root `AGENTS.md` + root `plugin.json` + `.agents/skills/` |
27
+ | Cross-harness integration | native manifests | generated root `AGENTS.md` + `.agents/skills/`; separate Claude, Codex and Cursor manifests |
28
28
  | Session shapes | `housekeeping` / `feature` / `deep` | unchanged, plus the `ultradeep` PROFILE over `deep` |
29
29
  | Runtime | Node 24+, npm | unchanged |
30
30
 
@@ -126,6 +126,17 @@ New in 4.0.0 and relevant here: this repository now ships a **root `AGENTS.md`**
126
126
  instructions from `AGENTS.md` found nothing in this repo. If you keep your own
127
127
  `AGENTS.md`, nothing changes for you.
128
128
 
129
+ The Codex command integration uses generated skills: search for `go` or `close` in the
130
+ picker and select the **Session Orchestrator** entry, or invoke `$session-orchestrator:go` and
131
+ `$session-orchestrator:close`. Refresh the installed plugin as above and restart Codex if
132
+ the new entries do not appear. The native `/goal` command is separate.
133
+
134
+ The initial 4.0.0 root Agent Plugins `plugin.json` has been replaced by a native Cursor
135
+ manifest. Read-only probes on Codex 0.153.3 and desktop runtime 0.153.4 showed that the
136
+ standard root manifest overrides Codex's declared skill directory and cache version.
137
+ The separate native manifests avoid that conflict; details are in
138
+ [Codex manifest compatibility](codex-setup.md#manifest-compatibility).
139
+
129
140
  ### 3c. Cursor IDE
130
141
 
131
142
  ```bash
@@ -145,8 +156,12 @@ But the three retired commands do **not** disappear from `.cursor/commands/` on
145
156
  `hooks.json` written before 4.0.0 is never synchronised with a new hook event automatically.
146
157
  Both need the manual step in § 5 ("Cursor still shows the removed commands") below.
147
158
 
148
- A root `plugin.json` following the [agent-plugins.org](https://agent-plugins.org) 1.0.0
149
- schema now ships as well, for Cursor's plugin system.
159
+ Cursor's plugin metadata now lives at `.cursor-plugin/plugin.json`, replacing the initial
160
+ 4.0.0 standard root `plugin.json`. The native manifest retains the canonical skills and
161
+ MCP paths and explicitly disables additional component discovery; the installer above
162
+ continues to supply commands and hooks. The shape follows the
163
+ [official Cursor manifest reference](https://cursor.com/docs/reference/plugins); native
164
+ Cursor loading was not runtime-tested during this compatibility repair.
150
165
 
151
166
  ### 3d. Pi
152
167
 
@@ -260,6 +275,11 @@ for f in autopilot-multi contract-version-bump journey-audit; do
260
275
  done
261
276
  ```
262
277
 
278
+ `[ -L "$p" ]` checks the destination's TYPE, not who created it — it removes any symlink at that
279
+ path regardless of origin. If you separately symlinked your own command under one of these three
280
+ names, this recipe deletes that symlink too; a regular (non-symlink) file is the only thing it
281
+ leaves untouched.
282
+
263
283
  Then restart Cursor. Same story for `.cursor/hooks.json`: the installer skips it outright once
264
284
  it exists (`:139-140`), so a `hooks.json` written before 4.0.0 is never re-synced with a new hook
265
285
  event automatically — re-check it by hand (diff it against a fresh `node scripts/cursor-install.mjs`
@@ -317,12 +337,16 @@ What you get back, and what you do not:
317
337
  - **The removed skills, commands and scripts come back with the checkout.** They were deleted
318
338
  from the repository, not from your disk history.
319
339
  - **What does NOT roll back automatically** is anything an installer wrote into YOUR project:
320
- `.cursor/commands/`, `.cursor/hooks.json`, Pi settings. Both `cursor-install.mjs` and
321
- `pi-install.mjs` skip any destination that already exists — a symlink or a file
322
- (`scripts/cursor-install.mjs:69-73`; the `hooks.json` writer at `:139-140`) — so re-running
323
- the installer from the 3.24.0 checkout only **adds** files missing from your project. It does
324
- not restore a symlink you removed yourself, and it does not resync an existing `hooks.json` or
325
- Pi settings file. To get those back: remove the stale file first (§ "Cursor still shows the
340
+ `.cursor/commands/`, `.cursor/hooks.json`, Pi settings. The two installers behave
341
+ differently: `cursor-install.mjs` skips any destination that already exists — a symlink or a
342
+ file (`scripts/cursor-install.mjs:69-73`; the `hooks.json` writer at `:139-140`) — and
343
+ re-creates only the ones that are MISSING, so re-running it from the 3.24.0 checkout only
344
+ **adds** command links missing from your project; it does not restore a link you removed
345
+ yourself and does not resync an existing `hooks.json`.
346
+ `pi-install.mjs --settings-only` **does** rewrite the Pi settings file: it reads it, upserts
347
+ this package's entry and writes it back (`scripts/pi-install.mjs` `upsertPackage` /
348
+ `writeSettings`), so re-running it from the 3.24.0 checkout re-points Pi at that checkout.
349
+ For Cursor, to get a removed link back: delete the stale file first (§ "Cursor still shows the
326
350
  removed commands" has a safe, symlink-only removal recipe), then re-run the installer with
327
351
  your project path, as shown above.
328
352
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "generated_at": "2026-09-06T19:04:31.959Z",
3
- "head": "bc49301b3eed05b5b235376ecec4a931522d26bf",
2
+ "generated_at": "2026-09-07T18:36:32.142Z",
3
+ "head": "a244b8707aa5bb650673fce2e6cf548564b43e58",
4
4
  "entries": [
5
5
  {
6
6
  "file": "hooks/_lib/atomic-json.mjs",
@@ -1274,7 +1274,8 @@
1274
1274
  "file": "scripts/lib/session-schema/constants.mjs",
1275
1275
  "reachable_from": [
1276
1276
  "on-session-end.mjs",
1277
- "on-session-start.mjs"
1277
+ "on-session-start.mjs",
1278
+ "skill-invocation-telemetry.mjs"
1278
1279
  ]
1279
1280
  },
1280
1281
  {
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "echo '🎯 Session Orchestrator v4.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
+ "command": "echo '🎯 Session Orchestrator v4.0.1 — /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 v4.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
9
+ "command": "echo '🎯 Session Orchestrator v4.0.1 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
10
10
  "async": false
11
11
  },
12
12
  {
package/hooks/on-stop.mjs CHANGED
@@ -48,7 +48,7 @@ import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
48
48
  if (!shouldRunHook('on-stop')) process.exit(0);
49
49
 
50
50
  import { emitEvent } from '../scripts/lib/events.mjs';
51
- import { getProjectDir } from '../scripts/lib/platform.mjs';
51
+ import { detectPlatform, getProjectDir } from '../scripts/lib/platform.mjs';
52
52
  import { parseSessionId } from '../scripts/lib/session-id.mjs';
53
53
  import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
54
54
  import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
@@ -772,7 +772,7 @@ async function main() {
772
772
  await handleStop(input);
773
773
  } finally {
774
774
  // terminalSequence is only meaningful for Stop (session-level) events.
775
- process.stdout.write(buildTerminalSequenceJson());
775
+ process.stdout.write(buildTerminalSequenceJson(detectPlatform()));
776
776
  }
777
777
  }
778
778
  }
@@ -781,15 +781,36 @@ async function main() {
781
781
  // terminal notification
782
782
  // ---------------------------------------------------------------------------
783
783
 
784
+ /**
785
+ * Display label per harness, keyed exactly like PLATFORM_UPDATE_INSTRUCTIONS in
786
+ * `scripts/lib/plugin-update-banner.mjs`.
787
+ * @type {Record<string, string>}
788
+ */
789
+ const PLATFORM_NOTIFY_LABELS = {
790
+ claude: 'Claude Code',
791
+ codex: 'Codex',
792
+ cursor: 'Cursor',
793
+ pi: 'Pi',
794
+ };
795
+
784
796
  /**
785
797
  * Emit a cross-platform desktop notification via the CC 2.1.141+ terminalSequence
786
798
  * output field. Supports OSC 9 (iTerm2, Windows Terminal, WezTerm, ConEmu) and
787
799
  * OSC 777 (Ghostty, urxvt, Warp). Both sequences are emitted together; unsupported
788
800
  * terminals silently ignore. Returns the JSON string to write to stdout.
801
+ * Pure: the caller resolves the platform, so this stays unit-testable.
802
+ * @param {string} [platform] - resolved harness key ('claude'|'codex'|'cursor'|'pi')
789
803
  * @returns {string}
790
804
  */
791
- function buildTerminalSequenceJson() {
792
- const title = 'Claude Code';
805
+ function buildTerminalSequenceJson(platform) {
806
+ // #1254: the notification LABEL follows the harness the session runs on.
807
+ // This is NOT in tension with the "Do NOT add a platform-detecting deregister
808
+ // branch here" note above — that note is scoped to DEREGISTRATION (the
809
+ // two-teardown-paths shape Epic #583 removed); a display label has no
810
+ // teardown semantics at all. Keys mirror PLATFORM_UPDATE_INSTRUCTIONS in
811
+ // scripts/lib/plugin-update-banner.mjs; unknown/undefined falls back to
812
+ // 'Claude Code' (the same signal-free default detectPlatform() returns).
813
+ const title = PLATFORM_NOTIFY_LABELS[platform] ?? 'Claude Code';
793
814
  const body = 'Session stopped — your turn';
794
815
  const osc9 = `\x1b]9;${title}: ${body}\x07`;
795
816
  const osc777 = `\x1b]777;notify;${title};${body}\x07`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "session-orchestrator",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
5
5
  "type": "module",
6
6
  "homepage": "https://session-orchestrator.com",
@@ -25,7 +25,6 @@
25
25
  ".mcp.json",
26
26
  "AGENTS.md",
27
27
  "CHANGELOG.md",
28
- "plugin.json",
29
28
  "pi/",
30
29
  "skills/",
31
30
  "agents/",
@@ -39,6 +38,7 @@
39
38
  "assets/",
40
39
  ".claude-plugin/",
41
40
  ".codex-plugin/",
41
+ ".cursor-plugin/",
42
42
  ".agents/",
43
43
  ".cursor/",
44
44
  ".orchestrator/policy/",
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate the Codex discovery surface from commands/ and skills/.
4
+ * Commands own overlapping public names; canonical workflow bodies stay in place.
5
+ * Usage: node scripts/generate-codex-skills.mjs [--plugin-root <dir>] [--check] [--json]
6
+ */
7
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import yaml from 'js-yaml';
11
+ import { toPortableFrontmatter } from './generate-agents-skills.mjs';
12
+
13
+ const OUTPUT = '.codex-plugin/skills';
14
+ const MARKER = 'Generated by scripts/generate-codex-skills.mjs; do not edit.';
15
+
16
+ /** @typedef {{name: string, path: string, command: boolean, fm: Record<string, unknown>, policy?: boolean}} Source */
17
+
18
+ const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
19
+
20
+ /** @param {unknown} fm @param {string} name @param {string} path */
21
+ function validateFrontmatter(fm, name, path) {
22
+ const fail = (message) => { throw new Error(`${path}: ${message}`); };
23
+ if (!isRecord(fm)) fail('YAML frontmatter must be a mapping');
24
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64) fail('name must be kebab-case, at most 64 characters');
25
+ if (Object.hasOwn(fm, 'name') && fm.name !== name) fail('name must match its file or directory name');
26
+ if (typeof fm.description !== 'string' || !fm.description.trim() || fm.description.trim().length > 1024) {
27
+ fail('description must be a non-empty string of at most 1024 characters');
28
+ }
29
+ for (const key of ['disable-model-invocation', 'user-invocable']) {
30
+ if (Object.hasOwn(fm, key) && typeof fm[key] !== 'boolean') fail(`${key} must be a boolean`);
31
+ }
32
+ for (const key of ['argument-hint', 'license', 'compatibility']) {
33
+ if (Object.hasOwn(fm, key) && typeof fm[key] !== 'string') fail(`${key} must be a string`);
34
+ }
35
+ if (Object.hasOwn(fm, 'metadata') && !isRecord(fm.metadata)) fail('metadata must be a mapping');
36
+ for (const key of ['allowed-tools', 'tools']) {
37
+ if (!Object.hasOwn(fm, key)) continue;
38
+ const value = fm[key];
39
+ if (!(typeof value === 'string' || (Array.isArray(value) && value.every((item) => typeof item === 'string' && item.trim())))) {
40
+ fail(`${key} must be a string or an array of non-empty strings`);
41
+ }
42
+ }
43
+ }
44
+
45
+ /** Existing native policy is meaningful; Claude's user-invocable flag is not a substitute. */
46
+ function readNativePolicy(root, name) {
47
+ const path = `skills/${name}/agents/openai.yaml`;
48
+ if (!existsSync(join(root, path))) return undefined;
49
+ let native;
50
+ try { native = yaml.load(readFileSync(join(root, path), 'utf8'), { schema: yaml.CORE_SCHEMA }); }
51
+ catch (error) { throw new Error(`${path}: invalid YAML: ${error.message}`, { cause: error }); }
52
+ if (!isRecord(native) || (Object.hasOwn(native, 'policy') && !isRecord(native.policy))) {
53
+ throw new Error(`${path}: native skill metadata and policy must be mappings`);
54
+ }
55
+ if (!native.policy || !Object.hasOwn(native.policy, 'allow_implicit_invocation')) return undefined;
56
+ if (typeof native.policy.allow_implicit_invocation !== 'boolean') {
57
+ throw new Error(`${path}: policy.allow_implicit_invocation must be a boolean`);
58
+ }
59
+ return native.policy.allow_implicit_invocation;
60
+ }
61
+
62
+ /** @param {string} root @returns {Source[]} */
63
+ function readSources(root) {
64
+ /** @type {Source[]} */
65
+ const sources = [];
66
+ for (const directory of ['skills', 'commands']) {
67
+ const directoryPath = join(root, directory);
68
+ if (!existsSync(directoryPath)) continue;
69
+ for (const entry of readdirSync(directoryPath, { withFileTypes: true })) {
70
+ if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
71
+ const command = directory === 'commands';
72
+ if (command ? !entry.isFile() || !entry.name.endsWith('.md') : !entry.isDirectory()) continue;
73
+ const name = command ? entry.name.slice(0, -3) : entry.name;
74
+ const path = command ? `commands/${name}.md` : `skills/${name}/SKILL.md`;
75
+ if (!existsSync(join(root, path))) continue;
76
+ const raw = readFileSync(join(root, path), 'utf8');
77
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
78
+ if (!match) throw new Error(`${path}: missing YAML frontmatter`);
79
+ let fm;
80
+ try { fm = yaml.load(match[1], { schema: yaml.CORE_SCHEMA }); }
81
+ catch (error) { throw new Error(`${path}: invalid YAML: ${error.message}`, { cause: error }); }
82
+ validateFrontmatter(fm, name, path);
83
+ sources.push({ name, path, command, fm, policy: command ? fm['disable-model-invocation'] !== true : readNativePolicy(root, name) });
84
+ }
85
+ }
86
+ return sources;
87
+ }
88
+
89
+ /** Only these two artifact shapes and their exact ownership marker belong to this generator. */
90
+ function isOwned(path, raw) {
91
+ if (/^\.codex-plugin\/skills\/[^/]+\/SKILL\.md$/.test(path)) return raw.includes(`\n<!-- ${MARKER} -->\n`);
92
+ return /^\.codex-plugin\/skills\/[^/]+\/agents\/openai\.yaml$/.test(path) && raw.startsWith(`# ${MARKER}\n`);
93
+ }
94
+
95
+ /** Read the tree without following symlinks, before any updates are permitted. */
96
+ function readOutput(root, directory = OUTPUT) {
97
+ const absolute = join(root, directory);
98
+ if (!existsSync(absolute)) return [];
99
+ if (!lstatSync(absolute).isDirectory() || lstatSync(absolute).isSymbolicLink()) {
100
+ throw new Error(`${directory}: generated directory must be a real directory`);
101
+ }
102
+ const files = [];
103
+ for (const entry of readdirSync(absolute, { withFileTypes: true })) {
104
+ const path = `${directory}/${entry.name}`;
105
+ if (entry.isSymbolicLink()) throw new Error(`${path}: refusing to follow a symlink in the generated surface`);
106
+ if (entry.isDirectory()) files.push(...readOutput(root, path));
107
+ else if (entry.isFile()) files.push({ path, raw: readFileSync(join(root, path), 'utf8') });
108
+ else throw new Error(`${path}: unsupported file type in generated surface`);
109
+ }
110
+ return files;
111
+ }
112
+
113
+ /** @param {Source} source @param {string[]} warnings */
114
+ function renderSkill(source, warnings) {
115
+ const projected = toPortableFrontmatter({ ...source.fm, name: source.name }, source.name);
116
+ warnings.push(...projected.warnings);
117
+ const lines = [
118
+ '---',
119
+ yaml.dump(projected.fm, { lineWidth: -1, noRefs: true }).trimEnd(),
120
+ '---',
121
+ '',
122
+ `<!-- ${MARKER} -->`,
123
+ '',
124
+ `# ${source.name}`,
125
+ '',
126
+ `Read [\`${source.path}\`](../../../${source.path}) in full before taking any action, then follow its complete workflow.`,
127
+ 'The linked document is authoritative, including its prechecks, argument rules, and stop conditions.',
128
+ '',
129
+ 'Resolve this 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; resolve relative links inside the canonical document from its own directory. Keep the user’s project as the target of project operations.',
130
+ ];
131
+ if (source.command) lines.push(
132
+ '',
133
+ '`$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. When the workflow needs a command, pass its arguments through structured tool parameters or safely quoted individual arguments.',
134
+ '',
135
+ 'Read the full command before invoking an internal skill. An instruction to invoke a skill (including `session-orchestrator:<name>` or the `Skill` tool) means read and follow the canonical `skills/<name>/SKILL.md` beneath the plugin root. Do not redispatch the public command adapter: a same-named command and internal skill are distinct documents, and redispatch would recurse.',
136
+ );
137
+ lines.push('', 'Regenerate with `node scripts/generate-codex-skills.mjs`.', '');
138
+ return lines.join('\n');
139
+ }
140
+
141
+ /**
142
+ * @param {{pluginRoot: string, check?: boolean}} options
143
+ * @returns {{ok: boolean, skills: string[], files: string[], written: string[], drift: string[], warnings: string[], errors: string[]}}
144
+ */
145
+ export function generateCodexSurface({ pluginRoot, check = false }) {
146
+ const result = { ok: true, skills: [], files: [], written: [], drift: [], warnings: [], errors: [] };
147
+ try {
148
+ if (!existsSync(pluginRoot) || !statSync(pluginRoot).isDirectory()) throw new Error(`${pluginRoot}: plugin root must be an existing directory`);
149
+ const sources = new Map(readSources(pluginRoot).map((source) => [source.name, source]));
150
+ result.skills = [...sources.keys()].sort();
151
+ const artifacts = new Map();
152
+ for (const name of result.skills) {
153
+ const source = sources.get(name);
154
+ artifacts.set(`${OUTPUT}/${name}/SKILL.md`, renderSkill(source, result.warnings));
155
+ if (source.policy !== undefined) artifacts.set(`${OUTPUT}/${name}/agents/openai.yaml`, [
156
+ `# ${MARKER}`,
157
+ yaml.dump({
158
+ interface: { display_name: name },
159
+ policy: { allow_implicit_invocation: source.policy },
160
+ }, { lineWidth: -1, noRefs: true }).trimEnd(),
161
+ '',
162
+ ].join('\n'));
163
+ }
164
+ result.files = [...artifacts.keys()];
165
+
166
+ // Preflight every destination and existing artifact before the first mutation.
167
+ // A conflict or invalid source must never leave a partly refreshed surface.
168
+ for (const path of [`${OUTPUT}/.preflight`, ...result.files]) {
169
+ const destination = join(pluginRoot, path);
170
+ if (artifacts.has(path) && existsSync(destination) && !lstatSync(destination).isFile()) {
171
+ throw new Error(`${path}: generated artifact must be a regular file`);
172
+ }
173
+ let parent = dirname(path);
174
+ while (parent !== '.') {
175
+ const absolute = join(pluginRoot, parent);
176
+ if (existsSync(absolute) && (!lstatSync(absolute).isDirectory() || lstatSync(absolute).isSymbolicLink())) {
177
+ throw new Error(`${parent}: generated directory must be a real directory`);
178
+ }
179
+ parent = dirname(parent);
180
+ }
181
+ }
182
+ const obsolete = [];
183
+ for (const { path, raw } of readOutput(pluginRoot)) {
184
+ if (artifacts.has(path)) {
185
+ if (!isOwned(path, raw)) throw new Error(`${path}: refusing to overwrite an unrelated artifact without the generated marker`);
186
+ } else if (isOwned(path, raw)) obsolete.push(path);
187
+ else result.warnings.push(`${path}: retained unrelated file`);
188
+ }
189
+
190
+ for (const [path, content] of artifacts) {
191
+ const absolute = join(pluginRoot, path);
192
+ if (existsSync(absolute) && readFileSync(absolute, 'utf8') === content) continue;
193
+ if (check) result.drift.push(`${path} is missing or stale`);
194
+ else {
195
+ mkdirSync(dirname(absolute), { recursive: true });
196
+ writeFileSync(absolute, content);
197
+ result.written.push(path);
198
+ }
199
+ }
200
+ for (const path of obsolete) {
201
+ if (check) result.drift.push(`${path} is obsolete`);
202
+ else {
203
+ unlinkSync(join(pluginRoot, path));
204
+ result.written.push(`removed ${path}`);
205
+ let parent = dirname(path);
206
+ while (parent !== OUTPUT && readdirSync(join(pluginRoot, parent)).length === 0) {
207
+ rmdirSync(join(pluginRoot, parent));
208
+ parent = dirname(parent);
209
+ }
210
+ }
211
+ }
212
+ } catch (error) {
213
+ result.errors.push(error.message);
214
+ }
215
+ result.ok = result.errors.length === 0 && result.drift.length === 0;
216
+ return result;
217
+ }
218
+
219
+ function runCli() {
220
+ let pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
221
+ let check = false;
222
+ let json = false;
223
+ const args = process.argv.slice(2);
224
+ for (let i = 0; i < args.length; i += 1) {
225
+ if (args[i] === '--check') check = true;
226
+ else if (args[i] === '--json') json = true;
227
+ else if (args[i] === '--plugin-root' && args[i + 1]) pluginRoot = resolve(args[++i]);
228
+ else if (args[i] === '--help') {
229
+ process.stdout.write('Usage: generate-codex-skills.mjs [--plugin-root <dir>] [--check] [--json]\n');
230
+ return;
231
+ } else {
232
+ process.stderr.write(`Unknown or incomplete argument: ${args[i]}\n`);
233
+ process.exitCode = 1;
234
+ return;
235
+ }
236
+ }
237
+ const result = generateCodexSurface({ pluginRoot, check });
238
+ if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
239
+ else {
240
+ process.stdout.write(`codex-surface: ${result.files.length} artefact(s), ${result.ok ? (check ? 'in sync' : 'generated') : 'failed'}\n`);
241
+ for (const message of [...result.errors, ...result.drift, ...result.warnings]) process.stderr.write(`${message}\n`);
242
+ }
243
+ process.exitCode = result.ok ? 0 : 1;
244
+ }
245
+
246
+ if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) runCli();
@@ -17,8 +17,11 @@
17
17
  *
18
18
  * Usage:
19
19
  * node scripts/generate-hook-import-set.mjs [--plugin-root <dir>] [--out <file>] [--check]
20
+ * node scripts/generate-hook-import-set.mjs --help
20
21
  *
21
- * Exit codes: 0 = written / in sync; 1 = drift (with --check) or usage error.
22
+ * Exit codes: 0 = written / in sync (or --help); 1 = drift (with --check);
23
+ * 2 = usage error (unknown flag, or a valued flag missing its value) — the
24
+ * write is NEVER performed on a usage error.
22
25
  */
23
26
 
24
27
  import { readFileSync, writeFileSync, existsSync, statSync, realpathSync } from 'node:fs';
@@ -178,19 +181,59 @@ export function buildArtifact(pluginRoot) {
178
181
  // CLI
179
182
  // ---------------------------------------------------------------------------
180
183
 
184
+ const USAGE = [
185
+ 'Usage: node scripts/generate-hook-import-set.mjs [--plugin-root <dir>] [--out <file>] [--check]',
186
+ '',
187
+ ' --plugin-root <dir> repository/plugin root to crawl (default: the repo this script lives in)',
188
+ ' --out <file> artefact path (default: <plugin-root>/hooks/_lib/hook-import-set.json)',
189
+ ' --check re-crawl and diff against the committed artefact instead of writing',
190
+ ' --help, -h print this help',
191
+ '',
192
+ 'Exit codes: 0 = written / in sync; 1 = drift (with --check); 2 = usage error.',
193
+ ].join('\n');
194
+
181
195
  /**
182
196
  * @param {string[]} argv
183
197
  * @returns {number} process exit code
184
198
  */
185
199
  export function main(argv) {
186
200
  const args = argv.slice(2);
187
- const flag = (name) => {
188
- const i = args.indexOf(name);
189
- return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
190
- };
191
- const pluginRoot = path.resolve(flag('--plugin-root') ?? path.resolve(import.meta.dirname, '..'));
192
- const out = path.resolve(flag('--out') ?? path.join(pluginRoot, 'hooks', '_lib', 'hook-import-set.json'));
193
- const check = args.includes('--check');
201
+
202
+ /** @type {{'--plugin-root': string|null, '--out': string|null}} */
203
+ const valued = { '--plugin-root': null, '--out': null };
204
+ let check = false;
205
+
206
+ for (let i = 0; i < args.length; i += 1) {
207
+ const arg = args[i];
208
+ if (arg === '--help' || arg === '-h') {
209
+ process.stdout.write(`${USAGE}\n`);
210
+ return 0;
211
+ }
212
+ if (arg === '--check') {
213
+ check = true;
214
+ continue;
215
+ }
216
+ if (arg === '--plugin-root' || arg === '--out') {
217
+ // A valued flag whose value is missing — or whose "value" is the next FLAG
218
+ // — is a usage error, not a silent default. Without the leading-dash test,
219
+ // `--out --check` swallows `--check` as the artefact path: checking is
220
+ // disabled, the WRITE branch runs, and a file literally named `--check`
221
+ // lands in the cwd.
222
+ const value = args[i + 1];
223
+ if (value === undefined || value.startsWith('-')) {
224
+ process.stderr.write(`✗ hook-import-set: ${arg} requires a value\n${USAGE}\n`);
225
+ return 2;
226
+ }
227
+ valued[arg] = value;
228
+ i += 1;
229
+ continue;
230
+ }
231
+ process.stderr.write(`✗ hook-import-set: unknown argument "${arg}"\n${USAGE}\n`);
232
+ return 2;
233
+ }
234
+
235
+ const pluginRoot = path.resolve(valued['--plugin-root'] ?? path.resolve(import.meta.dirname, '..'));
236
+ const out = path.resolve(valued['--out'] ?? path.join(pluginRoot, 'hooks', '_lib', 'hook-import-set.json'));
194
237
 
195
238
  const fresh = buildArtifact(pluginRoot);
196
239
 
@@ -7,6 +7,7 @@ const MANIFEST_KEYS = new Set([
7
7
  'description',
8
8
  'keywords',
9
9
  'skills',
10
+ 'commands',
10
11
  'mcpServers',
11
12
  'apps',
12
13
  'hooks',
@@ -206,6 +207,11 @@ function validateManifest(manifest, root, expectedBaseVersion, errors) {
206
207
  validatePathCollection(manifest.skills, '$.manifest.skills', 'directory', root, errors, {
207
208
  required: true,
208
209
  });
210
+ // Native [] suppresses Codex's conventional commands/ fallback. Nonempty
211
+ // paths would register automatic aliases beside our generated skill surface.
212
+ if (Object.hasOwn(manifest, 'commands') && (!Array.isArray(manifest.commands) || manifest.commands.length !== 0)) {
213
+ addError(errors, '$.manifest.commands', 'disabled-command-discovery', 'commands must be an empty array to disable automatic command conversion');
214
+ }
209
215
  validateMcpServers(manifest.mcpServers, root, errors);
210
216
  if (manifest.apps !== undefined) {
211
217
  validateContractPath(manifest.apps, '$.manifest.apps', 'file', root, errors);
@@ -40,17 +40,33 @@ const ENV_KEYS = /** @type {const} */ ({
40
40
  * merged with defaults, so a real owner.yaml without a `paths:` section yields
41
41
  * `ownerConfig.paths === undefined`. Callers must read defensively.
42
42
  *
43
- * @param {{ env?: Record<string, string|undefined>, ownerLoader?: () => { config: object } }} [opts]
44
- * @returns {{ ownerConfig: object|undefined, env: Record<string, string|undefined> }}
43
+ * HEALTH PASSTHROUGH (#1251): the owner loader reports HOW its result came about
44
+ * (`source`, `reason`, `droppedSections`). Discarding those forced callers that
45
+ * need them — CP11 in check-owner-leakage.mjs is the live one — to load owner.yaml
46
+ * a SECOND time just to see the health of the load this function already did. They
47
+ * are passed through verbatim; `ownerConfig` and `env` keep their exact prior
48
+ * meaning, so callers reading only those two (scripts/lib/config.mjs,
49
+ * scripts/lib/vault-mirror/namespace.mjs) are unaffected. On a throwing loader all
50
+ * three health fields stay `undefined`, exactly like `ownerConfig`.
51
+ *
52
+ * @param {{ env?: Record<string, string|undefined>, ownerLoader?: () => { config: object, source?: string, reason?: string, droppedSections?: Array<{section: string, errors: string[]}> } }} [opts]
53
+ * @returns {{ ownerConfig: object|undefined, env: Record<string, string|undefined>, source: string|undefined, reason: string|undefined, droppedSections: Array<{section: string, errors: string[]}>|undefined }}
45
54
  */
46
55
  export function loadHostPaths({ env = process.env, ownerLoader = loadOwnerConfig } = {}) {
47
56
  let ownerConfig;
57
+ let source;
58
+ let reason;
59
+ let droppedSections;
48
60
  try {
49
- ownerConfig = ownerLoader().config;
61
+ const loaded = ownerLoader();
62
+ ownerConfig = loaded?.config;
63
+ source = loaded?.source;
64
+ reason = loaded?.reason;
65
+ droppedSections = loaded?.droppedSections;
50
66
  } catch {
51
67
  ownerConfig = undefined;
52
68
  }
53
- return { ownerConfig, env };
69
+ return { ownerConfig, env, source, reason, droppedSections };
54
70
  }
55
71
 
56
72
  /**
@@ -72,9 +72,13 @@ const testCounts =
72
72
  // no name is what made the 2026-09-06 pre-push block unusable —
73
73
  // `files_failed: 1` out of 662, and reconstructing WHICH file cost a manual
74
74
  // re-materialisation of the tracked tree. An EMPTY array here is meaningful and
75
- // is NOT the absent case: it says the file-level summary was parsed and no path
76
- // could be read out of it (a non-vitest reporter, a truncated capture), which
77
- // is a parser gap worth seeing the unmeasured case is the absent key.
75
+ // is NOT the absent case: it means vitest reported file-level failures but the
76
+ // file-name parser (`extractFailedTestFiles`) matched none of them out of the
77
+ // captured output — a parser gap worth seeing. The consumer,
78
+ // `failedFilesFromGateStdout()` in `scripts/run-quality-gate.mjs`, normalizes
79
+ // this `[]` to `null` (same as the true-absent case) before it reaches its own
80
+ // caller — so this array's "parsed but empty" distinction is visible here, in
81
+ // this envelope, and nowhere downstream.
78
82
  const failedTestFiles = testResult.status === 'fail'
79
83
  ? extractFailedTestFiles(testResult.fullOutput ?? testResult.output ?? '')
80
84
  : [];