session-orchestrator 5.1.0 → 5.2.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.
- package/.agents/skills/autopilot/SKILL.md +1 -0
- package/.agents/skills/bootstrap/SKILL.md +2 -0
- package/.agents/skills/brainstorm/SKILL.md +3 -0
- package/.agents/skills/close/SKILL.md +17 -0
- package/.agents/skills/debug/SKILL.md +2 -0
- package/.agents/skills/discovery/SKILL.md +2 -1
- package/.agents/skills/dispatcher/SKILL.md +2 -0
- package/.agents/skills/eli5/SKILL.md +2 -0
- package/.agents/skills/eval/SKILL.md +1 -0
- package/.agents/skills/evolve/SKILL.md +2 -1
- package/.agents/skills/go/SKILL.md +18 -0
- package/.agents/skills/grill/SKILL.md +2 -0
- package/.agents/skills/harness-audit/SKILL.md +16 -0
- package/.agents/skills/memory-cleanup/SKILL.md +1 -0
- package/.agents/skills/persona-panel/SKILL.md +1 -0
- package/.agents/skills/plan/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +17 -0
- package/.agents/skills/reconcile/SKILL.md +1 -0
- package/.agents/skills/release/SKILL.md +18 -0
- package/.agents/skills/repo-audit/SKILL.md +1 -0
- package/.agents/skills/spinout/SKILL.md +1 -0
- package/.agents/skills/sunset-review/SKILL.md +2 -0
- package/.agents/skills/test/SKILL.md +17 -0
- package/.agents/skills/ux-grill/SKILL.md +2 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/convergence-monitoring.md +13 -0
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/npm-publish.md +13 -0
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +61 -0
- package/README.md +11 -9
- package/commands/session.md +10 -0
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +11 -6
- package/docs/events-schema.md +4 -1
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +83 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +6 -4
- package/hooks/_lib/hook-import-set.json +46 -6
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +14 -2
- package/hooks/on-stop.mjs +43 -1
- package/hooks/post-bash-write-verify.mjs +3 -0
- package/hooks/pre-auq-clarity.mjs +3 -0
- package/hooks/pre-bash-issue-budget.mjs +103 -17
- package/hooks/pre-task-scope-disjoint.mjs +152 -3
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/convergence-monitoring.md +11 -0
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/npm-publish.md +11 -0
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/backfill-abandoned-sessions.mjs +57 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/emit-session.mjs +2 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +173 -9
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +133 -5
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/convergence-monitor.mjs +2 -2
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/instruction-budget-guard.mjs +186 -46
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +69 -3
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +392 -7
- package/scripts/lib/session-close-backfill.mjs +58 -6
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
- package/scripts/lib/user-invocable-skills.mjs +185 -0
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +366 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-skill-links.mjs +27 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
- package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
- package/scripts/lib/validate/check-unwired-features.mjs +91 -7
- package/scripts/lib/validate/check-validator-registration.mjs +2 -2
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate-vendored-rules.mjs +35 -9
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release.mjs +80 -35
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +216 -0
- package/scripts/validate-plugin.mjs +9 -0
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +2 -3
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +58 -4
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +14 -0
- package/skills/evolve/SKILL.md +8 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +10 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-plan/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +19 -1
- package/skills/wave-executor/SKILL.md +7 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
* `skills/`; these wrappers keep one maintained body while giving Cursor a
|
|
8
8
|
* native entry per command/skill (same pattern as `generate-pi-prompts.mjs`).
|
|
9
9
|
*
|
|
10
|
+
* `.cursor/commands/` is generated from the UNION of two sources: every
|
|
11
|
+
* `commands/*.md`, plus every skill whose frontmatter carries an explicit
|
|
12
|
+
* `user-invocable: true`. The second source exists because the operator-facing
|
|
13
|
+
* slash-command marker moved INTO the skill frontmatter when the command bodies
|
|
14
|
+
* were folded into their same-named `skills/<name>/SKILL.md`. A name present in
|
|
15
|
+
* both sources is a generator ERROR, not a precedence question — two documents
|
|
16
|
+
* claiming one public `/name` is a merge that did not finish.
|
|
17
|
+
*
|
|
18
|
+
* `disable-model-invocation` is propagated from the SOURCE frontmatter and is
|
|
19
|
+
* NOT derived from `user-invocable` — see {@link disablesModelInvocation} for
|
|
20
|
+
* the two grounds and the measurement behind them.
|
|
21
|
+
*
|
|
10
22
|
* Usage:
|
|
11
23
|
* node scripts/generate-cursor-adapter.mjs
|
|
12
24
|
* node scripts/generate-cursor-adapter.mjs --check
|
|
@@ -15,6 +27,7 @@
|
|
|
15
27
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
28
|
import path from 'node:path';
|
|
17
29
|
import { fileURLToPath } from 'node:url';
|
|
30
|
+
import { isUserInvocableValue } from './lib/user-invocable-skills.mjs';
|
|
18
31
|
|
|
19
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
33
|
const SCRIPT_DIR = path.dirname(__filename);
|
|
@@ -31,12 +44,14 @@ function isDir(p) {
|
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
function commandFiles() {
|
|
47
|
+
if (!existsSync(COMMANDS_DIR)) return [];
|
|
34
48
|
return readdirSync(COMMANDS_DIR)
|
|
35
49
|
.filter((name) => name.endsWith('.md'))
|
|
36
50
|
.sort();
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
function skillDirs() {
|
|
54
|
+
if (!existsSync(SKILLS_DIR)) return [];
|
|
40
55
|
return readdirSync(SKILLS_DIR)
|
|
41
56
|
.filter((name) => isDir(path.join(SKILLS_DIR, name)) && existsSync(path.join(SKILLS_DIR, name, 'SKILL.md')))
|
|
42
57
|
.sort();
|
|
@@ -44,15 +59,30 @@ function skillDirs() {
|
|
|
44
59
|
|
|
45
60
|
/**
|
|
46
61
|
* Parse YAML-ish frontmatter including `>` / `|` folded scalars.
|
|
62
|
+
*
|
|
63
|
+
* A UTF-8 BOM before the opening `---`, or CRLF line endings, used to make the
|
|
64
|
+
* two probes below miss the block entirely: the file parsed as "no
|
|
65
|
+
* frontmatter", so every flag in it (`user-invocable`,
|
|
66
|
+
* `disable-model-invocation`) silently disappeared and the skill was demoted
|
|
67
|
+
* out of `.cursor/commands/` with no diagnostic anywhere. Both are normalised
|
|
68
|
+
* away first — the same treatment `parseAgentFrontmatter`
|
|
69
|
+
* (`scripts/lib/agent-frontmatter.mjs`) gives them.
|
|
70
|
+
*
|
|
71
|
+
* NOT replaced by the shared `parseSkillFrontmatter`: that one returns the
|
|
72
|
+
* `__BLOCK_SCALAR__` sentinel for a `description: >`, which is the form every
|
|
73
|
+
* merged skill actually uses — routing through it would emit the sentinel as
|
|
74
|
+
* the wrapper description.
|
|
75
|
+
*
|
|
47
76
|
* @param {string} content
|
|
48
77
|
* @returns {Record<string, string>}
|
|
49
78
|
*/
|
|
50
79
|
function parseFrontmatter(content) {
|
|
51
|
-
|
|
52
|
-
|
|
80
|
+
const text = (content.charCodeAt(0) === 0xfeff ? content.slice(1) : content).replace(/\r\n?/g, '\n');
|
|
81
|
+
if (!text.startsWith('---\n')) return {};
|
|
82
|
+
const end = text.indexOf('\n---\n', 4);
|
|
53
83
|
if (end === -1) return {};
|
|
54
84
|
|
|
55
|
-
const lines =
|
|
85
|
+
const lines = text.slice(4, end).split('\n');
|
|
56
86
|
const fields = {};
|
|
57
87
|
let i = 0;
|
|
58
88
|
while (i < lines.length) {
|
|
@@ -158,8 +188,108 @@ Cursor has no Skill tool. When the command says to invoke a skill, Read \`skills
|
|
|
158
188
|
`;
|
|
159
189
|
}
|
|
160
190
|
|
|
161
|
-
|
|
162
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Skills that declare themselves operator-facing slash commands.
|
|
193
|
+
*
|
|
194
|
+
* The predicate is `isUserInvocableValue` from `scripts/lib/user-invocable-skills.mjs`
|
|
195
|
+
* — the ONE normaliser for this marker, shared with the Pi and Codex generators
|
|
196
|
+
* and with every counter. The private copy this replaced read only a
|
|
197
|
+
* whitespace-trimmed bare `true`, so `True` and `true # note` were demoted HERE
|
|
198
|
+
* while the shared counter listed them as commands; the SKILL.md path is passed
|
|
199
|
+
* so a demotion WARN names the file.
|
|
200
|
+
*
|
|
201
|
+
* @returns {string[]} skill names, sorted
|
|
202
|
+
*/
|
|
203
|
+
function userInvocableSkills() {
|
|
204
|
+
return skillDirs().filter((name) => {
|
|
205
|
+
const file = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
206
|
+
const fields = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
207
|
+
return isUserInvocableValue(fields['user-invocable'], file);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A `.cursor/commands/<name>.md` wrapper for a skill that IS the slash command
|
|
213
|
+
* (`user-invocable: true`). Same contract as {@link renderCommand}, pointing at
|
|
214
|
+
* the skill body instead of a command file.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} skillName
|
|
217
|
+
* @returns {string}
|
|
218
|
+
*/
|
|
219
|
+
function renderSkillCommand(skillName) {
|
|
220
|
+
const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
221
|
+
const fields = parseFrontmatter(readFileSync(skillPath, 'utf8'));
|
|
222
|
+
const description = clampDescription(fields.description || `Session Orchestrator skill: ${skillName}`);
|
|
223
|
+
// Same GH#54 rule as renderCommand: `argument-hint` ALWAYS goes through
|
|
224
|
+
// yamlQuote(), because its canonical authored form (`[mode] [--flag]`) is a
|
|
225
|
+
// YAML flow sequence when emitted bare.
|
|
226
|
+
// The command wrapper carries the SOURCE value only — never the library-skill
|
|
227
|
+
// ground of {@link disablesModelInvocation}, which cannot apply here: this
|
|
228
|
+
// surface exists BECAUSE the skill is `user-invocable: true`. Propagated so an
|
|
229
|
+
// operator-only command stays operator-only on whichever of the two surfaces
|
|
230
|
+
// Cursor reads the policy from (same mapping the Codex adapter makes onto
|
|
231
|
+
// `policy.allow_implicit_invocation`, `scripts/generate-codex-skills.mjs`).
|
|
232
|
+
const frontmatter = [
|
|
233
|
+
'---',
|
|
234
|
+
frontmatterLine('description', yamlQuote(description)),
|
|
235
|
+
frontmatterLine('argument-hint', yamlQuote(fields['argument-hint'])),
|
|
236
|
+
isUserInvocableValue(fields['disable-model-invocation'], skillPath) ? 'disable-model-invocation: true' : null,
|
|
237
|
+
'---',
|
|
238
|
+
].filter(Boolean).join('\n');
|
|
239
|
+
|
|
240
|
+
return `${frontmatter}
|
|
241
|
+
|
|
242
|
+
# /${skillName}
|
|
243
|
+
|
|
244
|
+
Use the Session Orchestrator skill definition at \`skills/${skillName}/SKILL.md\`.
|
|
245
|
+
|
|
246
|
+
Arguments: $ARGUMENTS
|
|
247
|
+
|
|
248
|
+
Read that skill file and follow it exactly. When it references \`$ARGUMENTS\`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
|
249
|
+
|
|
250
|
+
Cursor has no Skill tool. When the skill says to invoke another skill, Read \`skills/<skill-name>/SKILL.md\` and follow it. Supporting files (\`soul.md\`, phase docs) live in that same \`skills/<skill-name>/\` directory.
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Emit `disable-model-invocation: true` into a Cursor wrapper?
|
|
256
|
+
*
|
|
257
|
+
* TWO independent grounds, OR-ed — the flag is a restriction, so the rule is
|
|
258
|
+
* fail-closed:
|
|
259
|
+
* 1. the SOURCE skill declares it (the single truth for operator-only skills).
|
|
260
|
+
* That flag is an INDEPENDENT axis from `user-invocable`, not its inverse:
|
|
261
|
+
* six skills carry BOTH (measured 2026-09-17: bootstrap, brainstorm, close,
|
|
262
|
+
* go, plan, release), and deriving the wrapper's flag from `user-invocable`
|
|
263
|
+
* alone emitted NO flag for exactly those six — letting a Cursor model
|
|
264
|
+
* auto-invoke `/close`, `/go` and `/release`;
|
|
265
|
+
* 2. the skill is not `user-invocable`, i.e. a library skill whose Cursor
|
|
266
|
+
* wrapper is reached by an explicit Read from a command body, never by a
|
|
267
|
+
* model's own dispatch. This ground predates the #1 fix and is kept
|
|
268
|
+
* deliberately: dropping it would REMOVE the flag from 24 of 50 wrappers
|
|
269
|
+
* (measured 2026-09-17) on no Cursor-side evidence, which is the one
|
|
270
|
+
* direction a guard fix must never move.
|
|
271
|
+
*
|
|
272
|
+
* Truth table — both readings normalised by `isUserInvocableValue`, so every
|
|
273
|
+
* form a YAML parser reads as `true` (`True`, `"true"`, `true # note`) counts
|
|
274
|
+
* as `true` on BOTH axes. Ground 2 made that load-bearing rather than cosmetic:
|
|
275
|
+
* while this file kept a private bare-`true` predicate, a `True`-marked
|
|
276
|
+
* user-invocable skill read as NOT user-invocable HERE and got the restriction
|
|
277
|
+
* STAMPED, while the shared counter listed it as a command:
|
|
278
|
+
*
|
|
279
|
+
* | source flag | user-invocable | emitted |
|
|
280
|
+
* |-------------|----------------|---------|
|
|
281
|
+
* | true | true | yes (1) |
|
|
282
|
+
* | true | false/absent | yes (1+2) |
|
|
283
|
+
* | false/absent| true | no |
|
|
284
|
+
* | false/absent| false/absent | yes (2) |
|
|
285
|
+
*
|
|
286
|
+
* @param {Record<string, string>} fields source skill frontmatter
|
|
287
|
+
* @param {string} [file] SKILL.md path, named in a demotion WARN
|
|
288
|
+
* @returns {boolean}
|
|
289
|
+
*/
|
|
290
|
+
function disablesModelInvocation(fields, file) {
|
|
291
|
+
return isUserInvocableValue(fields['disable-model-invocation'], file)
|
|
292
|
+
|| !isUserInvocableValue(fields['user-invocable'], file);
|
|
163
293
|
}
|
|
164
294
|
|
|
165
295
|
function renderSkill(skillName) {
|
|
@@ -174,7 +304,7 @@ function renderSkill(skillName) {
|
|
|
174
304
|
`name: ${yamlQuote(skillName)}`,
|
|
175
305
|
`description: ${yamlQuote(description)}`,
|
|
176
306
|
];
|
|
177
|
-
if (
|
|
307
|
+
if (disablesModelInvocation(fields, skillPath)) {
|
|
178
308
|
lines.push('disable-model-invocation: true');
|
|
179
309
|
}
|
|
180
310
|
lines.push('---');
|
|
@@ -191,12 +321,37 @@ Cursor has no Skill tool. Treat "invoke the ${skillName} skill" as: Read \`skill
|
|
|
191
321
|
`;
|
|
192
322
|
}
|
|
193
323
|
|
|
324
|
+
/**
|
|
325
|
+
* The expected `.cursor/commands/` set: every `commands/*.md` PLUS every skill
|
|
326
|
+
* marked `user-invocable: true`.
|
|
327
|
+
*
|
|
328
|
+
* @returns {Map<string, string>} file name → content, sorted by file name
|
|
329
|
+
* @throws {Error} when one public name is claimed by both sources
|
|
330
|
+
*/
|
|
194
331
|
function expectedCommands() {
|
|
195
332
|
const commands = new Map();
|
|
196
333
|
for (const commandFile of commandFiles()) {
|
|
197
334
|
commands.set(commandFile, renderCommand(commandFile));
|
|
198
335
|
}
|
|
199
|
-
|
|
336
|
+
|
|
337
|
+
const collisions = [];
|
|
338
|
+
for (const skillName of userInvocableSkills()) {
|
|
339
|
+
const fileName = `${skillName}.md`;
|
|
340
|
+
if (commands.has(fileName)) {
|
|
341
|
+
collisions.push(skillName);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
commands.set(fileName, renderSkillCommand(skillName));
|
|
345
|
+
}
|
|
346
|
+
if (collisions.length > 0) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`${collisions.length} public name(s) claimed by BOTH commands/ and a user-invocable skill: ${collisions.join(', ')}. `
|
|
349
|
+
+ 'Exactly one document may own a slash command — delete the commands/<name>.md whose body was folded into skills/<name>/SKILL.md, '
|
|
350
|
+
+ 'or drop `user-invocable: true` from the skill.',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return new Map([...commands].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
200
355
|
}
|
|
201
356
|
|
|
202
357
|
function expectedSkills() {
|
|
@@ -284,7 +439,16 @@ function writeAll(expectedCmds, expectedSkillsMap) {
|
|
|
284
439
|
process.stdout.write(`cursor adapter: wrote ${expectedCmds.size} command(s), ${expectedSkillsMap.size} skill(s)\n`);
|
|
285
440
|
}
|
|
286
441
|
|
|
287
|
-
|
|
288
|
-
|
|
442
|
+
let expectedCmds;
|
|
443
|
+
let expectedSkillsMap;
|
|
444
|
+
try {
|
|
445
|
+
expectedCmds = expectedCommands();
|
|
446
|
+
expectedSkillsMap = expectedSkills();
|
|
447
|
+
} catch (error) {
|
|
448
|
+
// Loud and diagnosable: a source conflict must never degrade into a partial
|
|
449
|
+
// write or a stack trace read as "some node thing went wrong".
|
|
450
|
+
process.stderr.write(`FAIL: ${error.message}\n`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
289
453
|
if (CHECK_ONLY) checkAll(expectedCmds, expectedSkillsMap);
|
|
290
454
|
else writeAll(expectedCmds, expectedSkillsMap);
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
* write is NEVER performed on a usage error.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { readFileSync, writeFileSync, existsSync, statSync
|
|
27
|
+
import { readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
-
import { fileURLToPath } from 'node:url';
|
|
30
29
|
import { execFileSync } from 'node:child_process';
|
|
30
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
31
31
|
|
|
32
32
|
/** The four hook manifests whose entry files seed the crawl. */
|
|
33
33
|
export const HOOK_MANIFESTS = [
|
|
@@ -263,30 +263,15 @@ export function main(argv) {
|
|
|
263
263
|
return 0;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* @returns {boolean}
|
|
277
|
-
*/
|
|
278
|
-
function invokedAsScript() {
|
|
279
|
-
const entry = process.argv[1];
|
|
280
|
-
if (!entry) return false;
|
|
281
|
-
const self = fileURLToPath(import.meta.url);
|
|
282
|
-
try {
|
|
283
|
-
return realpathSync(entry) === realpathSync(self);
|
|
284
|
-
} catch {
|
|
285
|
-
// argv[1] unresolvable (deleted/renamed mid-run) — best-effort raw compare.
|
|
286
|
-
return entry === self;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (invokedAsScript()) {
|
|
266
|
+
// Entry guard via the canonical predicate (`scripts/lib/is-main-module.mjs`).
|
|
267
|
+
// A string compare of `import.meta.url` against `file://${process.argv[1]}` is
|
|
268
|
+
// fragile in exactly the invocations CI and husky use: any symlinked or
|
|
269
|
+
// realpath-differing path (measured: `/tmp` → `/private/tmp` on macOS) makes the
|
|
270
|
+
// compare false, so the whole CLI body becomes a silent no-op that still exits 0
|
|
271
|
+
// — a fail-OPEN drift gate. This script is a plain CLI, not a hook, so it may
|
|
272
|
+
// import from `scripts/lib/` (the hooks that still inline the same predicate do
|
|
273
|
+
// so to keep `scripts/lib` off their hot path — see the ceiling comment in
|
|
274
|
+
// `hooks/post-bash-write-verify.mjs`).
|
|
275
|
+
if (isMainModule(import.meta.url)) {
|
|
291
276
|
process.exit(main(process.argv));
|
|
292
277
|
}
|
|
@@ -6,56 +6,162 @@
|
|
|
6
6
|
* cross-harness `$ARGUMENTS` placeholder. The generated wrapper keeps one
|
|
7
7
|
* maintained command source while giving Pi a native prompt entry per command.
|
|
8
8
|
*
|
|
9
|
+
* `pi/prompts/` is generated from the UNION of two sources: every
|
|
10
|
+
* `commands/*.md`, plus every skill whose frontmatter carries an explicit
|
|
11
|
+
* `user-invocable: true`. The second source exists because the operator-facing
|
|
12
|
+
* slash-command marker moved INTO the skill frontmatter when the command bodies
|
|
13
|
+
* were folded into their same-named `skills/<name>/SKILL.md`. A name present in
|
|
14
|
+
* both sources is a generator ERROR, not a precedence question — two documents
|
|
15
|
+
* claiming one public `/name` is a merge that did not finish.
|
|
16
|
+
*
|
|
9
17
|
* Usage:
|
|
10
18
|
* node scripts/generate-pi-prompts.mjs
|
|
11
19
|
* node scripts/generate-pi-prompts.mjs --check
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
15
23
|
import path from 'node:path';
|
|
16
24
|
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { isUserInvocableValue } from './lib/user-invocable-skills.mjs';
|
|
17
26
|
|
|
18
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
28
|
const SCRIPT_DIR = path.dirname(__filename);
|
|
20
29
|
const ROOT = path.dirname(SCRIPT_DIR);
|
|
21
30
|
const COMMANDS_DIR = path.join(ROOT, 'commands');
|
|
31
|
+
const SKILLS_DIR = path.join(ROOT, 'skills');
|
|
22
32
|
const PROMPTS_DIR = path.join(ROOT, 'pi', 'prompts');
|
|
23
33
|
const CHECK_ONLY = process.argv.includes('--check');
|
|
34
|
+
const DESCRIPTION_MAX = 1024;
|
|
35
|
+
|
|
36
|
+
function isDir(p) {
|
|
37
|
+
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
38
|
+
}
|
|
24
39
|
|
|
25
40
|
function commandFiles() {
|
|
41
|
+
if (!existsSync(COMMANDS_DIR)) return [];
|
|
26
42
|
return readdirSync(COMMANDS_DIR)
|
|
27
43
|
.filter((name) => name.endsWith('.md'))
|
|
28
44
|
.sort();
|
|
29
45
|
}
|
|
30
46
|
|
|
47
|
+
function skillDirs() {
|
|
48
|
+
if (!existsSync(SKILLS_DIR)) return [];
|
|
49
|
+
return readdirSync(SKILLS_DIR)
|
|
50
|
+
.filter((name) => isDir(path.join(SKILLS_DIR, name)) && existsSync(path.join(SKILLS_DIR, name, 'SKILL.md')))
|
|
51
|
+
.sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse YAML-ish frontmatter including `>` / `|` folded scalars, returning
|
|
56
|
+
* DECODED values (surrounding quotes stripped). Skill descriptions are folded
|
|
57
|
+
* scalars in practice, which the previous line-at-a-time parser read as the
|
|
58
|
+
* literal `>`. Mirrors `generate-cursor-adapter.mjs`.
|
|
59
|
+
*
|
|
60
|
+
* A UTF-8 BOM before the opening `---`, or CRLF line endings, used to make the
|
|
61
|
+
* two probes below miss the block entirely: the file parsed as "no
|
|
62
|
+
* frontmatter", so `user-invocable` silently disappeared and the skill was
|
|
63
|
+
* demoted out of `pi/prompts/` with no diagnostic anywhere. Both are normalised
|
|
64
|
+
* away first — the same treatment `parseAgentFrontmatter`
|
|
65
|
+
* (`scripts/lib/agent-frontmatter.mjs`) gives them.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} content
|
|
68
|
+
* @returns {Record<string, string>}
|
|
69
|
+
*/
|
|
31
70
|
function parseFrontmatter(content) {
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
const text = (content.charCodeAt(0) === 0xfeff ? content.slice(1) : content).replace(/\r\n?/g, '\n');
|
|
72
|
+
if (!text.startsWith('---\n')) return {};
|
|
73
|
+
const end = text.indexOf('\n---\n', 4);
|
|
34
74
|
if (end === -1) return {};
|
|
35
75
|
|
|
76
|
+
const lines = text.slice(4, end).split('\n');
|
|
36
77
|
const fields = {};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const match =
|
|
40
|
-
if (!match)
|
|
41
|
-
|
|
78
|
+
let i = 0;
|
|
79
|
+
while (i < lines.length) {
|
|
80
|
+
const match = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
81
|
+
if (!match) {
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const key = match[1];
|
|
86
|
+
const raw = match[2];
|
|
87
|
+
if (raw === '>' || raw === '| ' || raw === '|' || raw === '>-' || raw === '|-') {
|
|
88
|
+
const folded = [];
|
|
89
|
+
i += 1;
|
|
90
|
+
while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t') || lines[i] === '')) {
|
|
91
|
+
folded.push(lines[i].replace(/^\s+/, ''));
|
|
92
|
+
i += 1;
|
|
93
|
+
}
|
|
94
|
+
const joiner = raw.startsWith('|') ? '\n' : ' ';
|
|
95
|
+
fields[key] = folded.filter(Boolean).join(joiner).trim();
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
fields[key] = raw.replace(/^["']|["']$/g, '');
|
|
99
|
+
i += 1;
|
|
42
100
|
}
|
|
43
101
|
return fields;
|
|
44
102
|
}
|
|
45
103
|
|
|
104
|
+
/** @see generate-cursor-adapter.mjs — same allow-list, same reasoning. */
|
|
105
|
+
const YAML_SCALAR_LOOKALIKE = /^(?:true|false|yes|no|on|off|null|nan|[-+]?\.?inf|~)$/i;
|
|
106
|
+
const YAML_PLAIN_SAFE = /^[A-Za-z0-9_][A-Za-z0-9 _.,()/-]*$/;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Render a frontmatter VALUE as YAML. `argument-hint`'s canonical authored form
|
|
110
|
+
* (`[mode] [--flag]`) is a YAML flow SEQUENCE when emitted bare — GH#54, which
|
|
111
|
+
* made Copilot CLI >= 1.0.65 silently drop the file.
|
|
112
|
+
*
|
|
113
|
+
* @param {unknown} value
|
|
114
|
+
* @returns {string|null} the YAML scalar, or `null` when there is nothing to emit
|
|
115
|
+
*/
|
|
116
|
+
function yamlQuote(value) {
|
|
117
|
+
if (value === undefined || value === null || value === '') return null;
|
|
118
|
+
const text = String(value);
|
|
119
|
+
if (text.trim() === '') return null;
|
|
120
|
+
if (text !== text.trim()) return JSON.stringify(text);
|
|
121
|
+
if (!YAML_PLAIN_SAFE.test(text)) return JSON.stringify(text);
|
|
122
|
+
if (YAML_SCALAR_LOOKALIKE.test(text)) return JSON.stringify(text);
|
|
123
|
+
if (/^\d/.test(text)) return JSON.stringify(text);
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
46
127
|
function frontmatterLine(key, value) {
|
|
47
|
-
if (value === undefined || value === '') return null;
|
|
128
|
+
if (value === undefined || value === '' || value === null) return null;
|
|
48
129
|
return `${key}: ${value}`;
|
|
49
130
|
}
|
|
50
131
|
|
|
132
|
+
function clampDescription(text) {
|
|
133
|
+
const collapsed = String(text || '').replace(/\s+/g, ' ').trim();
|
|
134
|
+
if (collapsed.length <= DESCRIPTION_MAX) return collapsed;
|
|
135
|
+
return collapsed.slice(0, DESCRIPTION_MAX - 1).trimEnd() + '…';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Skills that declare themselves operator-facing slash commands.
|
|
140
|
+
*
|
|
141
|
+
* The predicate is `isUserInvocableValue` from `scripts/lib/user-invocable-skills.mjs`
|
|
142
|
+
* — the ONE normaliser for this marker, shared with the Cursor and Codex
|
|
143
|
+
* generators and with every counter. A local copy here is how the quoted-value
|
|
144
|
+
* disagreement arose (`"true"`: wrapper generated on two adapters, counted by
|
|
145
|
+
* neither counter, crash on the third).
|
|
146
|
+
*
|
|
147
|
+
* @returns {string[]} skill names, sorted
|
|
148
|
+
*/
|
|
149
|
+
function userInvocableSkills() {
|
|
150
|
+
return skillDirs().filter((name) => {
|
|
151
|
+
const file = path.join(SKILLS_DIR, name, 'SKILL.md');
|
|
152
|
+
const fields = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
153
|
+
return isUserInvocableValue(fields['user-invocable'], file);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
51
157
|
function renderPrompt(commandFile) {
|
|
52
158
|
const commandPath = path.join(COMMANDS_DIR, commandFile);
|
|
53
159
|
const commandName = commandFile.replace(/\.md$/, '');
|
|
54
160
|
const fields = parseFrontmatter(readFileSync(commandPath, 'utf8'));
|
|
55
161
|
const frontmatter = [
|
|
56
162
|
'---',
|
|
57
|
-
frontmatterLine('description', fields.description),
|
|
58
|
-
frontmatterLine('argument-hint', fields['argument-hint']),
|
|
163
|
+
frontmatterLine('description', yamlQuote(clampDescription(fields.description))),
|
|
164
|
+
frontmatterLine('argument-hint', yamlQuote(fields['argument-hint'])),
|
|
59
165
|
'---',
|
|
60
166
|
].filter(Boolean).join('\n');
|
|
61
167
|
|
|
@@ -71,12 +177,68 @@ Read that command file and follow it exactly. When it references \`$ARGUMENTS\`,
|
|
|
71
177
|
`;
|
|
72
178
|
}
|
|
73
179
|
|
|
180
|
+
/**
|
|
181
|
+
* A `pi/prompts/<name>.md` wrapper for a skill that IS the slash command
|
|
182
|
+
* (`user-invocable: true`). Same contract as {@link renderPrompt}, pointing at
|
|
183
|
+
* the skill body instead of a command file.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} skillName
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
function renderSkillPrompt(skillName) {
|
|
189
|
+
const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
190
|
+
const fields = parseFrontmatter(readFileSync(skillPath, 'utf8'));
|
|
191
|
+
const description = clampDescription(fields.description || `Session Orchestrator skill: ${skillName}`);
|
|
192
|
+
const frontmatter = [
|
|
193
|
+
'---',
|
|
194
|
+
frontmatterLine('description', yamlQuote(description)),
|
|
195
|
+
frontmatterLine('argument-hint', yamlQuote(fields['argument-hint'])),
|
|
196
|
+
'---',
|
|
197
|
+
].filter(Boolean).join('\n');
|
|
198
|
+
|
|
199
|
+
return `${frontmatter}
|
|
200
|
+
|
|
201
|
+
# /${skillName}
|
|
202
|
+
|
|
203
|
+
Use the Session Orchestrator skill definition at \`skills/${skillName}/SKILL.md\`.
|
|
204
|
+
|
|
205
|
+
Arguments: $@
|
|
206
|
+
|
|
207
|
+
Read that skill file and follow it exactly. When it references \`$ARGUMENTS\`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The expected `pi/prompts/` set: every `commands/*.md` PLUS every skill marked
|
|
213
|
+
* `user-invocable: true`.
|
|
214
|
+
*
|
|
215
|
+
* @returns {Map<string, string>} file name -> content, sorted by file name
|
|
216
|
+
* @throws {Error} when one public name is claimed by both sources
|
|
217
|
+
*/
|
|
74
218
|
function expectedPrompts() {
|
|
75
219
|
const prompts = new Map();
|
|
76
220
|
for (const commandFile of commandFiles()) {
|
|
77
221
|
prompts.set(commandFile, renderPrompt(commandFile));
|
|
78
222
|
}
|
|
79
|
-
|
|
223
|
+
|
|
224
|
+
const collisions = [];
|
|
225
|
+
for (const skillName of userInvocableSkills()) {
|
|
226
|
+
const fileName = `${skillName}.md`;
|
|
227
|
+
if (prompts.has(fileName)) {
|
|
228
|
+
collisions.push(skillName);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
prompts.set(fileName, renderSkillPrompt(skillName));
|
|
232
|
+
}
|
|
233
|
+
if (collisions.length > 0) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`${collisions.length} public name(s) claimed by BOTH commands/ and a user-invocable skill: ${collisions.join(', ')}. `
|
|
236
|
+
+ 'Exactly one document may own a slash command — delete the commands/<name>.md whose body was folded into skills/<name>/SKILL.md, '
|
|
237
|
+
+ 'or drop `user-invocable: true` from the skill.',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return new Map([...prompts].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
80
242
|
}
|
|
81
243
|
|
|
82
244
|
function checkPrompts(expected) {
|
|
@@ -122,6 +284,14 @@ function writePrompts(expected) {
|
|
|
122
284
|
console.log(`pi prompts: wrote ${expected.size} file(s)`);
|
|
123
285
|
}
|
|
124
286
|
|
|
125
|
-
|
|
287
|
+
let expected;
|
|
288
|
+
try {
|
|
289
|
+
expected = expectedPrompts();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// Loud and diagnosable: a source conflict must never degrade into a partial
|
|
292
|
+
// write or a stack trace read as "some node thing went wrong".
|
|
293
|
+
console.error(`FAIL: ${error.message}`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
126
296
|
if (CHECK_ONLY) checkPrompts(expected);
|
|
127
297
|
else writePrompts(expected);
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
|
|
35
35
|
import { execFile as _execFile } from 'node:child_process';
|
|
36
36
|
import { promisify } from 'node:util';
|
|
37
|
-
import { pathToFileURL } from 'node:url';
|
|
38
37
|
import { resolveRepoSpec as _resolveRepoSpec, resolveRepoHost as _resolveRepoHost } from './lib/vcs-repo-spec.mjs';
|
|
38
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
39
39
|
|
|
40
40
|
const execFileAsync = promisify(_execFile);
|
|
41
41
|
|
|
@@ -347,8 +347,7 @@ export async function auditGithubBranchProtection(opts = {}, deps = {}) {
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
// CLI entry — only when run directly, not when imported (e.g. by tests).
|
|
350
|
-
const isMain =
|
|
351
|
-
process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
350
|
+
const isMain =isMainModule(import.meta.url);
|
|
352
351
|
|
|
353
352
|
if (isMain) {
|
|
354
353
|
auditGithubBranchProtection({ repoRoot: process.cwd() }).then((result) => {
|
|
@@ -58,7 +58,12 @@ export function parseAgentFrontmatter(contents) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// A UTF-8 BOM before `---` makes FRONTMATTER_RE miss the block entirely, so a
|
|
62
|
+
// BOM-prefixed SKILL.md would parse as "no frontmatter" and every flag in it
|
|
63
|
+
// (`user-invocable` included) silently disappear.
|
|
64
|
+
const text = contents.charCodeAt(0) === 0xfeff ? contents.slice(1) : contents;
|
|
65
|
+
|
|
66
|
+
const match = FRONTMATTER_RE.exec(text);
|
|
62
67
|
if (!match) {
|
|
63
68
|
return {
|
|
64
69
|
ok: false,
|
|
@@ -76,19 +81,36 @@ export function parseAgentFrontmatter(contents) {
|
|
|
76
81
|
const frontmatter = /** @type {Record<string,string>} */ ({});
|
|
77
82
|
const lines = fmText.split(/\r?\n/);
|
|
78
83
|
|
|
84
|
+
// Indented lines belong to whatever the preceding top-level key opened (a
|
|
85
|
+
// nested mapping, or a block scalar) — they are NOT top-level keys. Without
|
|
86
|
+
// this, `metadata:\n user-invocable: true` was HOISTED to top level and read
|
|
87
|
+
// as the slash-command marker, so the counters saw a skill no generator did.
|
|
88
|
+
let inNestedBlock = false;
|
|
89
|
+
|
|
79
90
|
for (const rawLine of lines) {
|
|
80
91
|
const line = rawLine.replace(/\s+$/, '');
|
|
81
92
|
if (line === '' || /^\s*#/.test(line)) continue;
|
|
82
93
|
|
|
94
|
+
if (/^\s/.test(rawLine)) {
|
|
95
|
+
if (inNestedBlock) continue;
|
|
96
|
+
} else {
|
|
97
|
+
inNestedBlock = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
// Detect block-scalar indicators on a key line, e.g. `description: >` or `description: |`
|
|
84
101
|
// These are valid YAML but break Claude Code's parser.
|
|
85
102
|
const blockScalarMatch = /^(\w[\w-]*):\s*[>|]\s*$/.exec(line);
|
|
86
103
|
if (blockScalarMatch) {
|
|
87
104
|
// Store a sentinel so the validator can emit the correct rule.
|
|
88
105
|
frontmatter[blockScalarMatch[1]] = '__BLOCK_SCALAR__';
|
|
106
|
+
inNestedBlock = true;
|
|
89
107
|
continue;
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
// A bare `key:` at top level opens a nested mapping (or an empty value);
|
|
111
|
+
// everything indented under it is that mapping's business, not ours.
|
|
112
|
+
if (/^\w[\w-]*:$/.test(line)) inNestedBlock = true;
|
|
113
|
+
|
|
92
114
|
const idx = line.indexOf(':');
|
|
93
115
|
if (idx === -1) continue;
|
|
94
116
|
const key = line.slice(0, idx).trim();
|
|
@@ -67,6 +67,7 @@ import { basename, resolve, sep } from 'node:path';
|
|
|
67
67
|
import { homedir } from 'node:os';
|
|
68
68
|
import { resolveInstructionFile } from './common.mjs';
|
|
69
69
|
import { isSessionConfigHeading } from './config/section-extractor.mjs';
|
|
70
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
70
71
|
|
|
71
72
|
/** Thrown by `lintClaudeMd()` on any infra-level failure (missing/unreadable file). */
|
|
72
73
|
export class ClaudeMdLintInfraError extends Error {
|
|
@@ -536,10 +537,6 @@ function main() {
|
|
|
536
537
|
}
|
|
537
538
|
|
|
538
539
|
const __filename = fileURLToPath(import.meta.url);
|
|
539
|
-
const isMain =
|
|
540
|
-
typeof process !== 'undefined' &&
|
|
541
|
-
process.argv[1] !== null &&
|
|
542
|
-
process.argv[1] !== undefined &&
|
|
543
|
-
resolve(process.argv[1]) === resolve(__filename);
|
|
540
|
+
const isMain =isMainModule(import.meta.url);
|
|
544
541
|
|
|
545
542
|
if (isMain) main();
|