Skip to content

feat(cli): os g skill scaffolds an AI skill as NAME.skill.ts, the name the loader looks for - #11054

Merged
os-elon merged 2 commits into
mainfrom
claude/issue-11025-os-g-skill-scaffolder
Aug 22, 2026
Merged

feat(cli): os g skill scaffolds an AI skill as NAME.skill.ts, the name the loader looks for#11054
os-elon merged 2 commits into
mainfrom
claude/issue-11025-os-g-skill-scaffolder

Conversation

@os-elon

@os-elonos-elon commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

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 NAME scaffolds an AI skill into src/skills/NAME.skill.ts. The agent retirement 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_REGISTRY declares skill's file convention as *.skill.ts / *.skill.yml. This harness has always written NAME.ts. skill is allowRuntimeCreate: true — a type the platform expects to discover — and a file matching neither pattern still type-checks, still passes os validate and still publishes, with nothing anywhere reporting that it was skipped. That is the silent-strip shape the agent retirement 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 fileName hook, set by skill alone, 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.ts convention — 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 that os g object still writes customer.ts with a './customer' barrel line.

The three authoring decisions, and their reasons

decisionchoicewhy
tools (required, no default)tools: [], with a commented exampleADR-0064 removed the global fall-through, so an empty list grants nothing rather than everything — the ADR's "hole" was the fall-through, not the empty array. A placeholder name would be worse than nothing: it resolves to nothing, os validate reports ai-skill-tool-unresolved, and the runtime drops the reference while the instructions keep promising the capability.
surface (schema default 'ask')emitted as surface: 'ask'The affinity it declares is enforced at load (ADR-0063 §3). Emitting it changes nothing at runtime and makes the choice visible; a default taken in silence is one the next author cannot see they are inheriting. ADR-0063 §3's own authoring example writes it out too.
template sourcehand-written, using defineSkillNot copied from SkillSchema's or defineSkill's @example blocks — both pass triggerPhrases, a retired-key tombstone that rejects on parse (#11026). defineSkill rather 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_REGISTRY at test time (never restated), discovers the written file by listing the output directory (never assumed), and matches with node:path's own matchesGlob (never a hand-rolled matcher). The parse pin imports the generated module in a child process — the template calls defineSkill at module scope, so the import IS SkillSchema.parse.

Three ablations, each with the direction predicted before running:

legpredictionobserved
remove the skill filename overrideRED on the pattern pin, GREEN on "a file was written" — the vacuity this pin exists to defeat1 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.
add triggerPhrases to the templateRED on the parse pin3 failed / 9 passed, the parse pin's message naming ZodError. Measured directly too: the probe exits 1 with path: ["triggerPhrases"] and the tombstone's own text, thrown from defineSkill.
rebuild the barrel specifier from the metadata nameRED on the barrel pin only1 failed / 11 passed: expected the barrel to contain from './lead_qualification.skill', actual './lead_qualification'.
restoreGREEN12 passed (12).

No rebuild leg is needed here, and that is a property of this pair rather than an omission:bin/run-dev.js runs packages/cli/src through tsx, so the mutated subject IS the source the spawned CLI executes — no dist sits between the edit and the measurement. The workspace closure the test's own @objectstack/spec/kernel import resolves through was built before every leg.

⚠️ One finding from the first ablation run, fixed rather than glossed: passing the child's whole stderr as the assertion message made vitest's stack formatter crash while source-mapping the embedded ZodError, so the run failed with an unhandled "Test Run Error" and blank counts instead of naming the assertion. The message is now one line. The assertion is unchanged.

Also exercised as a user would

os g skill lead-qualification
Type: skill — AI skill (ADR-0063 extension primitive)
File: src/skills/lead_qualification.skill.ts
Created src/skills/lead_qualification.skill.ts
Created src/skills/index.ts

The generated file imports cleanly, and the barrel line reads from './lead_qualification.skill'.

⚠️ One bounded repair outside the stated file surface, declared

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 the os g agent refusal verbatim (output this card changed), stated "there is no command to rewrite the call into" and "os g skill does 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-registration re-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 from defineSkill agent skills. Nothing under content/docs/releases/. The @capabilities docs are untouched.

⚠️packages/spec is read as evidence and not edited, so the Clause-② path limb does not fire.

Verification

Gate family derived with node scripts/pm/dispatch-gates.mjs and no path arguments, re-derived after the second commit (8 paths off merge base 2866d5f97): 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 at 26bbdac873, every one exit 0, and the ratchet family plus the five changeset gates re-run at the current head 6a8fbbc431, 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. (at 6a8fbbc431; 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 posture
  • OK: 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/spec was already the cli entry's recorded unaliased import.
  • pnpm lint — 0 errors, 0 warnings.
  • Full package suite: 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.

⚠️ CI is not awaited, per the standing ruling that report delivery happens at draft-PR time; its conclusions are still in flight as this is written.


Generated by Claude Code

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
@github-actions

github-actionsBot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 5 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via query_records (literal))
  • content/docs/ai/agents.mdx(via query_records (literal))
  • content/docs/ai/connect-mcp.mdx(via query_records (literal))
  • content/docs/ai/index.mdx(via query_records (literal))
  • content/docs/ai/natural-language-queries.mdx(via query_records (literal))
  • content/docs/ai/tools.mdx(via query_records (literal))
  • content/docs/api/data-flow.mdx(via os generate (command))
  • content/docs/api/index.mdx(via query_records (literal))
  • content/docs/deployment/cli.mdx(via query_records (literal), os generate (command))
  • content/docs/deployment/environment-variables.mdx(via query_records (literal))
  • content/docs/getting-started/build-with-claude-code.mdx(via query_records (literal))
  • content/docs/getting-started/how-ai-development-works.mdx(via query_records (literal))
  • content/docs/protocol/kernel/lifecycle.mdx(via os generate (command))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/cli/README.md) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 072d072d2a74e358e923f6f6a76acbf7621305c3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3649bb31c730f86e01063db045cfa0117ef4b5ba — the merge of head 6a8fbbc43176de9c474fef964c140057b5d69855 into base 072d072d2a74e358e923f6f6a76acbf7621305c3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 072d072d2a74e358e923f6f6a76acbf7621305c3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 22, 2026
…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
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32580494827 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 5.94s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 83 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-elon@claude