Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/maintainer-repair-patches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/intent': patch
---

Add a lightweight `intent repair` command for unambiguous frontmatter migrations and reviewable before/after example patches. Preserve conflicting metadata and refuse alias edits that could change unrelated fields. Write mode applies only safe frontmatter changes; code-example suggestions require review and are emitted as patches without changing source files.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Use the repository's installed Intent command for these actions, such as `pnpm e
2. For a new task, run `intent maintainer add <name> --domain <slug> --description <activation-text> --source <path> --task <developer-task>`. Repeat `--source`, `--requires`, and `--task` for multiple entries; each `--task` becomes an assessed developer task in the domain map. In a monorepo, run the command from the owning package directory or pass `--package packages/<owner>` relative to the repository root. Source paths are relative to the owning package; `owner/repo:path` is relative to the repository. Use `--path <package-relative-path>/SKILL.md` for an established custom layout. To register an existing skill, supply its name, domain, package, and path; its frontmatter supplies the other fields. The command prints every file it wrote.
3. Author the skill and reconcile all three records using the procedures in this skill. The command creates a skeleton and a domain-map entry; add any tasks not supplied on the command line. Write the spec's decisions/history. Remove `<!-- intent:needs-authoring -->` only after authoring the corresponding document. Do not remove it simply to make a check pass. To retire a registered skill, run `intent maintainer remove <name>`; it marks the tree entry `retired` and notes it in the spec without deleting the file, and refuses while the skill is selected for distribution or required by another skill.
4. Run `intent maintainer status` to see missing work, stale metadata, and pending reviews. `--json` includes the full source-review report. For a supplied PR base, use `--base <ref>`.
5. Run `intent maintainer sync` after edits. It copies descriptions, purpose, sources, and prerequisites from registered skills into the tree, repairs the tree's record links, and includes the skill directories in existing package `files` allowlists. It preserves authored map/spec content and other manifest fields. It does not change version claims or run a package release. An absent `files` allowlist stays absent so npm's default contents are preserved; check the actual packed archive as part of the package's release checks.
5. Run `intent repair --json` when mechanical validation failures are present. Use `intent repair --write` for unambiguous frontmatter migrations; resolve conflicting values from evidence. `intent repair --patch` proposes code-preserving splits for labeled before/after examples, but inspect whether they are alternatives before applying those suggestions. The repair command does not replace validation or establish that guidance is current. Run `intent maintainer sync` after edits. It copies descriptions, purpose, sources, and prerequisites from registered skills into the tree, repairs the tree's record links, and includes the skill directories in existing package `files` allowlists. It preserves authored map/spec content and other manifest fields. It does not change version claims or run a package release. An absent `files` allowlist stays absent so npm's default contents are preserved; check the actual packed archive as part of the package's release checks.
6. Follow [source review](source-review.md) with `intent maintainer review --json`, supply justified outcomes, and record them with `intent maintainer review --record .intent/review.json`. A maintainer working in a terminal can do the same with `intent maintainer review --interactive`. The command retains the existing revision and content-fingerprint checks. Run `intent maintainer check` after recording; it exits nonzero for incomplete authoring, stale generated metadata, invalid skills, missing local prerequisites, or pending reviews. Use the same check in CI, passing the actual PR base and `--github-summary` so the reasons appear in the step summary; the workflow that `setup` copies does both.

Keep unimplemented future skills in the tree with `status: planned` and retired entries with `status: retired`. They remain part of the cumulative record but do not count as implemented skills or enter package publishing configuration. An active entry with a missing file is an error to resolve, not an invitation to delete the entry. Local prerequisite slugs are checked against implemented tree entries; verify external package prerequisites and the developer task through the task-quality procedure.
Expand Down
14 changes: 14 additions & 0 deletions packages/intent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
} from './commands/maintainer.js'
import type { ReviewCommandOptions } from './commands/review.js'
import type { ValidateCommandOptions } from './commands/validate.js'
import type { RepairCommandOptions } from './commands/repair.js'

function createCli(
runtime: InstallCommandRuntime & MaintainerCommandRuntime = {},
Expand Down Expand Up @@ -102,6 +103,19 @@ function createCli(
await runMetaCommand(name, getMetaDir())
})

