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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +3 -2
- package/.codex-plugin/skills/architecture/SKILL.md +20 -0
- package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
- package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
- package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
- package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
- package/.codex-plugin/skills/close/SKILL.md +21 -0
- package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
- package/.codex-plugin/skills/debug/SKILL.md +21 -0
- package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/discovery/SKILL.md +21 -0
- package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
- package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
- package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
- package/.codex-plugin/skills/eli5/SKILL.md +21 -0
- package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/eval/SKILL.md +21 -0
- package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/evolve/SKILL.md +21 -0
- package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
- package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
- package/.codex-plugin/skills/go/SKILL.md +22 -0
- package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/grill/SKILL.md +21 -0
- package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
- package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
- package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
- package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
- package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
- package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/plan/SKILL.md +22 -0
- package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
- package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
- package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
- package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
- package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/release/SKILL.md +22 -0
- package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
- package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
- package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/session/SKILL.md +21 -0
- package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/session-end/SKILL.md +22 -0
- package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
- package/.codex-plugin/skills/session-start/SKILL.md +22 -0
- package/.codex-plugin/skills/spinout/SKILL.md +21 -0
- package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
- package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
- package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test/SKILL.md +21 -0
- package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
- package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
- package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
- package/{plugin.json → .cursor-plugin/plugin.json} +5 -2
- package/CHANGELOG.md +190 -1
- package/README.md +26 -18
- package/docs/codex-setup.md +43 -9
- package/docs/components.md +3 -2
- package/docs/instruction-delivery.md +12 -5
- package/docs/migration-v4.md +33 -9
- package/hooks/_lib/hook-import-set.json +4 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-stop.mjs +25 -4
- package/package.json +2 -2
- package/scripts/generate-codex-skills.mjs +246 -0
- package/scripts/generate-hook-import-set.mjs +51 -8
- package/scripts/lib/codex/plugin-contract.mjs +6 -0
- package/scripts/lib/config/host-paths.mjs +20 -4
- package/scripts/lib/gates/gate-full.mjs +7 -3
- package/scripts/lib/owner-config-banner.mjs +7 -9
- package/scripts/lib/owner-yaml.mjs +8 -1
- package/scripts/lib/plugin-update-banner.mjs +10 -2
- package/scripts/lib/reconcile/engine.mjs +38 -7
- package/scripts/lib/session-schema/constants.mjs +38 -11
- package/scripts/lib/session-start-probes.mjs +12 -0
- package/scripts/lib/telemetry/schema.mjs +39 -18
- package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
- package/scripts/lib/validate/check-codex-skills.mjs +191 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +91 -55
- package/scripts/lib/validate/check-skill-links.mjs +37 -7
- package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
- package/scripts/lib/validate/check-unwired-features.mjs +163 -13
- package/scripts/lib/validate/confidential-names.mjs +95 -30
- package/scripts/lib/validate/repo-files.mjs +48 -14
- package/scripts/release.mjs +109 -18
- package/scripts/site-numbers.mjs +344 -8
- package/scripts/validate-plugin.mjs +3 -0
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +5 -0
- package/skills/vault-sync/SKILL.md +10 -0
package/docs/components.md
CHANGED
|
@@ -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`)
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
`.
|
|
694
|
-
|
|
695
|
-
|
|
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
|
|
package/docs/migration-v4.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
149
|
-
|
|
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.
|
|
321
|
-
`
|
|
322
|
-
(`scripts/cursor-install.mjs:69-73`; the `hooks.json` writer at `:139-140`) —
|
|
323
|
-
the
|
|
324
|
-
|
|
325
|
-
|
|
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-
|
|
3
|
-
"head": "
|
|
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
|
{
|
package/hooks/hooks-codex.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "echo '🎯 Session Orchestrator v4.0.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
-
|
|
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
|
|
76
|
-
//
|
|
77
|
-
//
|
|
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
|
: [];
|