Uh oh!
There was an error while loading. Please reload this page.
feat(cli): os g skill scaffolds an AI skill as NAME.skill.ts, the name the loader looks for - #11054
Conversation
Second half of the ADR-0063 Option A ruling whose first half retired `os g agent` (#10359). That retirement told authors to write src/skills/NAME.skill.ts by hand because no scaffolder existed; this adds one, and the refusal message, the CLI README and the CLI docs now name the command instead of apologising for its absence. The filename is the point. DEFAULT_METADATA_TYPE_REGISTRY declares skill's file convention as *.skill.ts / *.skill.yml while this harness has always written NAME.ts, and skill is allowRuntimeCreate: true — a type the platform expects to discover. A scaffold matching no pattern would type-check, validate and publish with nothing reporting that it was skipped: the silent-strip shape the agent retirement closed, re-entering through the scaffolder that replaced it. So skill overrides the harness filename through a new per-generator hook, and the barrel re-export is derived from the file actually written rather than rebuilt from the metadata name. The other six generators are unchanged and still write NAME.ts with a './NAME' barrel line, pinned by a control assertion in the new test. Converging the whole scaffolder on the registry's NAME.TYPE.ts convention is a repo-wide decision and is deliberately not taken here. Template decisions, each with its reasoning written into the generated file: tools: [] (ADR-0064 removed the global fall-through, so an empty list grants nothing, while a placeholder name resolves to nothing and is reported as ai-skill-tool-unresolved); surface: 'ask' written out rather than left to the schema default, because the affinity is enforced at load and a silent default is invisible to the next author; and defineSkill rather than a bare typed literal, so the object is parsed at module load. Not copied from SkillSchema or defineSkill's @example blocks — both pass triggerPhrases, a retired-key tombstone that rejects on parse (#11026). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3649bb31c730f86e01063db045cfa0117ef4b5ba && git checkout 3649bb31c730f86e01063db045cfa0117ef4b5ba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 072d072d2a74e358e923f6f6a76acbf7621305c3 6a8fbbc43176de9c474fef964c140057b5d69855 && git checkout -B drift-repro 072d072d2a74e358e923f6f6a76acbf7621305c3 && git merge --no-ff 6a8fbbc43176de9c474fef964c140057b5d69855
node scripts/docs-audit/affected-docs.mjs --json 072d072d2a74e358e923f6f6a76acbf7621305c3
|
…folder exists (#11025) `.changeset/retire-agent-generator.md` is unconsumed, so it compiles into the same release notes as this card's own changeset. It quoted the `os g agent` refusal verbatim, and this card changed that output; it also stated "there is no command to rewrite the call into", "`os g skill` does not exist yet", and a six-entry generator roster. All three ship as customer-facing text in the release that adds the seventh. Bounded in-place repair, same defect class as the card (the sentences that go stale), mechanical, no other claim on the file, and inside the changeset gate family this diff already runs. The quoted block is now the output the command actually prints, measured. The ADR-0087 disposition is unchanged — a CLI command name is still not authorable metadata and there is still no FROM -> TO rewrite to prescribe, since a skill is a different artifact and not a renamed agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32580494827 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#11025
Closes#10359 — this is the second half of that card's Option A ruling; the first half (retiring
os g agent) landed as #11028, and the ruling's remaining obligation was this scaffolder.What changes
os g skill NAMEscaffolds an AI skill intosrc/skills/NAME.skill.ts. Theagentretirement message, the CLI README and the CLI docs previously told authors to write that file by hand because no scaffolder existed; all three now name the command.The filename is the point, not a detail
DEFAULT_METADATA_TYPE_REGISTRYdeclaresskill's file convention as*.skill.ts/*.skill.yml. This harness has always writtenNAME.ts.skillisallowRuntimeCreate: true— a type the platform expects to discover — and a file matching neither pattern still type-checks, still passesos validateand still publishes, with nothing anywhere reporting that it was skipped. That is the silent-strip shape theagentretirement closed, re-entering through the scaffolder that replaced it, which is why a template alone would have been worse than no generator.Mechanism chosen: an optional per-generator
fileNamehook, set byskillalone, with the divergence and the reason it was not resolved the other way written into the code. The barrel re-export is now derived from the file actually written rather than rebuilt from the metadata name — for the six generators that take the default it produces a byte-identical line.⛔ The other six generators are unchanged. Converging the whole scaffolder on the registry's
NAME.TYPE.tsconvention — the shape the example apps already author in — moves every generator's output plus the docs and examples that show it. It is a repo-wide decision and is deliberately not taken here. A control assertion in the new test pins thatos g objectstill writescustomer.tswith a'./customer'barrel line.The three authoring decisions, and their reasons
tools(required, no default)tools: [], with a commented exampleos validatereportsai-skill-tool-unresolved, and the runtime drops the reference while the instructions keep promising the capability.surface(schema default'ask')surface: 'ask'defineSkillSkillSchema's ordefineSkill's@exampleblocks — both passtriggerPhrases, a retired-key tombstone that rejects on parse (#11026).defineSkillrather than a bare typed literal so the object is parsed at module load, which is what makes the pin below possible.Each reason is written into the generated file, so the author inherits the reasoning and not just the value.
Non-vacuity — how the pins were proven
A test asserting "the command wrote a file" passes even when the filename matches no pattern, which IS the defect. So the new pin reads the patterns out of
DEFAULT_METADATA_TYPE_REGISTRYat test time (never restated), discovers the written file by listing the output directory (never assumed), and matches withnode:path's ownmatchesGlob(never a hand-rolled matcher). The parse pin imports the generated module in a child process — the template callsdefineSkillat module scope, so the import ISSkillSchema.parse.Three ablations, each with the direction predicted before running:
skillfilename override1 failed / 11 passed. The failure message:generated "src/skills/lead_qualification.ts" matches none of ["**/*.skill.ts","**/*.skill.yml"]. "writes exactly one skill file" passed, as predicted.triggerPhrasesto the template3 failed / 9 passed, the parse pin's message namingZodError. Measured directly too: the probe exits 1 withpath: ["triggerPhrases"]and the tombstone's own text, thrown fromdefineSkill.1 failed / 11 passed: expected the barrel to containfrom './lead_qualification.skill', actual'./lead_qualification'.12 passed (12).No rebuild leg is needed here, and that is a property of this pair rather than an omission:
bin/run-dev.jsrunspackages/cli/srcthroughtsx, so the mutated subject IS the source the spawned CLI executes — nodistsits between the edit and the measurement. The workspace closure the test's own@objectstack/spec/kernelimport resolves through was built before every leg.Also exercised as a user would
The generated file imports cleanly, and the barrel line reads
from './lead_qualification.skill'.The second commit edits
.changeset/retire-agent-generator.md, which belongs to the already-merged #11028 and is unconsumed — so it compiles into the same release notes as this card's own changeset. It quoted theos g agentrefusal verbatim (output this card changed), stated "there is no command to rewrite the call into" and "os g skilldoes not exist yet", and listed a six-entry generator roster. All of that ships as customer-facing text in the release that adds the seventh entry, so the release notes would have contradicted themselves in adjacent bullets.The quoted block is now the output the command actually prints, captured by running it. The ADR-0087 disposition is unchanged — a CLI command name is still not authorable metadata and there is still no FROM -> TO rewrite to prescribe, since a skill is a different artifact and not a renamed agent; only the one clause asserting no replacement command exists was corrected.
check-adr-0087-registrationre-run and green over the edited file.It qualifies as a bounded in-place repair on all four counts: same defect class as this card (the sentences that go stale), mechanical with the correct text measured rather than invented, no other claim on the file (#11028 is merged and nothing open touches it), and entirely inside the changeset gate family this diff already runs. Declaring it here is what makes it reviewable; ⛔ nothing else outside the stated surface was touched.
Scope note
⛔ Repo-root
skills/is untouched — it is the published authoring-skill catalog, a different thing fromdefineSkillagent skills. Nothing undercontent/docs/releases/. The@capabilitiesdocs are untouched.packages/specis read as evidence and not edited, so the Clause-② path limb does not fire.Verification
Gate family derived with
node scripts/pm/dispatch-gates.mjsand no path arguments, re-derived after the second commit (8 paths off merge base2866d5f97): the gate SET is identical, only the "matched via" attributions gained the new changeset path. Exit codes captured before any pipe; verdict lines quoted from the gates themselves.34 gates +
pnpm lint+ the full package suite at26bbdac873, every one exit 0, and the ratchet family plus the five changeset gates re-run at the current head6a8fbbc431, all exit 0. The results that carry information:check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 272.3s, 1908 raw tsc error(s) total, none above its recorded number.(at6a8fbbc431; 264.5s at the first head, same totals) — the ratchet ran for real against the built workspace closure (turbo run build, 70/70 successful) rather than refusing.check-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger, 1 exempt.check-nul-bytes: OK (scanned 6391 text file(s) ... no raw ASCII control bytes).check-engine-double-contract: OK — 377 pinned, 133 in the DEBT ledger, 2 exempt·where-matcher conformance holds: 276 matcher(s) discovered·query-options-erasure ratchet holds— the convention-triggered trio for a new test file, re-run on the new head. No baseline moved in any of them.check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)·No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)·This diff introduces no major bump— all three re-run over the edited sibling changeset.doc authoring guard: 389 files clean·check-doc-anchors: 279 internal #fragment link(s) across 407 source file(s) all resolve to a real heading·docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s)·check-docs-redirects: OK (apps/docs/redirects.mjs: 92 entries ...)·175 outbound link(s) across 60 published markdown file(s) ... 102/102 relative target(s) found in the tree·check-role-word: OK, no new occurrences of the reserved word·22 ObjectSchema.create example(s) ... carry an os validate-clean security postureOK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob·check-test-source-alias OK — 72 packages with tests scanned— the new test adds nothing to either ledger: it reaches outside its package only through package specifiers, and@objectstack/specwas already the cli entry's recorded unaliased import.pnpm lint— 0 errors, 0 warnings.Test Files 154 passed (154)/Tests 1704 passed (1704)(was 153/1691 before this change: +1 file, +13 tests).pnpm --filter @objectstack/cli typecheck— exit 0, no diagnostics.⛔ No gate was weakened: no threshold moved, no baseline raised, nothing skipped.
Generated by Claude Code