cli
.command(
'repair [dir]',
'Plan conservative skill repairs without full validation',
)
.option('--write', 'Apply unambiguous frontmatter repairs')
.option('--json', 'Output the repair report as JSON')
.option('--patch', 'Print a reviewable patch without editing skill files')
.action(async (dir: string | undefined, options: RepairCommandOptions) => {
const { runRepairCommand } = await import('./commands/repair.js')
runRepairCommand(dir, options)
})

cli
.command('validate [dir]', 'Validate skill files')
.usage(
Expand Down
87 changes: 87 additions & 0 deletions packages/intent/src/commands/repair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { existsSync, readFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { resolveProjectContext } from '../core/project-context.js'
import { createIntentFsCache } from '../discovery/fs-cache.js'
import { writeChanges } from '../maintainer/files.js'
import { fail } from '../shared/cli-error.js'
import { repositoryWritePath } from '../shared/write-path.js'
import { renderRepairPatch } from '../shared/patch.js'
import { planExampleRepairs } from '../validate/blocks.js'
import { planFrontmatterRepair } from '../validate/repairs.js'
import { collectDefaultSkillsDirs } from './validate.js'
import type { FileChange } from '../maintainer/files.js'

export interface RepairCommandOptions {
write?: boolean
json?: boolean
patch?: boolean
}

export function runRepairCommand(
dir: string | undefined,
options: RepairCommandOptions,
) {
if (options.patch && (options.write || options.json))
fail('Cannot combine --patch with --write or --json')
const context = resolveProjectContext({ cwd: process.cwd() })
const root = context.workspaceRoot ?? context.packageRoot ?? context.cwd
const { findSkillFiles } = createIntentFsCache()
const directories =
dir === undefined
? collectDefaultSkillsDirs(context, findSkillFiles)
: [
resolveProjectContext({ cwd: process.cwd(), targetPath: dir })
.targetSkillsDir ?? resolve(dir),
]
if (dir !== undefined && !existsSync(directories[0]!))
fail(`Skills directory not found: ${dir}`)
const paths = [...new Set(directories.flatMap(findSkillFiles))]
if (dir !== undefined && !paths.length) fail('No SKILL.md files found')
const report = {
version: 1,
repairs: [] as Array<{ file: string; changes: Array<string> }>,
suggestions: [] as Array<{ file: string; line: number; message: string }>,
problems: [] as Array<{ file: string; message: string }>,
}
const changes: Array<FileChange> = []
const proposed: Array<FileChange & { source: string }> = []
for (const path of paths) {
const destination = repositoryWritePath(root, path)
const file = relative(root, destination).replaceAll('\\', '/')
const source = readFileSync(destination, 'utf8')
const plan = planFrontmatterRepair(destination, source)
report.problems.push(...plan.problems.map((message) => ({ file, message })))
if (plan.change) {
changes.push(plan.change)
report.repairs.push({ file, changes: plan.changes })
}
const examples = planExampleRepairs(root, plan.change?.content ?? source)
report.suggestions.push(
...examples.suggestions.map((suggestion) => ({ file, ...suggestion })),
)
if (examples.skipped)
report.problems.push({ file, message: examples.skipped })
if (examples.content !== source)
proposed.push({ path: destination, source, content: examples.content })
}
if (options.write) writeChanges(root, changes)
if (options.patch) process.stdout.write(renderRepairPatch(root, proposed))
else if (options.json) console.log(JSON.stringify(report, null, 2))
else {
console.log(
`${report.repairs.length} file(s) ${options.write ? 'repaired' : 'with mechanical repairs'}, ${report.suggestions.length} suggestion(s), ${report.problems.length} problem(s).`,
)
for (const repair of report.repairs)
console.log(`${repair.file}: ${repair.changes.join('; ')}`)
for (const problem of report.problems)
console.log(`${problem.file}: ${problem.message}`)
for (const suggestion of report.suggestions)
console.log(
`${suggestion.file}:${suggestion.line}: ${suggestion.message}`,
)
}
if (report.problems.length)
fail(
'Some repairs need assessment; resolve the reported problems and run repair again.',
)
}
Loading
Loading