session-orchestrator 5.7.0 → 5.9.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/architecture/SKILL.md +3 -0
- package/.agents/skills/architecture/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.agents/skills/eval/SKILL.md +1 -1
- package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.agents/skills/hook-development/SKILL.md +1 -0
- package/.agents/skills/mcp-builder/SKILL.md +1 -0
- package/.agents/skills/tmux-layout/SKILL.md +3 -0
- package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +1 -0
- package/.agents/skills/vault-sync/SKILL.md +1 -0
- package/.agents/skills/write-executable-plan/SKILL.md +3 -0
- package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -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/architecture/SKILL.md +3 -0
- package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
- package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
- package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.cursor/commands/architecture.md +13 -0
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/tmux-layout.md +13 -0
- package/.cursor/commands/write-executable-plan.md +13 -0
- package/.cursor/skills/architecture/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/tmux-layout/SKILL.md +0 -1
- package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +13 -9
- package/agents/eval-judge.md +5 -5
- package/docs/baseline.md +4 -3
- package/docs/codex-setup.md +6 -4
- package/docs/components.md +5 -4
- package/docs/events-schema.md +45 -4
- package/docs/github-mirror-protection.md +3 -2
- package/docs/install.md +39 -3
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +2 -2
- package/docs/session-config-reference.md +7 -5
- package/docs/session-config-template.md +3 -2
- package/evals/README.md +60 -0
- package/evals/instruction-ablation/README.md +147 -0
- package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
- package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
- package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
- package/evals/instruction-ablation/run.mjs +336 -0
- package/evals/psa-003-foreign-work/case.yaml +16 -0
- package/evals/psa-003-foreign-work/fixture.sh +12 -0
- package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
- package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
- package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
- package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
- package/evals/psa-003-foreign-work/prompt.md +6 -0
- package/hooks/_lib/hook-import-set.json +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/post-tool-batch-wave-signal.mjs +1 -0
- package/hooks/post-tooluse-frontend-slop.mjs +11 -1
- package/hooks/pre-task-scope-disjoint.mjs +8 -6
- package/monitors/monitors.json +1 -1
- package/package.json +3 -1
- package/pi/prompts/architecture.md +11 -0
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/tmux-layout.md +11 -0
- package/pi/prompts/write-executable-plan.md +11 -0
- package/scripts/backfill-abandoned-sessions.mjs +2 -2
- package/scripts/backfill-sessions.mjs +9 -7
- package/scripts/codex-install.mjs +108 -23
- package/scripts/emit-session.mjs +15 -2
- package/scripts/fleet-auflagen.mjs +54 -0
- package/scripts/generate-agents-skills.mjs +9 -16
- package/scripts/generate-codex-skills.mjs +8 -6
- package/scripts/generate-cursor-adapter.mjs +9 -74
- package/scripts/generate-pi-prompts.mjs +8 -74
- package/scripts/github-protection-audit.mjs +8 -0
- package/scripts/lib/auto-dialectic.mjs +10 -9
- package/scripts/lib/auto-dream.mjs +8 -10
- package/scripts/lib/autopilot/telemetry.mjs +12 -6
- package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
- package/scripts/lib/ci-status-banner.mjs +38 -5
- package/scripts/lib/config/drift-check.mjs +1 -1
- package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/convergence-monitor.mjs +5 -3
- package/scripts/lib/ecosystem-health.mjs +5 -3
- package/scripts/lib/eval/engine.mjs +61 -27
- package/scripts/lib/eval/judge.mjs +14 -8
- package/scripts/lib/eval/session-resolve.mjs +7 -2
- package/scripts/lib/events-retention-banner.mjs +5 -2
- package/scripts/lib/events-schema.mjs +8 -0
- package/scripts/lib/events.mjs +19 -5
- package/scripts/lib/fleet-protocol.mjs +98 -1
- package/scripts/lib/instruction-budget-guard.mjs +25 -18
- package/scripts/lib/io.mjs +172 -35
- package/scripts/lib/learnings/candidates.mjs +7 -28
- package/scripts/lib/learnings/io.mjs +7 -2
- package/scripts/lib/learnings/judgment.mjs +5 -8
- package/scripts/lib/learnings/select.mjs +3 -3
- package/scripts/lib/maintenance-due-banner.mjs +2 -2
- package/scripts/lib/memory-proposals/sink.mjs +5 -6
- package/scripts/lib/memory-proposals/store.mjs +3 -5
- package/scripts/lib/mirror-issues-banner.mjs +11 -1
- package/scripts/lib/native-command.mjs +48 -0
- package/scripts/lib/orphan-reaper/scan.mjs +29 -3
- package/scripts/lib/peer-cards/writer.mjs +6 -18
- package/scripts/lib/platform.mjs +113 -4
- package/scripts/lib/plugin-package-stage.mjs +169 -0
- package/scripts/lib/plugin-surface.mjs +121 -0
- package/scripts/lib/quality-gate-wave.mjs +40 -0
- package/scripts/lib/quality-gate.mjs +36 -30
- package/scripts/lib/reconcile/idempotency.mjs +9 -10
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +48 -11
- package/scripts/lib/reconcile/writer.mjs +11 -17
- package/scripts/lib/rule-loader.mjs +32 -6
- package/scripts/lib/scope-echo.mjs +12 -2
- package/scripts/lib/scope-gate.mjs +2 -1
- package/scripts/lib/session-close-backfill.mjs +3 -4
- package/scripts/lib/session-id.mjs +2 -1
- package/scripts/lib/session-record-repair.mjs +28 -21
- package/scripts/lib/session-schema/constants.mjs +20 -5
- package/scripts/lib/session-schema/validator.mjs +12 -0
- package/scripts/lib/session-start-probes.mjs +22 -7
- package/scripts/lib/sessions-staleness-banner.mjs +2 -3
- package/scripts/lib/skill-evolution/engine.mjs +6 -9
- package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
- package/scripts/lib/slopcheck.mjs +9 -7
- package/scripts/lib/sunset/walker.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +3 -1
- package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
- package/scripts/lib/test-sibling-coverage.mjs +167 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
- package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
- package/scripts/lib/user-invocable-skills.mjs +37 -10
- package/scripts/lib/ux-grill/run-record.mjs +5 -3
- package/scripts/lib/validate/check-banner-parity.mjs +1 -0
- package/scripts/lib/validate/check-skills.mjs +12 -2
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
- package/scripts/lib/validate/check-test-value-bans.mjs +3 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/vault-archive.mjs +2 -4
- package/scripts/lib/vault-mirror/process.mjs +207 -26
- package/scripts/lib/vault-mirror/utils.mjs +16 -1
- package/scripts/lib/vault-sync-baseline.mjs +9 -8
- package/scripts/lib/wave-executor/remote-dispatch.mjs +27 -9
- package/scripts/lib/wave-resource-gate.mjs +2 -1
- package/scripts/lib/wave-transcript-tail.mjs +8 -4
- package/scripts/lib/worktree/meta.mjs +7 -3
- package/scripts/mcp-server.mjs +158 -0
- package/scripts/migrate-sessions-jsonl.mjs +7 -5
- package/scripts/migrate-vault-paths.mjs +7 -4
- package/scripts/print-learnings-index.mjs +2 -1
- package/scripts/release.mjs +4 -1
- package/scripts/relocate-vault-corpus.mjs +5 -3
- package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
- package/scripts/run-quality-gate.mjs +9 -39
- package/scripts/self-update.mjs +179 -39
- package/scripts/site-numbers.mjs +8 -3
- package/scripts/sweep-expired-learnings.mjs +6 -4
- package/scripts/sync-vault-schema.mjs +3 -7
- package/scripts/windows-setup.mjs +45 -0
- package/skills/_shared/fleet-protocol.md +10 -0
- package/skills/architecture/SKILL.md +1 -0
- package/skills/bootstrap/_shared-template.md +5 -2
- package/skills/claude-md-drift-check/SKILL.md +4 -1
- package/skills/claude-md-drift-check/checker.mjs +83 -10
- package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
- package/skills/eval/SKILL.md +12 -13
- package/skills/eval/rubric-v3.md +521 -0
- package/skills/frontmatter-guard/SKILL.md +1 -0
- package/skills/gitlab-portfolio/SKILL.md +1 -0
- package/skills/hook-development/SKILL.md +1 -0
- package/skills/mcp-builder/SKILL.md +1 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/repo-audit/SKILL.md +2 -2
- package/skills/session-end/SKILL.md +32 -1
- package/skills/session-end/metrics-collection.md +15 -9
- package/skills/session-end/vault-operations.md +12 -2
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/phase-7-1-premise-check.md +2 -1
- package/skills/session-start/references/operations-contract.md +4 -1
- package/skills/tmux-layout/SKILL.md +1 -0
- package/skills/vault-mirror/SKILL.md +1 -0
- package/skills/vault-sync/SKILL.md +6 -6
- package/skills/vault-sync/validator.sh +29 -10
- package/skills/wave-executor/references/wave-loop-dispatch.md +1 -1
- package/skills/wave-executor/references/wave-loop-review.md +13 -7
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +4 -4
- package/skills/write-executable-plan/SKILL.md +1 -0
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
* {"mode":"dry-run|apply|mark-deprecated-only","total":N,"rewritten":N,"deprecated":N,"unchanged":N,"backup":"<path>|null"}
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { readFileSync,
|
|
32
|
-
import {
|
|
31
|
+
import { readFileSync, copyFileSync, existsSync } from 'node:fs';
|
|
32
|
+
import { basename } from 'node:path';
|
|
33
|
+
import { atomicWriteWithBackup, envelopeToError } from './lib/io.mjs';
|
|
33
34
|
import { validateSession, normalizeSession, ValidationError } from './lib/session-schema.mjs';
|
|
34
35
|
|
|
35
36
|
const DEFAULT_FILE = '.orchestrator/metrics/sessions.jsonl';
|
|
@@ -253,17 +254,18 @@ async function main() {
|
|
|
253
254
|
// by a pre-write copy-to-backup. This eliminates the crash window where the
|
|
254
255
|
// canonical path could be briefly missing if the rename dance were split:
|
|
255
256
|
// 1. copyFileSync: canonical → .bak-<ISO> (backup; canonical still present)
|
|
256
|
-
// 2.
|
|
257
|
-
//
|
|
257
|
+
// 2+3. atomicWriteWithBackup: rewritten → tmp → rename over canonical
|
|
258
|
+
// (atomic replace per POSIX; the helper unlinks its tmp on failure)
|
|
258
259
|
// At every point between steps, the canonical file exists and holds either
|
|
259
260
|
// the old content or the new content. Recovery artifact: .bak-<ISO>.
|
|
260
261
|
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
261
262
|
const backupPath = `${args.file}.bak-${iso}`;
|
|
262
|
-
const tmpPath = join(dirname(args.file), `.${basename(args.file)}.tmp-${process.pid}`);
|
|
263
263
|
try {
|
|
264
264
|
copyFileSync(args.file, backupPath);
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
const res = atomicWriteWithBackup(args.file, rewritten.join('\n') + '\n', {
|
|
266
|
+
tmpPrefix: `.${basename(args.file)}.tmp`,
|
|
267
|
+
});
|
|
268
|
+
if (!res.ok) throw envelopeToError(res);
|
|
267
269
|
summary.backup = backupPath;
|
|
268
270
|
} catch (err) {
|
|
269
271
|
const recoveryHint = existsSync(backupPath)
|
|
@@ -2,22 +2,31 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Install Session Orchestrator through Codex's public plugin lifecycle.
|
|
4
4
|
*
|
|
5
|
+
* The marketplace reads the host-local packed copy (`npm pack` of this clone,
|
|
6
|
+
* with runtime dependencies, under ~/.cache/session-orchestrator/plugin-package),
|
|
7
|
+
* never the clone itself: a marketplace on the working checkout copied all of
|
|
8
|
+
* it into the Codex plugin cache — 15742 files, `.env.local`, `.orchestrator/`
|
|
9
|
+
* and `tests/` included (#1518, measured 2026-10-03).
|
|
10
|
+
*
|
|
5
11
|
* Exit codes:
|
|
6
12
|
* 0 — installed, enabled, and verified
|
|
7
13
|
* 1 — argument or user-correctable environment error
|
|
8
14
|
* 2 — system, upstream command/JSON, or runtime postcondition failure
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
12
19
|
import path from 'node:path';
|
|
13
20
|
import { spawnSync } from 'node:child_process';
|
|
14
21
|
import { fileURLToPath } from 'node:url';
|
|
15
22
|
|
|
16
23
|
import { validateCodexPluginContract } from './lib/codex/plugin-contract.mjs';
|
|
24
|
+
import { resolveStageDir, stagePackage } from './lib/plugin-package-stage.mjs';
|
|
17
25
|
|
|
18
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
27
|
const SCRIPT_DIR = path.dirname(__filename);
|
|
20
28
|
const SO_ROOT = path.dirname(SCRIPT_DIR);
|
|
29
|
+
const STAGE_DIR = resolveStageDir();
|
|
21
30
|
|
|
22
31
|
const MIN_CODEX_VERSION = [0, 144, 4];
|
|
23
32
|
const MARKETPLACE_NAME = 'kanevry';
|
|
@@ -166,27 +175,28 @@ function requireStableFeature(features, name) {
|
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
|
|
169
|
-
function readLocalJson(relativePath, label) {
|
|
178
|
+
function readLocalJson(relativePath, label, root = SO_ROOT) {
|
|
170
179
|
try {
|
|
171
|
-
return JSON.parse(readFileSync(path.join(
|
|
180
|
+
return JSON.parse(readFileSync(path.join(root, relativePath), 'utf8'));
|
|
172
181
|
} catch (error) {
|
|
173
182
|
throw new InstallerError(`Cannot read valid ${label}: ${error.message}`);
|
|
174
183
|
}
|
|
175
184
|
}
|
|
176
185
|
|
|
177
|
-
function readBaseVersion() {
|
|
178
|
-
const packageJson = readLocalJson('package.json', 'package.json');
|
|
186
|
+
function readBaseVersion(root = SO_ROOT) {
|
|
187
|
+
const packageJson = readLocalJson('package.json', 'package.json', root);
|
|
179
188
|
if (typeof packageJson.version !== 'string' || packageJson.version.trim() === '') {
|
|
180
189
|
throw new InstallerError('package.json must contain a non-empty version string.');
|
|
181
190
|
}
|
|
182
191
|
return packageJson.version;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
194
|
+
/** Validates the plugin Codex will actually install: the staged packed copy. */
|
|
195
|
+
function validateLocalContract(root) {
|
|
196
|
+
const expectedBaseVersion = readBaseVersion(root);
|
|
197
|
+
const manifest = readLocalJson(path.join('.codex-plugin', 'plugin.json'), 'Codex plugin manifest', root);
|
|
188
198
|
const verdict = validateCodexPluginContract({
|
|
189
|
-
pluginRoot:
|
|
199
|
+
pluginRoot: root,
|
|
190
200
|
expectedBaseVersion,
|
|
191
201
|
});
|
|
192
202
|
|
|
@@ -210,12 +220,34 @@ function requireArray(value, label) {
|
|
|
210
220
|
return value;
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
const realOrResolved = (p) => { try { return realpathSync(p); } catch { return path.resolve(p); } };
|
|
224
|
+
|
|
213
225
|
function sameLocalPath(left, right) {
|
|
214
|
-
return typeof left === 'string' &&
|
|
226
|
+
return typeof left === 'string' && realOrResolved(left) === realOrResolved(right);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function marketplaceSource(entry) {
|
|
230
|
+
return { sourceType: entry.marketplaceSource?.sourceType, source: entry.marketplaceSource?.source ?? entry.root };
|
|
215
231
|
}
|
|
216
232
|
|
|
217
233
|
function assertMarketplaceCompatible(payload) {
|
|
218
234
|
const marketplaces = requireArray(payload.marketplaces, 'marketplaces');
|
|
235
|
+
// Any local marketplace on the working checkout copies all of it into the
|
|
236
|
+
// plugin cache (#1518) — refuse, naming the one-time re-registration.
|
|
237
|
+
const onCheckout = marketplaces.filter((entry) => {
|
|
238
|
+
if (!isRecord(entry)) return false;
|
|
239
|
+
const { sourceType, source } = marketplaceSource(entry);
|
|
240
|
+
return sourceType === 'local' && sameLocalPath(source, SO_ROOT);
|
|
241
|
+
});
|
|
242
|
+
if (onCheckout.length > 0) {
|
|
243
|
+
const removes = onCheckout.map((entry) => `codex plugin marketplace remove ${String(entry.name)}`).join(' && ');
|
|
244
|
+
throw new InstallerError(
|
|
245
|
+
`Marketplace ${onCheckout.map((entry) => `'${String(entry.name)}'`).join(', ')} reads this clone '${SO_ROOT}' — `
|
|
246
|
+
+ `the Codex plugin cache becomes a full copy incl. .env.local (#1518). Re-register once: `
|
|
247
|
+
+ `${removes} && node scripts/codex-install.mjs (it adds the packed copy at '${STAGE_DIR}').`,
|
|
248
|
+
ERROR_CATEGORIES.USER,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
219
251
|
const matches = marketplaces.filter((entry) => isRecord(entry) && entry.name === MARKETPLACE_NAME);
|
|
220
252
|
|
|
221
253
|
if (matches.length > 1) {
|
|
@@ -227,28 +259,61 @@ function assertMarketplaceCompatible(payload) {
|
|
|
227
259
|
}
|
|
228
260
|
if (matches.length === 0) return;
|
|
229
261
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const source = existing.marketplaceSource?.source ?? existing.root;
|
|
233
|
-
if (sourceType !== 'local' || !sameLocalPath(source, SO_ROOT)) {
|
|
262
|
+
const { sourceType, source } = marketplaceSource(matches[0]);
|
|
263
|
+
if (sourceType !== 'local' || !sameLocalPath(source, STAGE_DIR)) {
|
|
234
264
|
throw new InstallerError(
|
|
235
265
|
`Marketplace '${MARKETPLACE_NAME}' already points to '${String(source)}' (${String(sourceType)}), `
|
|
236
|
-
+ `not '${
|
|
266
|
+
+ `not '${STAGE_DIR}'. Resolve the conflict manually after reviewing `
|
|
237
267
|
+ `'codex plugin marketplace list --json'; this installer will not remove or replace it.`,
|
|
238
268
|
ERROR_CATEGORIES.USER,
|
|
239
269
|
);
|
|
240
270
|
}
|
|
241
271
|
}
|
|
242
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Codex's personal marketplace (`~/.agents/plugins/marketplace.json`, paths
|
|
275
|
+
* relative to the home dir) may list a plugin that resolves to this clone — on
|
|
276
|
+
* the reference host `~/plugins/session-orchestrator` is a symlink to the main
|
|
277
|
+
* checkout, and the resulting `session-orchestrator@local` cache held
|
|
278
|
+
* `.env.local` (#1518). That catalog belongs to the operator, so this is a
|
|
279
|
+
* read-only hint naming the cleanup, never a refusal or an edit.
|
|
280
|
+
*/
|
|
281
|
+
function printPersonalMarketplaceHint(json) {
|
|
282
|
+
const home = os.homedir();
|
|
283
|
+
const catalog = path.join(home, '.agents', 'plugins', 'marketplace.json');
|
|
284
|
+
let plugins;
|
|
285
|
+
try {
|
|
286
|
+
plugins = JSON.parse(readFileSync(catalog, 'utf8')).plugins;
|
|
287
|
+
} catch {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!Array.isArray(plugins)) return;
|
|
291
|
+
const hits = plugins.filter((plugin) => (
|
|
292
|
+
isRecord(plugin) && typeof plugin.source?.path === 'string'
|
|
293
|
+
&& sameLocalPath(path.resolve(home, plugin.source.path), SO_ROOT)
|
|
294
|
+
));
|
|
295
|
+
for (const plugin of hits) {
|
|
296
|
+
const linked = path.resolve(home, plugin.source.path);
|
|
297
|
+
const message = `Hint: ${catalog} lists '${String(plugin.name)}' at '${linked}', which resolves to this clone — `
|
|
298
|
+
+ `its Codex cache copies the whole checkout incl. .env.local (#1518). Cleanup, once: `
|
|
299
|
+
+ `(1) let this installer finish (it removes ${PLUGIN_NAME}@local after a healthy install); `
|
|
300
|
+
+ `(2) remove that entry from ${catalog}; (3) delete the link '${linked}' if it is a symlink; `
|
|
301
|
+
+ `(4) after restarting Codex, delete the old cache folders under ~/.codex/plugins/cache/local/ `
|
|
302
|
+
+ `(${PLUGIN_NAME}/ and any plugin-install-*/ or plugin-backup-*/ holding ${PLUGIN_NAME}).`;
|
|
303
|
+
if (json) process.stderr.write(`${message}\n`);
|
|
304
|
+
else progress(json, message);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
243
308
|
function assertMarketplaceAddResult(payload) {
|
|
244
309
|
if (payload.marketplaceName !== MARKETPLACE_NAME) {
|
|
245
310
|
throw new InstallerError(
|
|
246
311
|
`Marketplace add returned '${String(payload.marketplaceName)}', expected '${MARKETPLACE_NAME}'.`,
|
|
247
312
|
);
|
|
248
313
|
}
|
|
249
|
-
if (!sameLocalPath(payload.installedRoot,
|
|
314
|
+
if (!sameLocalPath(payload.installedRoot, STAGE_DIR)) {
|
|
250
315
|
throw new InstallerError(
|
|
251
|
-
`Marketplace add returned root '${String(payload.installedRoot)}', expected '${
|
|
316
|
+
`Marketplace add returned root '${String(payload.installedRoot)}', expected '${STAGE_DIR}'.`,
|
|
252
317
|
);
|
|
253
318
|
}
|
|
254
319
|
if (typeof payload.alreadyAdded !== 'boolean') {
|
|
@@ -336,18 +401,38 @@ function install({ json }) {
|
|
|
336
401
|
requireStableFeature(features, 'hooks');
|
|
337
402
|
progress(json, 'Features: plugins stable true; hooks stable true');
|
|
338
403
|
|
|
339
|
-
const expectedVersion = validateLocalContract();
|
|
340
|
-
progress(json, `Local contract: valid (${expectedVersion})`);
|
|
341
|
-
|
|
342
404
|
const marketplaceList = runCodexJson(
|
|
343
405
|
['plugin', 'marketplace', 'list', '--json'],
|
|
344
406
|
'Marketplace list',
|
|
345
407
|
);
|
|
408
|
+
// Checked before staging: a refused install must not replace the stage dir.
|
|
346
409
|
assertMarketplaceCompatible(marketplaceList);
|
|
347
410
|
|
|
348
|
-
|
|
411
|
+
printPersonalMarketplaceHint(json);
|
|
412
|
+
|
|
413
|
+
// The contract is checked on the new copy BEFORE it replaces the shared
|
|
414
|
+
// stage: an invalid copy must never become what Claude Code or Codex reads.
|
|
415
|
+
let expectedVersion;
|
|
416
|
+
const staged = stagePackage({
|
|
417
|
+
soRoot: SO_ROOT,
|
|
418
|
+
stageDir: STAGE_DIR,
|
|
419
|
+
installDeps: true,
|
|
420
|
+
log: (line) => progress(json, `Package: ${line}`),
|
|
421
|
+
validate: (dir) => {
|
|
422
|
+
try {
|
|
423
|
+
expectedVersion = validateLocalContract(dir);
|
|
424
|
+
return { ok: true };
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return { ok: false, detail: error.message };
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
if (!staged.ok) throw new InstallerError(`Cannot stage the packed copy: ${staged.detail}`);
|
|
431
|
+
progress(json, `Local contract: valid (${expectedVersion})`);
|
|
432
|
+
|
|
433
|
+
progress(json, `Marketplace: adding '${STAGE_DIR}'`);
|
|
349
434
|
const marketplaceAdd = runCodexJson(
|
|
350
|
-
['plugin', 'marketplace', 'add',
|
|
435
|
+
['plugin', 'marketplace', 'add', STAGE_DIR, '--json'],
|
|
351
436
|
'Marketplace add',
|
|
352
437
|
);
|
|
353
438
|
assertMarketplaceAddResult(marketplaceAdd);
|
|
@@ -399,7 +484,7 @@ function install({ json }) {
|
|
|
399
484
|
ok: true,
|
|
400
485
|
codexVersion: formatVersion(version),
|
|
401
486
|
marketplace: MARKETPLACE_NAME,
|
|
402
|
-
marketplaceSource:
|
|
487
|
+
marketplaceSource: STAGE_DIR,
|
|
403
488
|
marketplaceAlreadyAdded: marketplaceAdd.alreadyAdded,
|
|
404
489
|
pluginId: PLUGIN_ID,
|
|
405
490
|
pluginVersion: expectedVersion,
|
package/scripts/emit-session.mjs
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { existsSync, readFileSync } from 'node:fs';
|
|
31
31
|
import { dirname, join } from 'node:path';
|
|
32
|
+
import { resolveEventsLedgerRoot } from './lib/platform.mjs';
|
|
32
33
|
import { appendJsonl } from './lib/common.mjs';
|
|
33
34
|
import {
|
|
34
35
|
MEMORY_CLEANUP_EVENT,
|
|
@@ -49,6 +50,18 @@ import {
|
|
|
49
50
|
} from './lib/session-schema.mjs';
|
|
50
51
|
import { isMainModule } from './lib/is-main-module.mjs';
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The events ledger beside `sessionsFile` — mapped to the main checkout when the
|
|
55
|
+
* metrics dir sits in a LINKED git worktree (#1514 point 1: one ledger per repo,
|
|
56
|
+
* the one the hooks write). Identity everywhere else, including a tmp fixture.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} sessionsFile
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function siblingEventsFile(sessionsFile) {
|
|
62
|
+
return join(resolveEventsLedgerRoot(dirname(sessionsFile)), 'events.jsonl');
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
export { serializeSessionLineChecked };
|
|
53
66
|
|
|
54
67
|
// A FULL object name only (SHA-1 or SHA-256 repo). A short sha or a symbolic
|
|
@@ -314,7 +327,7 @@ async function main() {
|
|
|
314
327
|
const alreadyStamped =
|
|
315
328
|
typeof repaired.memory_cleanup_at === 'string' && repaired.memory_cleanup_at.length > 0;
|
|
316
329
|
if (!alreadyStamped) {
|
|
317
|
-
const eventsFile =
|
|
330
|
+
const eventsFile = siblingEventsFile(args.file);
|
|
318
331
|
const signal = deriveMemoryCleanupSignal({
|
|
319
332
|
eventsFile,
|
|
320
333
|
sessionId: repaired.session_id,
|
|
@@ -455,7 +468,7 @@ async function main() {
|
|
|
455
468
|
// without one the fallback cannot run, which is exactly when silence hid it.
|
|
456
469
|
if (!hasOwn('session_start_ref')) {
|
|
457
470
|
const headSha = ownUuid !== null
|
|
458
|
-
? readOwnStartHeadSha(
|
|
471
|
+
? readOwnStartHeadSha(siblingEventsFile(args.file), ownUuid)
|
|
459
472
|
: null;
|
|
460
473
|
if (headSha !== null) {
|
|
461
474
|
repaired = { ...repaired, session_start_ref: headSha };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* fleet-auflagen.mjs — read this session's navigator conditions ("Auflagen")
|
|
4
|
+
* and accept them only when they belong to this repo (#1520, navigator#190).
|
|
5
|
+
*
|
|
6
|
+
* Thin CLI over `readAuflagen()` in `scripts/lib/fleet-protocol.mjs`, called by
|
|
7
|
+
* session-start Phase 7.6 after the check-in.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/fleet-auflagen.mjs <session_id>
|
|
11
|
+
*
|
|
12
|
+
* The own repo is `CLAUDE_PROJECT_DIR` (trimmed) or the working directory.
|
|
13
|
+
*
|
|
14
|
+
* stdout: one JSON line — `{ state: 'absent' }`, `{ state: 'accepted', auflagen }`
|
|
15
|
+
* or `{ state: 'rejected', reason }`. A rejected file counts as absent: the
|
|
16
|
+
* Standard-Auflagen apply. On rejection a WARN line goes to stderr.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 — any of the three states (rejection is a result, not an error)
|
|
20
|
+
* 2 — usage error (no session id)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readAuflagen } from './lib/fleet-protocol.mjs';
|
|
24
|
+
import { isMainModule } from './lib/is-main-module.mjs';
|
|
25
|
+
|
|
26
|
+
const PREFIX = 'fleet-auflagen:';
|
|
27
|
+
|
|
28
|
+
/** @returns {Promise<number>} exit code */
|
|
29
|
+
async function main() {
|
|
30
|
+
const sessionId = process.argv[2];
|
|
31
|
+
if (!sessionId) {
|
|
32
|
+
process.stderr.write(`${PREFIX} usage: fleet-auflagen.mjs <session_id>\n`);
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
const repoRoot = (process.env.CLAUDE_PROJECT_DIR || '').trim() || process.cwd();
|
|
36
|
+
const result = await readAuflagen(sessionId, { repoRoot });
|
|
37
|
+
if (result.state === 'rejected') {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`${PREFIX} WARN: auflagen file ignored (${result.reason}) — treated as absent, Standard-Auflagen apply\n`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isMainModule(import.meta.url)) {
|
|
47
|
+
main().then(
|
|
48
|
+
(code) => { process.exitCode = code; },
|
|
49
|
+
(err) => {
|
|
50
|
+
process.stderr.write(`${PREFIX} ${err instanceof Error ? err.message : String(err)}\n`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -54,7 +54,8 @@ import path from 'node:path';
|
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
55
|
import { execFileSync } from 'node:child_process';
|
|
56
56
|
import yaml from 'js-yaml';
|
|
57
|
-
import {
|
|
57
|
+
import { isUserInvocableSkill } from './lib/user-invocable-skills.mjs';
|
|
58
|
+
import { listCommandFiles, listSkillDirs } from './lib/plugin-surface.mjs';
|
|
58
59
|
|
|
59
60
|
const POLICY_MARKER = '# Generated by scripts/generate-agents-skills.mjs; do not edit.\n';
|
|
60
61
|
|
|
@@ -185,7 +186,9 @@ export function renderMirror(skillName, portableFm, canonical = `skills/${skillN
|
|
|
185
186
|
'',
|
|
186
187
|
'Read the linked document in full and follow its complete workflow, including prechecks and stop conditions. Resolve its link relative to this SKILL.md, not the project working directory. The plugin root is three directories above this file. Resolve package paths such as `skills/` and `scripts/` from that root and relative links in the canonical document from its own directory. Keep the user’s project as the target of project operations.',
|
|
187
188
|
];
|
|
188
|
-
|
|
189
|
+
// The source `user-invocable` travels as a string in `metadata`; absent there
|
|
190
|
+
// means absent in the source, which Claude Code reads as invocable (#1515).
|
|
191
|
+
if (command || isUserInvocableSkill(portableFm.metadata ?? {})) lines.push(
|
|
189
192
|
'',
|
|
190
193
|
'`$ARGUMENTS` means the trailing user input after the selected command skill, or an empty string when absent. Preserve flags, quoted text, and Unicode as data. Do not perform global substitution in the command document, shell expansion on the argument string, or execution of that string as shell code. Pass command arguments through structured tool parameters or safely quoted individual arguments.',
|
|
191
194
|
);
|
|
@@ -211,24 +214,14 @@ function readFrontmatter(absPath) {
|
|
|
211
214
|
|
|
212
215
|
/** List `skills/<name>` directories that carry a SKILL.md, sorted. */
|
|
213
216
|
export function listSourceSkills(pluginRoot) {
|
|
214
|
-
|
|
215
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
216
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
217
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_'))
|
|
218
|
-
.map((e) => e.name)
|
|
219
|
-
.filter((n) => existsSync(path.join(dir, n, 'SKILL.md')))
|
|
220
|
-
.sort();
|
|
217
|
+
return listSkillDirs(pluginRoot);
|
|
221
218
|
}
|
|
222
219
|
|
|
223
220
|
/** Union of canonical skills and commands, preserving command precedence. */
|
|
224
221
|
function listSourceEntries(pluginRoot) {
|
|
225
222
|
const sources = new Map(listSourceSkills(pluginRoot).map((name) => [name, `skills/${name}/SKILL.md`]));
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
for (const entry of readdirSync(commands, { withFileTypes: true })) {
|
|
229
|
-
if (!entry.isFile() || !entry.name.endsWith('.md') || /^[._]/.test(entry.name)) continue;
|
|
230
|
-
sources.set(entry.name.slice(0, -3), `commands/${entry.name}`);
|
|
231
|
-
}
|
|
223
|
+
for (const file of listCommandFiles(pluginRoot)) {
|
|
224
|
+
sources.set(file.slice(0, -3), `commands/${file}`);
|
|
232
225
|
}
|
|
233
226
|
return sources;
|
|
234
227
|
}
|
|
@@ -250,7 +243,7 @@ function invocationPolicy(pluginRoot, name, canonical, fm) {
|
|
|
250
243
|
return native.policy.allow_implicit_invocation;
|
|
251
244
|
}
|
|
252
245
|
}
|
|
253
|
-
if (!
|
|
246
|
+
if (!isUserInvocableSkill(fm, canonical)) return undefined;
|
|
254
247
|
}
|
|
255
248
|
if (Object.hasOwn(fm, 'disable-model-invocation') && typeof fm['disable-model-invocation'] !== 'boolean') {
|
|
256
249
|
throw new Error(`${canonical}: disable-model-invocation must be a boolean`);
|
|
@@ -9,7 +9,8 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import yaml from 'js-yaml';
|
|
11
11
|
import { toPortableFrontmatter } from './generate-agents-skills.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { isUserInvocableSkill } from './lib/user-invocable-skills.mjs';
|
|
13
|
+
import { isHiddenSourceName } from './lib/plugin-surface.mjs';
|
|
13
14
|
|
|
14
15
|
const OUTPUT = '.codex-plugin/skills';
|
|
15
16
|
const MARKER = 'Generated by scripts/generate-codex-skills.mjs; do not edit.';
|
|
@@ -56,7 +57,8 @@ function validateFrontmatter(fm, name, path) {
|
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* The repo-wide marker for "operator-facing slash command" — delegated to
|
|
59
|
-
* `
|
|
60
|
+
* `isUserInvocableSkill` (absent key = invocable, #1515; values via the ONE
|
|
61
|
+
* normaliser `isUserInvocableValue`), so this generator, the Cursor and
|
|
60
62
|
* Pi generators and the counters cannot disagree about a quoted or cased value.
|
|
61
63
|
*
|
|
62
64
|
* @param {Record<string, unknown>} fm
|
|
@@ -64,7 +66,7 @@ function validateFrontmatter(fm, name, path) {
|
|
|
64
66
|
* @returns {boolean}
|
|
65
67
|
*/
|
|
66
68
|
function isUserInvocable(fm, path) {
|
|
67
|
-
return
|
|
69
|
+
return isUserInvocableSkill(fm, path);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/** Existing native policy is meaningful; Claude's user-invocable flag is not a substitute. */
|
|
@@ -92,7 +94,7 @@ function readSources(root) {
|
|
|
92
94
|
const directoryPath = join(root, directory);
|
|
93
95
|
if (!existsSync(directoryPath)) continue;
|
|
94
96
|
for (const entry of readdirSync(directoryPath, { withFileTypes: true })) {
|
|
95
|
-
if (
|
|
97
|
+
if (isHiddenSourceName(entry.name)) continue;
|
|
96
98
|
const command = directory === 'commands';
|
|
97
99
|
if (command ? !entry.isFile() || !entry.name.endsWith('.md') : !entry.isDirectory()) continue;
|
|
98
100
|
const name = command ? entry.name.slice(0, -3) : entry.name;
|
|
@@ -106,7 +108,7 @@ function readSources(root) {
|
|
|
106
108
|
catch (error) { throw new Error(`${path}: invalid YAML: ${error.message}`, { cause: error }); }
|
|
107
109
|
validateFrontmatter(fm, name, path);
|
|
108
110
|
// Policy precedence for a skill: a native `agents/openai.yaml` wins; else
|
|
109
|
-
//
|
|
111
|
+
// a user-invocable skill (key `true` or absent; a slash command that used to be a
|
|
110
112
|
// `commands/*.md` file) derives it from `disable-model-invocation` exactly
|
|
111
113
|
// as command sources do; a library skill emits no policy.
|
|
112
114
|
const policy = command
|
|
@@ -161,7 +163,7 @@ function renderSkill(source, warnings) {
|
|
|
161
163
|
'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.',
|
|
162
164
|
];
|
|
163
165
|
// `$ARGUMENTS` semantics belong to every OPERATOR-FACING document — a
|
|
164
|
-
// `commands/*.md`, and equally a skill
|
|
166
|
+
// `commands/*.md`, and equally a skill whose `user-invocable` is `true` (or absent)
|
|
165
167
|
// and references `$ARGUMENTS` in its own body. Gating them on `source.command`
|
|
166
168
|
// alone silently dropped the argument-handling contract from every command
|
|
167
169
|
// body that was folded into its same-named skill.
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
28
28
|
import path from 'node:path';
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
|
-
import { isUserInvocableValue } from './lib/user-invocable-skills.mjs';
|
|
30
|
+
import { isUserInvocableSkill, isUserInvocableValue } from './lib/user-invocable-skills.mjs';
|
|
31
|
+
import { listCommandFiles, listSkillDirs, parseFrontmatter } from './lib/plugin-surface.mjs';
|
|
31
32
|
|
|
32
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
34
|
const SCRIPT_DIR = path.dirname(__filename);
|
|
@@ -46,73 +47,6 @@ function isDir(p) {
|
|
|
46
47
|
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function commandFiles() {
|
|
50
|
-
if (!existsSync(COMMANDS_DIR)) return [];
|
|
51
|
-
return readdirSync(COMMANDS_DIR)
|
|
52
|
-
.filter((name) => name.endsWith('.md'))
|
|
53
|
-
.sort();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function skillDirs() {
|
|
57
|
-
if (!existsSync(SKILLS_DIR)) return [];
|
|
58
|
-
return readdirSync(SKILLS_DIR)
|
|
59
|
-
.filter((name) => isDir(path.join(SKILLS_DIR, name)) && existsSync(path.join(SKILLS_DIR, name, 'SKILL.md')))
|
|
60
|
-
.sort();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Parse YAML-ish frontmatter including `>` / `|` folded scalars.
|
|
65
|
-
*
|
|
66
|
-
* A UTF-8 BOM before the opening `---`, or CRLF line endings, used to make the
|
|
67
|
-
* two probes below miss the block entirely: the file parsed as "no
|
|
68
|
-
* frontmatter", so every flag in it (`user-invocable`,
|
|
69
|
-
* `disable-model-invocation`) silently disappeared and the skill was demoted
|
|
70
|
-
* out of `.cursor/commands/` with no diagnostic anywhere. Both are normalised
|
|
71
|
-
* away first — the same treatment `parseAgentFrontmatter`
|
|
72
|
-
* (`scripts/lib/agent-frontmatter.mjs`) gives them.
|
|
73
|
-
*
|
|
74
|
-
* NOT replaced by the shared `parseSkillFrontmatter`: that one returns the
|
|
75
|
-
* `__BLOCK_SCALAR__` sentinel for a `description: >`, which is the form every
|
|
76
|
-
* merged skill actually uses — routing through it would emit the sentinel as
|
|
77
|
-
* the wrapper description.
|
|
78
|
-
*
|
|
79
|
-
* @param {string} content
|
|
80
|
-
* @returns {Record<string, string>}
|
|
81
|
-
*/
|
|
82
|
-
function parseFrontmatter(content) {
|
|
83
|
-
const text = (content.charCodeAt(0) === 0xfeff ? content.slice(1) : content).replace(/\r\n?/g, '\n');
|
|
84
|
-
if (!text.startsWith('---\n')) return {};
|
|
85
|
-
const end = text.indexOf('\n---\n', 4);
|
|
86
|
-
if (end === -1) return {};
|
|
87
|
-
|
|
88
|
-
const lines = text.slice(4, end).split('\n');
|
|
89
|
-
const fields = {};
|
|
90
|
-
let i = 0;
|
|
91
|
-
while (i < lines.length) {
|
|
92
|
-
const match = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
93
|
-
if (!match) {
|
|
94
|
-
i += 1;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const key = match[1];
|
|
98
|
-
const raw = match[2];
|
|
99
|
-
if (raw === '>' || raw === '| ' || raw === '|' || raw === '>-' || raw === '|-') {
|
|
100
|
-
const folded = [];
|
|
101
|
-
i += 1;
|
|
102
|
-
while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t') || lines[i] === '')) {
|
|
103
|
-
folded.push(lines[i].replace(/^\s+/, ''));
|
|
104
|
-
i += 1;
|
|
105
|
-
}
|
|
106
|
-
const joiner = raw.startsWith('|') ? '\n' : ' ';
|
|
107
|
-
fields[key] = folded.filter(Boolean).join(joiner).trim();
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
fields[key] = raw.replace(/^["']|["']$/g, '');
|
|
111
|
-
i += 1;
|
|
112
|
-
}
|
|
113
|
-
return fields;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
50
|
/**
|
|
117
51
|
* Values that YAML would read as a non-string scalar even though every
|
|
118
52
|
* character in them is "safe". `argument-hint: no` is a boolean, not the
|
|
@@ -194,8 +128,9 @@ Cursor has no Skill tool. When the command says to invoke a skill, Read \`skills
|
|
|
194
128
|
/**
|
|
195
129
|
* Skills that declare themselves operator-facing slash commands.
|
|
196
130
|
*
|
|
197
|
-
* The predicate is `
|
|
198
|
-
*
|
|
131
|
+
* The predicate is `isUserInvocableSkill` from `scripts/lib/user-invocable-skills.mjs`
|
|
132
|
+
* (absent key = invocable, #1515) over the ONE value normaliser `isUserInvocableValue`,
|
|
133
|
+
* shared with the Pi and Codex generators
|
|
199
134
|
* and with every counter. The private copy this replaced read only a
|
|
200
135
|
* whitespace-trimmed bare `true`, so `True` and `true # note` were demoted HERE
|
|
201
136
|
* while the shared counter listed them as commands; the SKILL.md path is passed
|
|
@@ -204,7 +139,7 @@ Cursor has no Skill tool. When the command says to invoke a skill, Read \`skills
|
|
|
204
139
|
* @returns {string[]} skill names, sorted
|
|
205
140
|
*/
|
|
206
141
|
function userInvocableSkills() {
|
|
207
|
-
return
|
|
142
|
+
return listSkillDirs(ROOT).filter((name) => skillFlags(name).userInvocable);
|
|
208
143
|
}
|
|
209
144
|
|
|
210
145
|
/**
|
|
@@ -225,7 +160,7 @@ function skillFlags(skillName) {
|
|
|
225
160
|
if (cached) return cached;
|
|
226
161
|
const file = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
227
162
|
const fields = parseFrontmatter(readFileSync(file, 'utf8'));
|
|
228
|
-
const userInvocable =
|
|
163
|
+
const userInvocable = isUserInvocableSkill(fields, file);
|
|
229
164
|
const sourceDisablesModel = isUserInvocableValue(
|
|
230
165
|
fields['disable-model-invocation'], file, 'disable-model-invocation',
|
|
231
166
|
);
|
|
@@ -375,7 +310,7 @@ Cursor has no Skill tool. Treat "invoke the ${skillName} skill" as: Read \`skill
|
|
|
375
310
|
*/
|
|
376
311
|
function expectedCommands() {
|
|
377
312
|
const commands = new Map();
|
|
378
|
-
for (const commandFile of
|
|
313
|
+
for (const commandFile of listCommandFiles(ROOT)) {
|
|
379
314
|
commands.set(commandFile, renderCommand(commandFile));
|
|
380
315
|
}
|
|
381
316
|
|
|
@@ -401,7 +336,7 @@ function expectedCommands() {
|
|
|
401
336
|
|
|
402
337
|
function expectedSkills() {
|
|
403
338
|
const skills = new Map();
|
|
404
|
-
for (const skillName of
|
|
339
|
+
for (const skillName of listSkillDirs(ROOT)) {
|
|
405
340
|
skills.set(skillName, renderSkill(skillName));
|
|
406
341
|
}
|
|
407
342
|
return skills;
|