From safer
Wrapper that exposes the sister codemod's safer-spec-init skill inside safer-by-default. The body of this skill is inlined at bin/safer-gen-skills time from vendor/safer-spec-development/skills/safer-spec-init/SKILL.md. Use when an adopter wants to bootstrap the living-spec layer for a new per-folder MODULE.md. Do NOT use to migrate an existing folder; route to /safer:contract-migrate. Do NOT edit the body block below. Edit the upstream vendor/safer-spec-development/skills/safer-spec-init/SKILL.md (via sister submodule bump) and re-run bin/safer-gen-skills.
How this skill is triggered — by the user, by Claude, or both
Slash command
/safer:contract-initThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
<!-- AUTO-GENERATED from this directory's SKILL.tmpl + PRINCIPLES.md. Do not edit; edit the .tmpl and regenerate via bin/safer-gen-skills. -->
You are scaffolding a folder's first MODULE.md + property-test stub. The codemod ships generate and validate as CLI commands; init is a SKILL because picking the right export to bind the stub to requires reading TypeScript correctly, and a coding agent does that more reliably than a regex / ts-morph picker baked into the CLI.
index.ts but no MODULE.md. "Leaf" means no descendant folder also lacks MODULE.md; the project root (./index.ts) loses to any descendant candidate.index.ts (if any). If it doesn't exist, scaffold the placeholder template below. If it does exist, read it and pick the first runtime-named export (see Picking the export below).MODULE.md in the folder → REFUSE. Tell the user "MODULE.md already exists; run pnpm safer-spec generate --folder <folder> --write to refresh it."When the target folder already has an index.ts, read it and pick one runtime-named export to import into the test stub. Apply these rules — they are the same rules generate's sidecar regenerator uses, just enforced through your reading instead of a regex picker:
Accept as the stub target — direct value declarations:
export const x = 1;
export let x; export var x;
export function x() {} export async function x() {}
export function* x() {} export async function* x() {}
export class X {} export abstract class X {}
export enum X {}
export namespace X { … } export module X { … }
Accept local re-exports — the alias side is what's publicly bound:
const inner = 1; export { inner as PublicName }; // pick PublicName
import { Foo } from "./foo.js"; export { Foo }; // pick Foo
Accept re-exports from another file when the chain eventually reaches a runtime-named export. Walk transitively — generate and validate register every reachable sibling source on their ts-morph project and follow the graph, so this skill must too. Recurse with a seen set keyed by absolute file path to terminate cycles; cap the depth at something generous (e.g. 6 hops) so a malformed chain doesn't loop forever.
export { Foo } from "./foo.js" → resolve Foo against ./foo.ts. If foo.ts is itself a barrel (export { Foo } from "./bar.js"), keep walking.export { foo as Bar } from "./foo.js" → the public binding is Bar. Confirm foo resolves to a runtime export in the chain; pick Bar as the import name.export * from "./foo.js" → walk into ./foo.ts, apply these same rules to find its first runtime-named export. If foo.ts has only export * from "./bar.js", recurse into ./bar.ts. The picked name is whatever ultimately resolves.export * as ns from "./foo.js" → pick ns (the namespace binding) if any candidate path for ./foo.ts exists on disk.If recursion exceeds the depth cap or hits a cycle, fall through to the no-runtime-export refusal — the chain is too deep or malformed for this skill to scaffold against confidently.
Skip (do not pick these — they erase at compile time or aren't valid named imports):
export type T = …, export interface T {}export const enum E {} (type-erased under default TS config)export type { Foo }, export { type Foo } (per-entry type-only)export type * from …, export type * as ns from …export declare const x / export declare function x() (ambient — no JS binding)export default … (not a named import target)export { x as "x-y" } / export { x as class } (non-identifier or reserved-word public names — import { x-y } / import { class } are syntax errors)/* */, //, or string-literal content — only real source code counts.If nothing in the file matches "Accept", REFUSE with: "<folder>/index.ts declares no runtime-named export. Add export const <name> = … (or a function / class / valid re-export) before re-running, or remove the file to let this skill scaffold a placeholder."
If you cannot tell whether a TypeScript construct is value-bearing, ask the user before writing the stub. Better one clarifying question than a stub that fails to compile.
<folder>/index.ts (only if it doesn't already exist)/**
* @spec.purpose Scaffolded by `safer-spec-init`. Replace this with what the folder owns.
*/
export const placeholder = "TODO" as const;
Then the picked-export name in the test stub below is placeholder.
<folder>/__tests__/<slug>.spec.test.ts<slug> is the folder's base name, lowercased, with non-alphanumerics replaced by - and outer dashes trimmed (e.g. packages/identity/inbound-auth → inbound-auth; root folder → root).
/**
* @spec.purpose Scaffolded by `safer-spec-init`. Replace this with what the tests assert.
*/
import { itSpec } from "@chughtapan/safer-spec-development";
import { <EXPORT_NAME> } from "../index.js";
/**
* @spec.property <slug>-<export_name>-stub
* @spec.type Constant Equality
* @spec.exports <EXPORT_NAME>
* @spec.claim placeholder property for the `<EXPORT_NAME>` export; promote to itSpec.prop with a real claim
*/
itSpec.todo("<slug>-<export_name>-stub", {
type: "Constant Equality",
exports: [<EXPORT_NAME>],
});
Replace <EXPORT_NAME> literally with the picked export name. Replace <slug> and <export_name> (lowercased identifier) in the property id and JSDoc.
If the picked export name is itSpec, the simple template emits two import { itSpec } lines (one for the helper, one for the subject) — a duplicate-identifier TypeScript error. Use this variant instead:
/**
* @spec.purpose Scaffolded by `safer-spec-init`. Replace this with what the tests assert.
*/
import { itSpec } from "@chughtapan/safer-spec-development";
import * as subject from "../index.js";
/**
* @spec.property <slug>-itspec-stub
* @spec.type Constant Equality
* @spec.exports itSpec
* @spec.claim placeholder property for the `itSpec` export; promote to itSpec.prop with a real claim
*/
itSpec.todo("<slug>-itspec-stub", {
type: "Constant Equality",
exports: [subject.itSpec],
});
The @spec.exports directive still names itSpec — the cross-check matches the name string, not the local binding. The same workaround applies to any future helper-name collision: namespace-import the subject and reference its members through subject.<name>.
Refuse the scaffold (do NOT write any file) when:
<folder>/MODULE.md already exists.<folder>/__tests__/<slug>.spec.test.ts already exists. The test stub path collides with a real test file — overwriting it would clobber the user's work. Refuse even when index.ts is missing.<folder>/index.ts exists AND <folder>/__tests__/<slug>.spec.test.ts exists (redundant with #2 but kept as documentation: "the folder is already scaffolded; refresh with pnpm safer-spec generate --folder <folder> --write").<folder>/index.ts exists but contains no acceptable runtime-named export (see Picking the export above).Each refusal exits cleanly. Tell the user the specific reason and the remediation step. For case 2 specifically: ask whether the existing test file is the intended owner of this stub slot — if yes, run generate --write against the folder instead; if not, ask where the new stub should live (a non-conflicting filename).
Run, in this order:
pnpm safer-spec generate --folder <folder> --write
pnpm safer-spec validate --folder <folder> --planned
generate produces the canonical MODULE.md from the source + JSDoc + test stub. validate --planned verifies the directive set + drift cross-check passes. If validate reports a gap-class error, STOP and tell the user what it says — do not patch around it.
The first implementation of init lived in src/commands/init.ts. Twelve rounds of codex review surfaced TypeScript edge cases the picker had to learn: const enum, default re-exports, export type *, ambient declare, generators, namespace declarations, string-literal aliases, reserved-word aliases, import-then-export, transitive type-only chains, named-clause from resolution. Each fix worked; the picker kept growing. After the 12th round, the picker was a partial TypeScript export resolver disguised as a CLI helper.
A coding agent already reads TypeScript fluently. Move the judgment to the agent — the codemod stays small, the agent applies the rules above per call, and edge cases land as agent instructions rather than another regex tweak.
npx claudepluginhub chughtapan/safer-by-default --plugin saferProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.