session-orchestrator 3.24.0 → 4.0.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 +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* generate-agents-skills.mjs — generate the CROSS-HARNESS PORTABLE SURFACE.
|
|
4
|
+
*
|
|
5
|
+
* 7 of 8 surveyed agent harnesses read `AGENTS.md` and/or a `<dir>/skills/
|
|
6
|
+
* <name>/SKILL.md` layout; only Claude Code reads `CLAUDE.md` + a plugin skill
|
|
7
|
+
* directory (Copilot CLI reads both). Measured 2026-09-06 in this repo:
|
|
8
|
+
* `git ls-files | grep -i AGENTS.md` returned only `agents/AGENTS.md` (the
|
|
9
|
+
* sub-agent authoring spec) — i.e. this repository opened in Codex CLI,
|
|
10
|
+
* OpenCode, Kiro, Amp or Cursor could not find its own `## Session Config`.
|
|
11
|
+
*
|
|
12
|
+
* This generator writes two artefacts, both DERIVED and never hand-edited:
|
|
13
|
+
*
|
|
14
|
+
* 1. `AGENTS.md` — a BYTE-IDENTICAL copy of `CLAUDE.md`.
|
|
15
|
+
* 2. `.agents/skills/<name>/SKILL.md` — a portable mirror of each
|
|
16
|
+
* `skills/<name>/SKILL.md`, carrying ONLY agentskills.io-spec-legal
|
|
17
|
+
* frontmatter plus a pointer body (progressive disclosure — the mirror
|
|
18
|
+
* never duplicates the canonical instructions).
|
|
19
|
+
*
|
|
20
|
+
* ## Why a generated copy and NOT a symlink (measured, not preferred)
|
|
21
|
+
*
|
|
22
|
+
* A symlink `AGENTS.md -> CLAUDE.md` is the smaller diff, and it was rejected
|
|
23
|
+
* on three measurements taken 2026-09-06:
|
|
24
|
+
*
|
|
25
|
+
* - `package.json` `files[]` does NOT list `CLAUDE.md`, so the published npm
|
|
26
|
+
* tarball would carry a DANGLING symlink. The published artefact is the
|
|
27
|
+
* one a foreign harness consumes.
|
|
28
|
+
* - Git's `core.symlinks` defaults to FALSE on Windows without Developer
|
|
29
|
+
* Mode / admin (`git config core.symlinks` is unset here — exit 1 — i.e.
|
|
30
|
+
* the platform default applies). On such a checkout git materialises the
|
|
31
|
+
* link as a regular file whose entire content is the literal target path
|
|
32
|
+
* `CLAUDE.md` — a 10-byte file with zero Session Config, which is exactly
|
|
33
|
+
* the "pointer file that defeats the purpose" failure mode. The same
|
|
34
|
+
* applies to GitHub's Download-ZIP archives.
|
|
35
|
+
* - `git ls-files -s | awk '$1=="120000"'` returned ZERO rows: this repo has
|
|
36
|
+
* no symlink precedent, and it is itself a template other repos copy.
|
|
37
|
+
*
|
|
38
|
+
* A generated copy costs a drift gate, which `--check` provides and
|
|
39
|
+
* `scripts/validate-plugin.mjs` runs. It carries NO "do not edit" banner on
|
|
40
|
+
* purpose: byte-identity makes the drift-check's whole error class ("the two
|
|
41
|
+
* instruction files disagree") impossible to express, and the `--check`
|
|
42
|
+
* failure message carries the do-not-edit instruction at the moment it
|
|
43
|
+
* matters. See `skills/_shared/instruction-file-resolution.md`.
|
|
44
|
+
*
|
|
45
|
+
* ## Usage
|
|
46
|
+
* node scripts/generate-agents-skills.mjs [--plugin-root <dir>] [--check] [--json]
|
|
47
|
+
*
|
|
48
|
+
* Exit codes: 0 — written / in sync · 1 — drift (with `--check`) or write error.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { readFileSync, writeFileSync, existsSync, statSync, lstatSync, mkdirSync, readdirSync, rmSync, realpathSync } from 'node:fs';
|
|
52
|
+
import path from 'node:path';
|
|
53
|
+
import { fileURLToPath } from 'node:url';
|
|
54
|
+
import { execFileSync } from 'node:child_process';
|
|
55
|
+
import yaml from 'js-yaml';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The complete set of SKILL.md frontmatter keys agentskills.io permits outside
|
|
59
|
+
* Claude Code. Anything else is folded into `metadata` (as a string) or dropped.
|
|
60
|
+
* @type {readonly string[]}
|
|
61
|
+
*/
|
|
62
|
+
export const PORTABLE_KEYS = Object.freeze([
|
|
63
|
+
'name', 'description', 'license', 'compatibility', 'metadata', 'allowed-tools',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Source keys that map onto a spec key rather than into `metadata`.
|
|
68
|
+
* `tools:` is our own (non-spec) spelling of `allowed-tools:` — 7 SKILL.md
|
|
69
|
+
* files use it, measured 2026-09-06.
|
|
70
|
+
* @type {Record<string, string>}
|
|
71
|
+
*/
|
|
72
|
+
const KEY_ALIASES = { tools: 'allowed-tools' };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* agentskills.io caps `description` at 1024 characters. Measured max across the
|
|
76
|
+
* 43 source skills on 2026-09-06: 1012 (`sunset-review`) — i.e. nothing is
|
|
77
|
+
* truncated today. Revisit trigger: if this generator starts reporting
|
|
78
|
+
* `description-truncated` warnings, shorten the SOURCE description instead of
|
|
79
|
+
* raising this constant; the cap is the spec's, not ours.
|
|
80
|
+
*/
|
|
81
|
+
export const DESCRIPTION_MAX = 1024;
|
|
82
|
+
|
|
83
|
+
/** Stringify a YAML scalar/array/object for the string-valued `metadata` map. */
|
|
84
|
+
function toMetadataString(value) {
|
|
85
|
+
if (value === null || value === undefined) return '';
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value.every((v) => typeof v !== 'object' || v === null)
|
|
88
|
+
? value.map((v) => String(v)).join(', ')
|
|
89
|
+
: JSON.stringify(value);
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
92
|
+
return String(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Project one source SKILL.md frontmatter object onto the spec-legal subset.
|
|
97
|
+
*
|
|
98
|
+
* @param {Record<string, unknown>} source - parsed source frontmatter
|
|
99
|
+
* @param {string} skillName - directory name, used when `name:` is absent
|
|
100
|
+
* @returns {{ fm: Record<string, unknown>, warnings: string[] }}
|
|
101
|
+
*/
|
|
102
|
+
export function toPortableFrontmatter(source, skillName) {
|
|
103
|
+
const warnings = [];
|
|
104
|
+
/** @type {Record<string, unknown>} */
|
|
105
|
+
const out = {};
|
|
106
|
+
/** @type {Record<string, string>} */
|
|
107
|
+
const metadata = {};
|
|
108
|
+
|
|
109
|
+
out.name = typeof source.name === 'string' && source.name.trim() ? source.name.trim() : skillName;
|
|
110
|
+
|
|
111
|
+
// Descriptions are authored as YAML block scalars (`description: >`), so they
|
|
112
|
+
// arrive with embedded newlines. Collapse to one line: the spec's field is a
|
|
113
|
+
// single-line summary and a folded scalar round-trips unpredictably.
|
|
114
|
+
const desc = String(source.description ?? '').replace(/\s+/g, ' ').trim();
|
|
115
|
+
if (!desc) warnings.push(`${skillName}: source has no description`);
|
|
116
|
+
out.description = desc.length > DESCRIPTION_MAX
|
|
117
|
+
? (warnings.push(`${skillName}: description-truncated (${desc.length} > ${DESCRIPTION_MAX})`),
|
|
118
|
+
`${desc.slice(0, DESCRIPTION_MAX - 1)}…`)
|
|
119
|
+
: desc;
|
|
120
|
+
|
|
121
|
+
for (const [rawKey, value] of Object.entries(source)) {
|
|
122
|
+
if (rawKey === 'name' || rawKey === 'description') continue;
|
|
123
|
+
const key = KEY_ALIASES[rawKey] ?? rawKey;
|
|
124
|
+
if (key === 'allowed-tools') {
|
|
125
|
+
const list = Array.isArray(value)
|
|
126
|
+
? value.map((v) => String(v).trim())
|
|
127
|
+
: String(value).split(',').map((v) => v.trim());
|
|
128
|
+
const tools = list.filter(Boolean);
|
|
129
|
+
if (tools.length > 0) out['allowed-tools'] = tools;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (key === 'license' || key === 'compatibility') {
|
|
133
|
+
out[key] = value;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (key === 'metadata' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
137
|
+
for (const [mk, mv] of Object.entries(value)) metadata[mk] = toMetadataString(mv);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const asString = toMetadataString(value);
|
|
141
|
+
if (asString !== '') metadata[rawKey] = asString;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (Object.keys(metadata).length > 0) out.metadata = metadata;
|
|
145
|
+
|
|
146
|
+
// Emit in the spec's own field order so the mirror reads the same everywhere.
|
|
147
|
+
/** @type {Record<string, unknown>} */
|
|
148
|
+
const ordered = {};
|
|
149
|
+
for (const k of PORTABLE_KEYS) if (k in out) ordered[k] = out[k];
|
|
150
|
+
return { fm: ordered, warnings };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render one mirror file: spec-legal frontmatter + a pointer body.
|
|
155
|
+
*
|
|
156
|
+
* The body deliberately does NOT restate the canonical instructions
|
|
157
|
+
* (progressive disclosure): a foreign harness discovers the skill from the
|
|
158
|
+
* frontmatter and reads the real body only when it invokes the skill.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} skillName
|
|
161
|
+
* @param {Record<string, unknown>} portableFm
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
export function renderMirror(skillName, portableFm) {
|
|
165
|
+
const front = yaml.dump(portableFm, { lineWidth: -1, noRefs: true, quotingType: '"' });
|
|
166
|
+
const canonical = `skills/${skillName}/SKILL.md`;
|
|
167
|
+
return [
|
|
168
|
+
'---',
|
|
169
|
+
front.trimEnd(),
|
|
170
|
+
'---',
|
|
171
|
+
'',
|
|
172
|
+
`# ${skillName}`,
|
|
173
|
+
'',
|
|
174
|
+
`> **Portable mirror — generated, do not edit.** The canonical skill body lives at`,
|
|
175
|
+
`> [\`${canonical}\`](../../../${canonical}); read that file for the full instructions.`,
|
|
176
|
+
`> This mirror carries only agentskills.io-spec-legal frontmatter so harnesses that`,
|
|
177
|
+
`> discover skills under \`.agents/skills/\` can find and route to the skill.`,
|
|
178
|
+
'>',
|
|
179
|
+
'> Regenerate with `node scripts/generate-agents-skills.mjs`.',
|
|
180
|
+
'',
|
|
181
|
+
].join('\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Parse a SKILL.md's YAML frontmatter block; returns `{}` when absent/unparseable. */
|
|
185
|
+
function readFrontmatter(absPath) {
|
|
186
|
+
const content = readFileSync(absPath, 'utf8');
|
|
187
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
188
|
+
if (!m) return null;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = yaml.load(m[1]);
|
|
191
|
+
return parsed && typeof parsed === 'object' ? /** @type {Record<string, unknown>} */ (parsed) : {};
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** List `skills/<name>` directories that carry a SKILL.md, sorted. */
|
|
198
|
+
export function listSourceSkills(pluginRoot) {
|
|
199
|
+
const dir = path.join(pluginRoot, 'skills');
|
|
200
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
201
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
202
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_'))
|
|
203
|
+
.map((e) => e.name)
|
|
204
|
+
.filter((n) => existsSync(path.join(dir, n, 'SKILL.md')))
|
|
205
|
+
.sort();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Is `AGENTS.md` already an alias of `CLAUDE.md` by construction?
|
|
210
|
+
*
|
|
211
|
+
* Two shapes count as satisfied: a symlink (or hardlink) resolving to the same
|
|
212
|
+
* inode, and a byte-identical regular file. The symlink branch exists because a
|
|
213
|
+
* CONSUMER repo may legitimately choose it even though this repo does not (see
|
|
214
|
+
* the header) — the invariant is "the two files cannot disagree", not "the two
|
|
215
|
+
* files were produced by this generator".
|
|
216
|
+
*
|
|
217
|
+
* @param {string} claudePath
|
|
218
|
+
* @param {string} agentsPath
|
|
219
|
+
* @returns {{ satisfied: boolean, kind: 'absent'|'symlink'|'identical'|'divergent' }}
|
|
220
|
+
*/
|
|
221
|
+
export function agentsMdAliasState(claudePath, agentsPath) {
|
|
222
|
+
if (!existsSync(claudePath)) return { satisfied: false, kind: 'absent' };
|
|
223
|
+
if (!existsSync(agentsPath)) return { satisfied: false, kind: 'absent' };
|
|
224
|
+
try {
|
|
225
|
+
if (lstatSync(agentsPath).isSymbolicLink()) {
|
|
226
|
+
if (realpathSync(agentsPath) === realpathSync(claudePath)) return { satisfied: true, kind: 'symlink' };
|
|
227
|
+
return { satisfied: false, kind: 'divergent' };
|
|
228
|
+
}
|
|
229
|
+
const a = statSync(agentsPath);
|
|
230
|
+
const c = statSync(claudePath);
|
|
231
|
+
if (a.ino !== 0 && a.ino === c.ino && a.dev === c.dev) return { satisfied: true, kind: 'symlink' };
|
|
232
|
+
} catch {
|
|
233
|
+
// fall through to a byte comparison
|
|
234
|
+
}
|
|
235
|
+
const same = readFileSync(agentsPath).equals(readFileSync(claudePath));
|
|
236
|
+
return { satisfied: same, kind: same ? 'identical' : 'divergent' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate (or, with `check`, verify) the whole portable surface.
|
|
241
|
+
*
|
|
242
|
+
* @param {{ pluginRoot: string, check?: boolean }} opts
|
|
243
|
+
* @returns {{ ok: boolean, drift: string[], warnings: string[], written: string[], files: number, bytes: number, skills: string[] }}
|
|
244
|
+
*/
|
|
245
|
+
export function generateAgentsSurface({ pluginRoot, check = false }) {
|
|
246
|
+
const drift = [];
|
|
247
|
+
const warnings = [];
|
|
248
|
+
const written = [];
|
|
249
|
+
|
|
250
|
+
// --- Artefact 1: root AGENTS.md ------------------------------------------
|
|
251
|
+
const claudePath = path.join(pluginRoot, 'CLAUDE.md');
|
|
252
|
+
const agentsPath = path.join(pluginRoot, 'AGENTS.md');
|
|
253
|
+
if (existsSync(claudePath)) {
|
|
254
|
+
const state = agentsMdAliasState(claudePath, agentsPath);
|
|
255
|
+
if (!state.satisfied) {
|
|
256
|
+
if (check) {
|
|
257
|
+
drift.push(state.kind === 'absent'
|
|
258
|
+
? 'AGENTS.md is missing (foreign harnesses — Codex CLI, OpenCode, Kiro, Amp — read AGENTS.md and never CLAUDE.md)'
|
|
259
|
+
: 'AGENTS.md differs from CLAUDE.md — AGENTS.md is GENERATED; put the change in CLAUDE.md, never in AGENTS.md');
|
|
260
|
+
} else {
|
|
261
|
+
writeFileSync(agentsPath, readFileSync(claudePath));
|
|
262
|
+
written.push('AGENTS.md');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
warnings.push('CLAUDE.md not found — AGENTS.md not generated');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- Artefact 2: .agents/skills/<name>/SKILL.md ---------------------------
|
|
270
|
+
const mirrorRoot = path.join(pluginRoot, '.agents', 'skills');
|
|
271
|
+
const skills = listSourceSkills(pluginRoot);
|
|
272
|
+
let files = 0;
|
|
273
|
+
let bytes = 0;
|
|
274
|
+
|
|
275
|
+
for (const name of skills) {
|
|
276
|
+
const src = path.join(pluginRoot, 'skills', name, 'SKILL.md');
|
|
277
|
+
const fm = readFrontmatter(src);
|
|
278
|
+
if (fm === null) {
|
|
279
|
+
warnings.push(`${name}: SKILL.md has no parseable frontmatter — skipped`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const { fm: portable, warnings: w } = toPortableFrontmatter(fm, name);
|
|
283
|
+
warnings.push(...w);
|
|
284
|
+
const rendered = renderMirror(name, portable);
|
|
285
|
+
const dest = path.join(mirrorRoot, name, 'SKILL.md');
|
|
286
|
+
files += 1;
|
|
287
|
+
bytes += Buffer.byteLength(rendered, 'utf8');
|
|
288
|
+
|
|
289
|
+
const current = existsSync(dest) ? readFileSync(dest, 'utf8') : null;
|
|
290
|
+
if (current === rendered) continue;
|
|
291
|
+
if (check) {
|
|
292
|
+
drift.push(current === null
|
|
293
|
+
? `.agents/skills/${name}/SKILL.md is missing`
|
|
294
|
+
: `.agents/skills/${name}/SKILL.md is stale`);
|
|
295
|
+
} else {
|
|
296
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
297
|
+
writeFileSync(dest, rendered, 'utf8');
|
|
298
|
+
written.push(`.agents/skills/${name}/SKILL.md`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Orphans: a mirror whose source skill was renamed or removed. Left behind,
|
|
303
|
+
// it advertises a skill no harness can route to.
|
|
304
|
+
if (existsSync(mirrorRoot)) {
|
|
305
|
+
const known = new Set(skills);
|
|
306
|
+
for (const entry of readdirSync(mirrorRoot, { withFileTypes: true })) {
|
|
307
|
+
if (!entry.isDirectory() || known.has(entry.name)) continue;
|
|
308
|
+
if (check) {
|
|
309
|
+
drift.push(`.agents/skills/${entry.name}/ is an orphan (no skills/${entry.name}/SKILL.md)`);
|
|
310
|
+
} else {
|
|
311
|
+
rmSync(path.join(mirrorRoot, entry.name), { recursive: true, force: true });
|
|
312
|
+
written.push(`removed .agents/skills/${entry.name}/`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { ok: drift.length === 0, drift, warnings, written, files, bytes, skills };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// CLI
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
/** Resolve the repo root the same way validate-plugin.mjs does. */
|
|
325
|
+
function defaultRoot() {
|
|
326
|
+
try {
|
|
327
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
|
|
328
|
+
} catch {
|
|
329
|
+
return process.cwd();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function main(argv) {
|
|
334
|
+
const args = { check: false, json: false, pluginRoot: null };
|
|
335
|
+
for (let i = 0; i < argv.length; i++) {
|
|
336
|
+
if (argv[i] === '--check') args.check = true;
|
|
337
|
+
else if (argv[i] === '--json') args.json = true;
|
|
338
|
+
else if (argv[i] === '--plugin-root') args.pluginRoot = argv[++i];
|
|
339
|
+
else if (argv[i] === '--help' || argv[i] === '-h') {
|
|
340
|
+
process.stdout.write('Usage: generate-agents-skills.mjs [--plugin-root <dir>] [--check] [--json]\n');
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const pluginRoot = path.resolve(args.pluginRoot ?? defaultRoot());
|
|
345
|
+
const result = generateAgentsSurface({ pluginRoot, check: args.check });
|
|
346
|
+
|
|
347
|
+
if (args.json) {
|
|
348
|
+
process.stdout.write(JSON.stringify({ ...result, plugin_root: pluginRoot }) + '\n');
|
|
349
|
+
} else if (args.check) {
|
|
350
|
+
for (const w of result.warnings) process.stderr.write(` WARN: agents-surface: ${w}\n`);
|
|
351
|
+
if (result.ok) {
|
|
352
|
+
process.stdout.write(`agents-surface: ${result.files + 1} artefact(s), in sync\n`);
|
|
353
|
+
} else {
|
|
354
|
+
for (const d of result.drift) process.stderr.write(`✗ agents-surface: ${d}\n`);
|
|
355
|
+
process.stderr.write(' Remedy: node scripts/generate-agents-skills.mjs\n');
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
for (const w of result.warnings) process.stderr.write(` WARN: agents-surface: ${w}\n`);
|
|
359
|
+
process.stdout.write(
|
|
360
|
+
`agents-surface: ${result.written.length} file(s) written, ${result.files} skill mirror(s), ${result.bytes} bytes\n`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return result.ok ? 0 : 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// realpathSync on both sides: a spawn whose path traverses a symlink (macOS
|
|
367
|
+
// /tmp -> /private/tmp) otherwise makes this whole block a silent no-op that
|
|
368
|
+
// still exits 0 — the failure mode documented in validate-plugin.mjs.
|
|
369
|
+
const invokedDirectly = (() => {
|
|
370
|
+
if (!process.argv[1]) return false;
|
|
371
|
+
try {
|
|
372
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
})();
|
|
377
|
+
|
|
378
|
+
if (invokedDirectly) process.exit(main(process.argv.slice(2)));
|
|
@@ -80,12 +80,40 @@ function parseFrontmatter(content) {
|
|
|
80
80
|
return fields;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Values that YAML would read as a non-string scalar even though every
|
|
85
|
+
* character in them is "safe". `argument-hint: no` is a boolean, not the
|
|
86
|
+
* string "no"; `description: 3` is an integer. A consumer asserting
|
|
87
|
+
* `typeof value === 'string'` fails on a file that looks perfectly fine.
|
|
88
|
+
*/
|
|
89
|
+
const YAML_SCALAR_LOOKALIKE = /^(?:true|false|yes|no|on|off|null|nan|[-+]?\.?inf|~)$/i;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A value safe to emit as a YAML *plain* (unquoted) scalar.
|
|
93
|
+
*
|
|
94
|
+
* Deliberately an ALLOW-list. Its predecessor was a deny-list of special
|
|
95
|
+
* characters, which is unenumerable by construction: it caught `[` and `:`
|
|
96
|
+
* but not a leading `-` (block-sequence entry), not surrounding whitespace,
|
|
97
|
+
* and not the scalar look-alikes above. An allow-list fails closed — an
|
|
98
|
+
* unforeseen shape gets quoted, which is never wrong, only noisier.
|
|
99
|
+
*/
|
|
100
|
+
const YAML_PLAIN_SAFE = /^[A-Za-z0-9_][A-Za-z0-9 _.,()/-]*$/;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render a frontmatter VALUE as YAML.
|
|
104
|
+
*
|
|
105
|
+
* @param {unknown} value
|
|
106
|
+
* @returns {string|null} the YAML scalar, or `null` when there is nothing to emit
|
|
107
|
+
*/
|
|
83
108
|
function yamlQuote(value) {
|
|
84
|
-
if (value === undefined || value === '') return null;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return
|
|
109
|
+
if (value === undefined || value === null || value === '') return null;
|
|
110
|
+
const text = String(value);
|
|
111
|
+
if (text.trim() === '') return null;
|
|
112
|
+
if (text !== text.trim()) return JSON.stringify(text);
|
|
113
|
+
if (!YAML_PLAIN_SAFE.test(text)) return JSON.stringify(text);
|
|
114
|
+
if (YAML_SCALAR_LOOKALIKE.test(text)) return JSON.stringify(text);
|
|
115
|
+
if (/^\d/.test(text)) return JSON.stringify(text);
|
|
116
|
+
return text;
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
function frontmatterLine(key, value) {
|
|
@@ -103,10 +131,16 @@ function renderCommand(commandFile) {
|
|
|
103
131
|
const commandPath = path.join(COMMANDS_DIR, commandFile);
|
|
104
132
|
const commandName = commandFile.replace(/\.md$/, '');
|
|
105
133
|
const fields = parseFrontmatter(readFileSync(commandPath, 'utf8'));
|
|
134
|
+
// EVERY value goes through yamlQuote(). `argument-hint` did not, and its
|
|
135
|
+
// canonical form is a bare bracket list (`[mode] [--flag]`) — a YAML flow
|
|
136
|
+
// sequence, which is GH#54: Copilot CLI >= 1.0.65 silently DROPS a command
|
|
137
|
+
// file whose `argument-hint` is not a string. Fixed in `commands/` and
|
|
138
|
+
// `pi/prompts/` by 93b40dd (v3.16.0) and re-introduced here on every
|
|
139
|
+
// generation, because this mirror re-derived the frontmatter by hand.
|
|
106
140
|
const frontmatter = [
|
|
107
141
|
'---',
|
|
108
|
-
frontmatterLine('description', yamlQuote(fields.description)
|
|
109
|
-
frontmatterLine('argument-hint', fields['argument-hint']),
|
|
142
|
+
frontmatterLine('description', yamlQuote(fields.description)),
|
|
143
|
+
frontmatterLine('argument-hint', yamlQuote(fields['argument-hint'])),
|
|
110
144
|
'---',
|
|
111
145
|
].filter(Boolean).join('\n');
|
|
112
146
|
|
|
@@ -132,9 +166,12 @@ function renderSkill(skillName) {
|
|
|
132
166
|
const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
|
|
133
167
|
const fields = parseFrontmatter(readFileSync(skillPath, 'utf8'));
|
|
134
168
|
const description = clampDescription(fields.description || `Session Orchestrator skill: ${skillName}`);
|
|
169
|
+
// Same rule as renderCommand: no frontmatter value is emitted raw. `name` is
|
|
170
|
+
// a directory basename today, so it is plain-safe in practice — routing it
|
|
171
|
+
// through yamlQuote() is what keeps that true after the next skill is added.
|
|
135
172
|
const lines = [
|
|
136
173
|
'---',
|
|
137
|
-
`name: ${skillName}`,
|
|
174
|
+
`name: ${yamlQuote(skillName)}`,
|
|
138
175
|
`description: ${yamlQuote(description)}`,
|
|
139
176
|
];
|
|
140
177
|
if (!isUserInvocable(fields['user-invocable'])) {
|