reffy-cli 1.7.0 → 1.8.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/README.md +133 -2
- package/dist/cli.js +167 -8
- package/dist/doctor.js +12 -0
- package/dist/plan.js +1 -1
- package/dist/skills.d.ts +96 -0
- package/dist/skills.js +471 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,10 +33,13 @@ Command summary:
|
|
|
33
33
|
- `reffy plan create`: generates proposal, task, design, and spec scaffolds from indexed Reffy artifacts.
|
|
34
34
|
- `reffy plan validate|list|show|archive`: manages the planning lifecycle under `.reffy/reffyspec/`.
|
|
35
35
|
- `reffy spec list|show`: inspects current spec state under `.reffy/reffyspec/`.
|
|
36
|
+
- `reffy skill list|show|create|validate`: lists, prints, scaffolds, and validates task-oriented skills under `.reffy/skills/`.
|
|
36
37
|
- `reffy remote init|status|push|ls|cat|snapshot`: links, publishes, and inspects a Paseo-backed remote `.reffy/` workspace.
|
|
37
38
|
- `reffy remote workspace create|get` and `reffy remote project register|list`: control-plane operations against the workspace manager actor.
|
|
38
39
|
- `reffy diagram render`: renders Mermaid diagrams as SVG or ASCII, including spec-aware generation from compatible `spec.md` files.
|
|
39
40
|
|
|
41
|
+
Run `reffy <command> --help` for the full flag list of any command. Most commands accept `--repo PATH` so they can be invoked from outside the project root (useful for agent harnesses).
|
|
42
|
+
|
|
40
43
|
Output modes:
|
|
41
44
|
|
|
42
45
|
- `--output text` (default)
|
|
@@ -56,7 +59,11 @@ reffy plan create --change-id add-login-flow --artifacts login-idea.md
|
|
|
56
59
|
reffy plan list --output json
|
|
57
60
|
reffy plan archive add-login-flow
|
|
58
61
|
reffy spec show auth --output json
|
|
59
|
-
reffy
|
|
62
|
+
reffy skill list --output json
|
|
63
|
+
reffy skill show create-change
|
|
64
|
+
reffy skill create my-workflow
|
|
65
|
+
reffy skill validate
|
|
66
|
+
reffy remote init --provision
|
|
60
67
|
reffy remote status --output json
|
|
61
68
|
reffy remote push
|
|
62
69
|
reffy remote ls
|
|
@@ -66,6 +73,81 @@ reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format ascii
|
|
|
66
73
|
reffy diagram render --input .reffy/reffyspec/specs/auth/spec.md --format svg --output .reffy/artifacts/auth-spec.svg
|
|
67
74
|
```
|
|
68
75
|
|
|
76
|
+
## Diagnostics and Inspection
|
|
77
|
+
|
|
78
|
+
### `reffy doctor`
|
|
79
|
+
|
|
80
|
+
Audits the local Reffy setup: the `.reffy/` workspace exists, `manifest.json` is valid, managed `AGENTS.md` blocks are in place, and optional tooling (mermaid CLI) is reachable. Useful as a first step when a command is misbehaving in an unfamiliar repo.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
reffy doctor # human-readable check list
|
|
84
|
+
reffy doctor --output json # machine-readable for CI
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `reffy summarize`
|
|
88
|
+
|
|
89
|
+
Produces a read-only handoff summary of indexed artifacts — titles, kinds, tags, related changes, derived outputs. Use it to brief a fresh agent or to audit what context has accumulated under `.reffy/artifacts/` without opening every file.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
reffy summarize # text overview grouped by kind
|
|
93
|
+
reffy summarize --output json # full structured payload
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `reffy spec list|show`
|
|
97
|
+
|
|
98
|
+
Inspects the current truth in `.reffy/reffyspec/specs/`. Each capability has its own spec file with requirements and scenarios; `list` enumerates capabilities, `show` prints one spec's requirements.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
reffy spec list
|
|
102
|
+
reffy spec show remote-workspace-manager --output json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Skills
|
|
106
|
+
|
|
107
|
+
Reffy gives *procedures* the same deterministic treatment it gives data. Skills are named, agent-readable task recipes that live as files under `.reffy/skills/`, one directory per skill:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
.reffy/skills/
|
|
111
|
+
├── create-change/
|
|
112
|
+
│ └── SKILL.md
|
|
113
|
+
└── <your-skill>/
|
|
114
|
+
└── SKILL.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each `SKILL.md` opens with frontmatter that doubles as a discovery index, followed by a markdown body:
|
|
118
|
+
|
|
119
|
+
```markdown
|
|
120
|
+
---
|
|
121
|
+
name: create-change
|
|
122
|
+
description: Turn one or more ideation artifacts into a ReffySpec change proposal.
|
|
123
|
+
triggers: ["new change", "plan create", "turn artifact into proposal"]
|
|
124
|
+
commands: ["reffy plan create", "reffy plan validate"]
|
|
125
|
+
managed: true
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## When to use this skill
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
## Steps
|
|
132
|
+
1. ...
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `name`, `description`, and `triggers` are required; `triggers` must have at least one entry so the skill is discoverable.
|
|
136
|
+
- `commands` declares the CLI commands the skill wraps, which `reffy doctor` cross-checks against the installed CLI.
|
|
137
|
+
- `managed: true` marks skills Reffy owns. `reffy init` scaffolds and refreshes managed skills in place and never touches unmanaged ones.
|
|
138
|
+
|
|
139
|
+
`reffy init` ships six managed skills covering the core workflows: `create-artifact`, `create-change`, `archive-change`, `inspect-specs`, `sync-remote`, and `diagnose`. Author your own with `reffy skill create <name>`.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
reffy skill list # name + description + managed flag
|
|
143
|
+
reffy skill list --output json # harness-native descriptors for programmatic discovery
|
|
144
|
+
reffy skill show create-change # print the procedure body
|
|
145
|
+
reffy skill create my-workflow # scaffold an unmanaged skill from a template
|
|
146
|
+
reffy skill validate # check the frontmatter contract for every skill
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`reffy skill list`/`show --output json` emit a tool/function-definition style descriptor (`name`, `description`, `triggers`, `commands`, `managed`, `path`; `show` adds `body`) so an agent harness can feed skills into its own discovery machinery without parsing markdown. Skills are discovered from the filesystem and validated by contract — they are not indexed in `manifest.json`. `reffy validate` enforces the skills contract (required fields, unique names, kebab-case directory names matching `name`) alongside the manifest, and `reffy doctor` warns when a skill's declared commands drift from the installed CLI.
|
|
150
|
+
|
|
69
151
|
## Remote Sync
|
|
70
152
|
|
|
71
153
|
Reffy can publish the local `.reffy/` workspace to a Paseo-backed remote workspace and inspect it later with native CLI commands.
|
|
@@ -239,7 +321,56 @@ A practical pattern is:
|
|
|
239
321
|
3. Keep a clear traceable path from exploratory artifacts to formal specs.
|
|
240
322
|
4. Use Reffy commands for day-to-day workflow.
|
|
241
323
|
|
|
242
|
-
|
|
324
|
+
### Planning lifecycle
|
|
325
|
+
|
|
326
|
+
The end-to-end arc for a non-trivial change:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# 1. Capture raw context as artifacts. Each artifact is a free-form
|
|
330
|
+
# markdown note; reindex registers it in the manifest.
|
|
331
|
+
echo "..." > .reffy/artifacts/login-flow-idea.md
|
|
332
|
+
reffy reindex
|
|
333
|
+
|
|
334
|
+
# 2. Scaffold a ReffySpec change from selected artifacts. This creates
|
|
335
|
+
# .reffy/reffyspec/changes/add-login-flow/ with proposal, design,
|
|
336
|
+
# tasks, and a placeholder spec delta — all linked back to the
|
|
337
|
+
# artifacts you passed in.
|
|
338
|
+
reffy plan create \
|
|
339
|
+
--change-id add-login-flow \
|
|
340
|
+
--artifacts login-flow-idea.md \
|
|
341
|
+
--title "Add login flow"
|
|
342
|
+
|
|
343
|
+
# 3. Edit the generated files:
|
|
344
|
+
# - proposal.md: Why / What Changes / Impact / Reffy References
|
|
345
|
+
# - design.md: decisions, data model, open questions
|
|
346
|
+
# - tasks.md: implementation + verification checklists
|
|
347
|
+
# - specs/<capability>/spec.md: ADDED / MODIFIED / REMOVED requirements
|
|
348
|
+
# with at least one Scenario each
|
|
349
|
+
|
|
350
|
+
# 4. Validate before implementing. Catches missing scenarios, malformed
|
|
351
|
+
# delta sections, and broken artifact references.
|
|
352
|
+
reffy plan validate add-login-flow
|
|
353
|
+
|
|
354
|
+
# 5. Implement, tick tasks in tasks.md as you go, re-validate.
|
|
355
|
+
|
|
356
|
+
# 6. Archive once shipped. Moves the change under
|
|
357
|
+
# .reffy/reffyspec/changes/archive/<date>-<change-id>/ and merges
|
|
358
|
+
# the delta spec(s) into the canonical specs in
|
|
359
|
+
# .reffy/reffyspec/specs/.
|
|
360
|
+
reffy plan archive add-login-flow
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Useful side commands during the arc:
|
|
364
|
+
|
|
365
|
+
- `reffy plan list` — enumerate active and archived changes.
|
|
366
|
+
- `reffy plan show <change-id>` — inspect one change's state without opening every file.
|
|
367
|
+
- `reffy spec list` / `reffy spec show <capability>` — see the canonical specs the deltas will land into.
|
|
368
|
+
|
|
369
|
+
### Representing pivots
|
|
370
|
+
|
|
371
|
+
Pivots, deprecations, and wind-downs are not a separate concept in ReffySpec — they are ordinary changes whose delta is mostly REMOVED or MODIFIED requirements instead of ADDED ones. Because every change's `specs/<capability>/spec.md` is already a delta against the canonical spec, a course correction reuses the same machinery as a feature addition: scaffold a change, author a delta that removes or rewrites the relevant requirements, pair it with code-removal tasks, and archive it when shipped. The history of pivots stays legible as a series of deltas under `changes/archive/`, rather than getting buried in code commits.
|
|
372
|
+
|
|
373
|
+
### Reference implementation in this repo
|
|
243
374
|
|
|
244
375
|
- `AGENTS.md`: contains both managed instruction blocks and encodes sequencing.
|
|
245
376
|
- `AGENTS.md`: Reffy block routes ideation/exploration requests to `@/.reffy/AGENTS.md`.
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { DEFAULT_PLANNING_RELATIVE_DIR, looksLikePlanningDir, resolveCanonicalPl
|
|
|
12
12
|
import { listPlanningChanges, showPlanningChange, validatePlanningChange } from "./plan-runtime.js";
|
|
13
13
|
import { assertWorkspaceSummaryIdentity, collectWorkspaceDocuments, describeRemoteLinkage, ensureManagerInit, ensureWorkspaceTarget, extractWorkspaceSummaryIdentity, PaseoManagerClient, PaseoWorkspaceBackendClient, readRemoteConfig, RemoteHttpError, removeWorkspaceTarget, requireWorkspaceIdentity, resolveRemoteConfigPath, resolveRemoteTarget, resolveSelectedWorkspaceId, updateRemoteConfigMetadata, validateImportResult, } from "./remote.js";
|
|
14
14
|
import { DEFAULT_REFS_DIRNAME, detectWorkspaceState, looksLikeRefsDir } from "./refs-paths.js";
|
|
15
|
+
import { createSkill, discoverSkills, findSkill, scaffoldManagedSkills, validateSkills, } from "./skills.js";
|
|
15
16
|
import { prepareCanonicalPlanningLayout } from "./planning-workspace.js";
|
|
16
17
|
import { ReferencesStore } from "./storage.js";
|
|
17
18
|
import { listSpecs, showSpec } from "./spec-runtime.js";
|
|
@@ -47,6 +48,8 @@ Use \`@/${refsDirName}/AGENTS.md\` to learn:
|
|
|
47
48
|
- How Reffy owns the runtime while preserving ReffySpec planning files
|
|
48
49
|
- How to store and consume ideation context in \`${refsDirName}/\`
|
|
49
50
|
|
|
51
|
+
Before performing a Reffy workflow, check \`${refsDirName}/skills/\` (or run \`reffy skill list\`) and follow the matching skill.
|
|
52
|
+
|
|
50
53
|
Keep this managed block so \`reffy init\` can refresh the instructions.
|
|
51
54
|
|
|
52
55
|
<!-- REFFY:END -->`;
|
|
@@ -83,6 +86,7 @@ These instructions are for AI assistants working in this project.
|
|
|
83
86
|
- Add/update exploratory artifacts and keep them concise.
|
|
84
87
|
- Run \`reffy reindex\` and \`reffy validate\` after artifact changes.
|
|
85
88
|
- Use \`reffy summarize --output json\` and \`reffy plan create\` to turn artifact context into planning scaffolds.
|
|
89
|
+
- Before performing a Reffy workflow, check \`${refsDirName}/skills/\` (or run \`reffy skill list\`) and follow the matching skill.
|
|
86
90
|
|
|
87
91
|
## When To Use Reffy
|
|
88
92
|
|
|
@@ -381,6 +385,7 @@ async function runSetupCommand(commandName, output, repoRoot) {
|
|
|
381
385
|
const workspace = await prepareCanonicalWorkspace(repoRoot);
|
|
382
386
|
const planning = await prepareCanonicalPlanningLayout(repoRoot);
|
|
383
387
|
const agents = await initAgents(repoRoot);
|
|
388
|
+
const skills = await scaffoldManagedSkills(repoRoot);
|
|
384
389
|
const store = new ReferencesStore(repoRoot);
|
|
385
390
|
const reindex = await store.reindexArtifacts();
|
|
386
391
|
const payload = {
|
|
@@ -393,6 +398,7 @@ async function runSetupCommand(commandName, output, repoRoot) {
|
|
|
393
398
|
migrated_planning_layout: planning.migrated,
|
|
394
399
|
created_planning_layout: planning.created,
|
|
395
400
|
...agents,
|
|
401
|
+
skills,
|
|
396
402
|
refs_dir: store.refsDir,
|
|
397
403
|
manifest_path: store.manifestPath,
|
|
398
404
|
reindex,
|
|
@@ -411,6 +417,7 @@ async function runSetupCommand(commandName, output, repoRoot) {
|
|
|
411
417
|
console.log(`Updated ${agents.root_agents_path}`);
|
|
412
418
|
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
413
419
|
console.log(`Updated ${agents.reffyspec_agents_path}`);
|
|
420
|
+
console.log(`Skills: ${String(skills.written_skills.length)} managed (${skills.preserved_unmanaged.length} unmanaged preserved)`);
|
|
414
421
|
console.log(`Reindex: added=${String(reindex.added)} removed=${String(reindex.removed)} total=${String(reindex.total)}`);
|
|
415
422
|
if (shouldPrintBootstrapOnboarding(workspace.created, workspace.migrated, planning.created, planning.migrated)) {
|
|
416
423
|
printBootstrapOnboarding();
|
|
@@ -434,6 +441,7 @@ function usage() {
|
|
|
434
441
|
" summarize Generate a read-only summary of indexed Reffy artifacts.",
|
|
435
442
|
" plan Generate and manage ReffySpec planning scaffolds from indexed Reffy artifacts.",
|
|
436
443
|
" spec Inspect current specs from the ReffySpec layout.",
|
|
444
|
+
" skill List, show, create, and validate Reffy skills under .reffy/skills.",
|
|
437
445
|
" remote Link, publish, and inspect a Paseo-backed remote Reffy workspace.",
|
|
438
446
|
" diagram Render Mermaid diagrams (supports SVG and ASCII).",
|
|
439
447
|
].join("\n");
|
|
@@ -465,6 +473,15 @@ function specUsage() {
|
|
|
465
473
|
" reffy spec show <spec-id> [--repo PATH] [--output text|json]",
|
|
466
474
|
].join("\n");
|
|
467
475
|
}
|
|
476
|
+
function skillUsage() {
|
|
477
|
+
return [
|
|
478
|
+
"Usage:",
|
|
479
|
+
" reffy skill list [--repo PATH] [--output text|json]",
|
|
480
|
+
" reffy skill show <name> [--repo PATH] [--output text|json]",
|
|
481
|
+
" reffy skill create <name> [--repo PATH] [--output text|json]",
|
|
482
|
+
" reffy skill validate [<name>] [--repo PATH] [--output text|json]",
|
|
483
|
+
].join("\n");
|
|
484
|
+
}
|
|
468
485
|
function planUsage() {
|
|
469
486
|
return [
|
|
470
487
|
"Usage:",
|
|
@@ -1744,6 +1761,129 @@ async function main() {
|
|
|
1744
1761
|
console.error(specUsage());
|
|
1745
1762
|
return 1;
|
|
1746
1763
|
}
|
|
1764
|
+
if (command === "skill") {
|
|
1765
|
+
const [subcommand, ...skillArgs] = rest;
|
|
1766
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
1767
|
+
console.error(skillUsage());
|
|
1768
|
+
return 1;
|
|
1769
|
+
}
|
|
1770
|
+
const output = parseOutputMode(skillArgs);
|
|
1771
|
+
const repoRoot = parseRepoArg(skillArgs);
|
|
1772
|
+
await prepareCanonicalWorkspace(repoRoot);
|
|
1773
|
+
const positionals = getPlanPositionalArgs(skillArgs);
|
|
1774
|
+
if (subcommand === "list") {
|
|
1775
|
+
const skills = await discoverSkills(repoRoot);
|
|
1776
|
+
const descriptors = skills.map(({ name, description, triggers, commands, managed, path: relPath }) => ({
|
|
1777
|
+
name,
|
|
1778
|
+
description,
|
|
1779
|
+
triggers,
|
|
1780
|
+
commands,
|
|
1781
|
+
managed,
|
|
1782
|
+
path: relPath,
|
|
1783
|
+
}));
|
|
1784
|
+
if (output === "json") {
|
|
1785
|
+
printResult(output, { status: "ok", command: "skill", subcommand: "list", skills: descriptors });
|
|
1786
|
+
}
|
|
1787
|
+
else if (descriptors.length === 0) {
|
|
1788
|
+
console.log("Skills:");
|
|
1789
|
+
console.log("- (none)");
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
console.log("Skills:");
|
|
1793
|
+
for (const skill of descriptors) {
|
|
1794
|
+
console.log(`- ${skill.name}${skill.managed ? " [managed]" : ""} - ${skill.description}`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return 0;
|
|
1798
|
+
}
|
|
1799
|
+
if (subcommand === "show") {
|
|
1800
|
+
const name = positionals[0];
|
|
1801
|
+
if (!name) {
|
|
1802
|
+
console.error("reffy skill show requires a skill name");
|
|
1803
|
+
console.error(skillUsage());
|
|
1804
|
+
return 1;
|
|
1805
|
+
}
|
|
1806
|
+
const skill = await findSkill(repoRoot, name);
|
|
1807
|
+
if (!skill) {
|
|
1808
|
+
if (output === "json") {
|
|
1809
|
+
printResult(output, { status: "error", command: "skill", subcommand: "show", error: `skill "${name}" not found` });
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
console.error(`Skill "${name}" not found`);
|
|
1813
|
+
}
|
|
1814
|
+
return 1;
|
|
1815
|
+
}
|
|
1816
|
+
if (output === "json") {
|
|
1817
|
+
printResult(output, {
|
|
1818
|
+
status: "ok",
|
|
1819
|
+
command: "skill",
|
|
1820
|
+
subcommand: "show",
|
|
1821
|
+
skill: {
|
|
1822
|
+
name: skill.name,
|
|
1823
|
+
description: skill.description,
|
|
1824
|
+
triggers: skill.triggers,
|
|
1825
|
+
commands: skill.commands,
|
|
1826
|
+
managed: skill.managed,
|
|
1827
|
+
path: skill.path,
|
|
1828
|
+
body: skill.body,
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
console.log(skill.body.trim());
|
|
1834
|
+
}
|
|
1835
|
+
return 0;
|
|
1836
|
+
}
|
|
1837
|
+
if (subcommand === "create") {
|
|
1838
|
+
const name = positionals[0];
|
|
1839
|
+
if (!name) {
|
|
1840
|
+
console.error("reffy skill create requires a skill name");
|
|
1841
|
+
console.error(skillUsage());
|
|
1842
|
+
return 1;
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const result = await createSkill(repoRoot, name);
|
|
1846
|
+
if (output === "json") {
|
|
1847
|
+
printResult(output, { status: "ok", command: "skill", subcommand: "create", ...result });
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
console.log(`Created skill "${result.name}" at ${result.entry_path}`);
|
|
1851
|
+
}
|
|
1852
|
+
return 0;
|
|
1853
|
+
}
|
|
1854
|
+
catch (error) {
|
|
1855
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1856
|
+
if (output === "json") {
|
|
1857
|
+
printResult(output, { status: "error", command: "skill", subcommand: "create", error: message });
|
|
1858
|
+
}
|
|
1859
|
+
else {
|
|
1860
|
+
console.error(message);
|
|
1861
|
+
}
|
|
1862
|
+
return 1;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (subcommand === "validate") {
|
|
1866
|
+
const name = positionals[0];
|
|
1867
|
+
const result = await validateSkills(repoRoot, name);
|
|
1868
|
+
const payload = { status: result.ok ? "ok" : "error", command: "skill", subcommand: "validate", ...result };
|
|
1869
|
+
if (output === "json") {
|
|
1870
|
+
printResult(output, payload);
|
|
1871
|
+
}
|
|
1872
|
+
else if (result.ok) {
|
|
1873
|
+
console.log(`Skills valid: skills=${String(result.skill_count)}`);
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
console.error(`Skills invalid: ${String(result.issues.length)} issue(s)`);
|
|
1877
|
+
for (const issue of result.issues) {
|
|
1878
|
+
console.error(`error: ${issue.skill}${issue.field ? ` (${issue.field})` : ""}: ${issue.message}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return result.ok ? 0 : 1;
|
|
1882
|
+
}
|
|
1883
|
+
console.error(`Unknown skill subcommand: ${subcommand}`);
|
|
1884
|
+
console.error(skillUsage());
|
|
1885
|
+
return 1;
|
|
1886
|
+
}
|
|
1747
1887
|
const output = parseOutputMode(rest);
|
|
1748
1888
|
if (command === "init") {
|
|
1749
1889
|
printBanner(output);
|
|
@@ -1759,6 +1899,7 @@ async function main() {
|
|
|
1759
1899
|
const workspace = await prepareCanonicalWorkspace(repoRoot);
|
|
1760
1900
|
const planning = await prepareCanonicalPlanningLayout(repoRoot);
|
|
1761
1901
|
const agents = await initAgents(repoRoot);
|
|
1902
|
+
const skills = await scaffoldManagedSkills(repoRoot);
|
|
1762
1903
|
const payload = {
|
|
1763
1904
|
status: "ok",
|
|
1764
1905
|
command: "migrate",
|
|
@@ -1770,6 +1911,7 @@ async function main() {
|
|
|
1770
1911
|
created_planning_layout: planning.created,
|
|
1771
1912
|
refs_dir: workspace.state.canonicalDir,
|
|
1772
1913
|
...agents,
|
|
1914
|
+
skills,
|
|
1773
1915
|
};
|
|
1774
1916
|
if (output === "json") {
|
|
1775
1917
|
printResult(output, payload);
|
|
@@ -1782,6 +1924,7 @@ async function main() {
|
|
|
1782
1924
|
console.log(`Updated ${agents.root_agents_path}`);
|
|
1783
1925
|
console.log(`Updated ${agents.reffy_agents_path}`);
|
|
1784
1926
|
console.log(`Updated ${agents.reffyspec_agents_path}`);
|
|
1927
|
+
console.log(`Skills: ${String(skills.written_skills.length)} managed (${skills.preserved_unmanaged.length} unmanaged preserved)`);
|
|
1785
1928
|
}
|
|
1786
1929
|
return 0;
|
|
1787
1930
|
}
|
|
@@ -1830,12 +1973,20 @@ async function main() {
|
|
|
1830
1973
|
const repoRoot = parseRepoArg(rest);
|
|
1831
1974
|
const store = new ReferencesStore(repoRoot);
|
|
1832
1975
|
const result = await store.validateManifest();
|
|
1833
|
-
const
|
|
1976
|
+
const skills = await validateSkills(repoRoot);
|
|
1977
|
+
const ok = result.ok && skills.ok;
|
|
1978
|
+
const payload = {
|
|
1979
|
+
status: ok ? "ok" : "error",
|
|
1980
|
+
command: "validate",
|
|
1981
|
+
...result,
|
|
1982
|
+
skills: { ok: skills.ok, issues: skills.issues, skill_count: skills.skill_count },
|
|
1983
|
+
};
|
|
1834
1984
|
if (output === "json") {
|
|
1835
1985
|
printResult(output, payload);
|
|
1836
1986
|
}
|
|
1837
|
-
else if (
|
|
1987
|
+
else if (ok) {
|
|
1838
1988
|
console.log(`Manifest valid: artifacts=${String(result.artifact_count)}`);
|
|
1989
|
+
console.log(`Skills valid: skills=${String(skills.skill_count)}`);
|
|
1839
1990
|
if (result.warnings.length > 0) {
|
|
1840
1991
|
for (const warning of result.warnings) {
|
|
1841
1992
|
console.log(`warn: ${warning}`);
|
|
@@ -1843,15 +1994,23 @@ async function main() {
|
|
|
1843
1994
|
}
|
|
1844
1995
|
}
|
|
1845
1996
|
else {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1997
|
+
if (!result.ok) {
|
|
1998
|
+
console.error(`Manifest invalid: ${String(result.errors.length)} error(s)`);
|
|
1999
|
+
for (const error of result.errors) {
|
|
2000
|
+
console.error(`error: ${error}`);
|
|
2001
|
+
}
|
|
2002
|
+
for (const warning of result.warnings) {
|
|
2003
|
+
console.error(`warn: ${warning}`);
|
|
2004
|
+
}
|
|
1849
2005
|
}
|
|
1850
|
-
|
|
1851
|
-
console.error(`
|
|
2006
|
+
if (!skills.ok) {
|
|
2007
|
+
console.error(`Skills invalid: ${String(skills.issues.length)} issue(s)`);
|
|
2008
|
+
for (const issue of skills.issues) {
|
|
2009
|
+
console.error(`error: ${issue.skill}${issue.field ? ` (${issue.field})` : ""}: ${issue.message}`);
|
|
2010
|
+
}
|
|
1852
2011
|
}
|
|
1853
2012
|
}
|
|
1854
|
-
return
|
|
2013
|
+
return ok ? 0 : 1;
|
|
1855
2014
|
}
|
|
1856
2015
|
if (command === "summarize") {
|
|
1857
2016
|
const repoRoot = parseRepoArg(rest);
|
package/dist/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { validateManifest } from "./manifest.js";
|
|
4
4
|
import { detectWorkspaceState, resolveRefsDirName } from "./refs-paths.js";
|
|
5
|
+
import { findCommandDrift } from "./skills.js";
|
|
5
6
|
async function pathExists(targetPath) {
|
|
6
7
|
try {
|
|
7
8
|
await fs.access(targetPath);
|
|
@@ -88,5 +89,16 @@ export async function runDoctor(repoRoot) {
|
|
|
88
89
|
? "legacy .references workspace detected; run `reffy migrate` to adopt .reffy/"
|
|
89
90
|
: ".reffy/ is the active workspace",
|
|
90
91
|
});
|
|
92
|
+
const commandDrift = await findCommandDrift(repoRoot);
|
|
93
|
+
checks.push({
|
|
94
|
+
id: "skills_commands_current",
|
|
95
|
+
level: "optional",
|
|
96
|
+
ok: commandDrift.length === 0,
|
|
97
|
+
message: commandDrift.length === 0
|
|
98
|
+
? "skill command references match the installed CLI"
|
|
99
|
+
: `stale skill command references: ${commandDrift
|
|
100
|
+
.map((drift) => `${drift.skill} -> "${drift.command}"`)
|
|
101
|
+
.join("; ")}`,
|
|
102
|
+
});
|
|
91
103
|
return { checks, summary: summarizeChecks(checks) };
|
|
92
104
|
}
|
package/dist/plan.js
CHANGED
|
@@ -94,7 +94,7 @@ function normalizeHeading(value) {
|
|
|
94
94
|
return value.trim().toLowerCase().replace(/[^a-z0-9 ]+/g, "");
|
|
95
95
|
}
|
|
96
96
|
function extractMarkdownSections(content) {
|
|
97
|
-
const sections =
|
|
97
|
+
const sections = Object.create(null);
|
|
98
98
|
let current = "root";
|
|
99
99
|
sections[current] = [];
|
|
100
100
|
for (const rawLine of content.split(/\r?\n/)) {
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export declare const SKILLS_DIRNAME = "skills";
|
|
2
|
+
export declare const SKILL_ENTRY_FILENAME = "SKILL.md";
|
|
3
|
+
export interface SkillFrontmatter {
|
|
4
|
+
name?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
triggers: string[];
|
|
7
|
+
commands: string[];
|
|
8
|
+
managed: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SkillDescriptor {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
triggers: string[];
|
|
14
|
+
commands: string[];
|
|
15
|
+
managed: boolean;
|
|
16
|
+
path: string;
|
|
17
|
+
}
|
|
18
|
+
export interface SkillRecord extends SkillDescriptor {
|
|
19
|
+
dir: string;
|
|
20
|
+
entryPath: string;
|
|
21
|
+
body: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SkillValidationIssue {
|
|
24
|
+
skill: string;
|
|
25
|
+
field?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SkillValidationResult {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
issues: SkillValidationIssue[];
|
|
31
|
+
skill_count: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function resolveSkillsDir(repoRoot: string): string;
|
|
34
|
+
export declare function isKebabCase(value: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Parse the `SKILL.md` frontmatter contract. Supports a leading `---` fenced
|
|
37
|
+
* block with scalar fields (`name`, `description`, `managed`) and list fields
|
|
38
|
+
* (`triggers`, `commands`) in either inline (`[a, b]`) or block (`- a`) form.
|
|
39
|
+
* Returns the parsed frontmatter and the remaining markdown body.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseSkillFile(content: string): {
|
|
42
|
+
frontmatter: SkillFrontmatter;
|
|
43
|
+
body: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Discover skills by scanning `.reffy/skills/` for directories containing a
|
|
47
|
+
* `SKILL.md` entry file. Skills are filesystem-discovered and never indexed in
|
|
48
|
+
* `manifest.json`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function discoverSkills(repoRoot: string): Promise<SkillRecord[]>;
|
|
51
|
+
export declare function findSkill(repoRoot: string, name: string): Promise<SkillRecord | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Validate every skill (or one named skill) against the frontmatter contract:
|
|
54
|
+
* required fields, non-empty triggers, unique names, and kebab-case directory
|
|
55
|
+
* names matching `name`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function validateSkills(repoRoot: string, name?: string): Promise<SkillValidationResult>;
|
|
58
|
+
/**
|
|
59
|
+
* The canonical command table of the installed CLI. `reffy doctor` cross-checks
|
|
60
|
+
* each skill's declared `commands` against this table and warns on drift.
|
|
61
|
+
*/
|
|
62
|
+
export declare const KNOWN_COMMANDS: readonly string[];
|
|
63
|
+
export declare function isKnownCommand(command: string): boolean;
|
|
64
|
+
export interface SkillCommandDrift {
|
|
65
|
+
skill: string;
|
|
66
|
+
command: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function findCommandDrift(repoRoot: string): Promise<SkillCommandDrift[]>;
|
|
69
|
+
interface ManagedSkillDefinition {
|
|
70
|
+
name: string;
|
|
71
|
+
description: string;
|
|
72
|
+
triggers: string[];
|
|
73
|
+
commands: string[];
|
|
74
|
+
body: string;
|
|
75
|
+
}
|
|
76
|
+
export declare const MANAGED_SKILLS: readonly ManagedSkillDefinition[];
|
|
77
|
+
export declare function isManagedSkillName(name: string): boolean;
|
|
78
|
+
export interface SkillScaffoldResult {
|
|
79
|
+
created_dir: boolean;
|
|
80
|
+
written_skills: string[];
|
|
81
|
+
preserved_unmanaged: string[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create `.reffy/skills/` and write the built-in managed skills. Managed skill
|
|
85
|
+
* bodies are (re)written in place on every call; unmanaged skills are never
|
|
86
|
+
* touched.
|
|
87
|
+
*/
|
|
88
|
+
export declare function scaffoldManagedSkills(repoRoot: string): Promise<SkillScaffoldResult>;
|
|
89
|
+
export interface SkillCreateResult {
|
|
90
|
+
name: string;
|
|
91
|
+
entry_path: string;
|
|
92
|
+
created: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** Scaffold a new unmanaged skill from a template. */
|
|
95
|
+
export declare function createSkill(repoRoot: string, name: string): Promise<SkillCreateResult>;
|
|
96
|
+
export {};
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRefsDir } from "./refs-paths.js";
|
|
4
|
+
export const SKILLS_DIRNAME = "skills";
|
|
5
|
+
export const SKILL_ENTRY_FILENAME = "SKILL.md";
|
|
6
|
+
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7
|
+
export function resolveSkillsDir(repoRoot) {
|
|
8
|
+
return path.join(resolveRefsDir(repoRoot), SKILLS_DIRNAME);
|
|
9
|
+
}
|
|
10
|
+
export function isKebabCase(value) {
|
|
11
|
+
return KEBAB_CASE.test(value);
|
|
12
|
+
}
|
|
13
|
+
async function pathExists(targetPath) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(targetPath);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseScalar(raw) {
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
25
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
26
|
+
return trimmed.slice(1, -1);
|
|
27
|
+
}
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
function parseInlineList(raw) {
|
|
31
|
+
const inner = raw.trim().replace(/^\[/, "").replace(/\]$/, "");
|
|
32
|
+
if (inner.trim() === "") {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return inner
|
|
36
|
+
.split(",")
|
|
37
|
+
.map((item) => parseScalar(item))
|
|
38
|
+
.filter((item) => item.length > 0);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse the `SKILL.md` frontmatter contract. Supports a leading `---` fenced
|
|
42
|
+
* block with scalar fields (`name`, `description`, `managed`) and list fields
|
|
43
|
+
* (`triggers`, `commands`) in either inline (`[a, b]`) or block (`- a`) form.
|
|
44
|
+
* Returns the parsed frontmatter and the remaining markdown body.
|
|
45
|
+
*/
|
|
46
|
+
export function parseSkillFile(content) {
|
|
47
|
+
const normalized = content.replace(/^/, "");
|
|
48
|
+
const frontmatter = { triggers: [], commands: [], managed: false };
|
|
49
|
+
const lines = normalized.split(/\r?\n/);
|
|
50
|
+
if (lines[0]?.trim() !== "---") {
|
|
51
|
+
return { frontmatter, body: normalized };
|
|
52
|
+
}
|
|
53
|
+
let end = -1;
|
|
54
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
55
|
+
if (lines[i].trim() === "---") {
|
|
56
|
+
end = i;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (end === -1) {
|
|
61
|
+
return { frontmatter, body: normalized };
|
|
62
|
+
}
|
|
63
|
+
let currentListKey = null;
|
|
64
|
+
for (let i = 1; i < end; i += 1) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
if (line.trim() === "") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const blockItem = /^\s*-\s+(.*)$/.exec(line);
|
|
70
|
+
if (blockItem && currentListKey) {
|
|
71
|
+
const value = parseScalar(blockItem[1]);
|
|
72
|
+
if (value.length > 0) {
|
|
73
|
+
frontmatter[currentListKey].push(value);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const keyMatch = /^([A-Za-z_][A-Za-z0-9_]*):(.*)$/.exec(line);
|
|
78
|
+
if (!keyMatch) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = keyMatch[1].trim();
|
|
82
|
+
const rawValue = keyMatch[2].trim();
|
|
83
|
+
currentListKey = null;
|
|
84
|
+
if (key === "name") {
|
|
85
|
+
frontmatter.name = parseScalar(rawValue);
|
|
86
|
+
}
|
|
87
|
+
else if (key === "description") {
|
|
88
|
+
frontmatter.description = parseScalar(rawValue);
|
|
89
|
+
}
|
|
90
|
+
else if (key === "managed") {
|
|
91
|
+
frontmatter.managed = parseScalar(rawValue).toLowerCase() === "true";
|
|
92
|
+
}
|
|
93
|
+
else if (key === "triggers" || key === "commands") {
|
|
94
|
+
if (rawValue.startsWith("[")) {
|
|
95
|
+
frontmatter[key] = parseInlineList(rawValue);
|
|
96
|
+
}
|
|
97
|
+
else if (rawValue === "") {
|
|
98
|
+
currentListKey = key;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
frontmatter[key] = [parseScalar(rawValue)];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const body = lines.slice(end + 1).join("\n").replace(/^\n+/, "");
|
|
106
|
+
return { frontmatter, body };
|
|
107
|
+
}
|
|
108
|
+
function toDescriptor(record) {
|
|
109
|
+
const { frontmatter, dirName, relPath } = record;
|
|
110
|
+
return {
|
|
111
|
+
name: frontmatter.name ?? dirName,
|
|
112
|
+
description: frontmatter.description ?? "",
|
|
113
|
+
triggers: frontmatter.triggers,
|
|
114
|
+
commands: frontmatter.commands,
|
|
115
|
+
managed: frontmatter.managed,
|
|
116
|
+
path: relPath,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Discover skills by scanning `.reffy/skills/` for directories containing a
|
|
121
|
+
* `SKILL.md` entry file. Skills are filesystem-discovered and never indexed in
|
|
122
|
+
* `manifest.json`.
|
|
123
|
+
*/
|
|
124
|
+
export async function discoverSkills(repoRoot) {
|
|
125
|
+
const skillsDir = resolveSkillsDir(repoRoot);
|
|
126
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => []);
|
|
127
|
+
const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
128
|
+
const records = [];
|
|
129
|
+
for (const dirName of dirs) {
|
|
130
|
+
const dir = path.join(skillsDir, dirName);
|
|
131
|
+
const entryPath = path.join(dir, SKILL_ENTRY_FILENAME);
|
|
132
|
+
if (!(await pathExists(entryPath))) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const content = await fs.readFile(entryPath, "utf8");
|
|
136
|
+
const { frontmatter, body } = parseSkillFile(content);
|
|
137
|
+
const relPath = path.relative(repoRoot, entryPath);
|
|
138
|
+
const descriptor = toDescriptor({ frontmatter, dirName, relPath });
|
|
139
|
+
records.push({ ...descriptor, dir, entryPath, body });
|
|
140
|
+
}
|
|
141
|
+
return records;
|
|
142
|
+
}
|
|
143
|
+
export async function findSkill(repoRoot, name) {
|
|
144
|
+
const skills = await discoverSkills(repoRoot);
|
|
145
|
+
return skills.find((skill) => skill.name === name) ?? null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validate every skill (or one named skill) against the frontmatter contract:
|
|
149
|
+
* required fields, non-empty triggers, unique names, and kebab-case directory
|
|
150
|
+
* names matching `name`.
|
|
151
|
+
*/
|
|
152
|
+
export async function validateSkills(repoRoot, name) {
|
|
153
|
+
const skillsDir = resolveSkillsDir(repoRoot);
|
|
154
|
+
const issues = [];
|
|
155
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => []);
|
|
156
|
+
const dirs = entries
|
|
157
|
+
.filter((entry) => entry.isDirectory())
|
|
158
|
+
.map((entry) => entry.name)
|
|
159
|
+
.filter((dirName) => name === undefined || dirName === name)
|
|
160
|
+
.sort();
|
|
161
|
+
const seenNames = new Map();
|
|
162
|
+
for (const dirName of dirs) {
|
|
163
|
+
const label = dirName;
|
|
164
|
+
const entryPath = path.join(skillsDir, dirName, SKILL_ENTRY_FILENAME);
|
|
165
|
+
if (!(await pathExists(entryPath))) {
|
|
166
|
+
issues.push({ skill: label, message: `${SKILL_ENTRY_FILENAME} is missing` });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const content = await fs.readFile(entryPath, "utf8");
|
|
170
|
+
const { frontmatter } = parseSkillFile(content);
|
|
171
|
+
if (!frontmatter.name || frontmatter.name.trim() === "") {
|
|
172
|
+
issues.push({ skill: label, field: "name", message: "frontmatter is missing required field: name" });
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
if (frontmatter.name !== dirName) {
|
|
176
|
+
issues.push({
|
|
177
|
+
skill: label,
|
|
178
|
+
field: "name",
|
|
179
|
+
message: `name "${frontmatter.name}" does not match directory name "${dirName}"`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const prior = seenNames.get(frontmatter.name);
|
|
183
|
+
if (prior) {
|
|
184
|
+
issues.push({
|
|
185
|
+
skill: label,
|
|
186
|
+
field: "name",
|
|
187
|
+
message: `duplicate skill name "${frontmatter.name}" (also defined in "${prior}")`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
seenNames.set(frontmatter.name, dirName);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!isKebabCase(dirName)) {
|
|
195
|
+
issues.push({ skill: label, message: `directory name "${dirName}" is not kebab-case` });
|
|
196
|
+
}
|
|
197
|
+
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
|
198
|
+
issues.push({ skill: label, field: "description", message: "frontmatter is missing required field: description" });
|
|
199
|
+
}
|
|
200
|
+
if (frontmatter.triggers.length === 0) {
|
|
201
|
+
issues.push({ skill: label, field: "triggers", message: "frontmatter field triggers must contain at least one entry" });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (name !== undefined && dirs.length === 0) {
|
|
205
|
+
issues.push({ skill: name, message: `skill "${name}" was not found` });
|
|
206
|
+
}
|
|
207
|
+
return { ok: issues.length === 0, issues, skill_count: dirs.length };
|
|
208
|
+
}
|
|
209
|
+
// --- Command-reference staleness -------------------------------------------
|
|
210
|
+
/**
|
|
211
|
+
* The canonical command table of the installed CLI. `reffy doctor` cross-checks
|
|
212
|
+
* each skill's declared `commands` against this table and warns on drift.
|
|
213
|
+
*/
|
|
214
|
+
export const KNOWN_COMMANDS = [
|
|
215
|
+
"reffy init",
|
|
216
|
+
"reffy bootstrap",
|
|
217
|
+
"reffy migrate",
|
|
218
|
+
"reffy doctor",
|
|
219
|
+
"reffy reindex",
|
|
220
|
+
"reffy validate",
|
|
221
|
+
"reffy summarize",
|
|
222
|
+
"reffy plan create",
|
|
223
|
+
"reffy plan validate",
|
|
224
|
+
"reffy plan list",
|
|
225
|
+
"reffy plan show",
|
|
226
|
+
"reffy plan archive",
|
|
227
|
+
"reffy spec list",
|
|
228
|
+
"reffy spec show",
|
|
229
|
+
"reffy remote init",
|
|
230
|
+
"reffy remote status",
|
|
231
|
+
"reffy remote push",
|
|
232
|
+
"reffy remote ls",
|
|
233
|
+
"reffy remote cat",
|
|
234
|
+
"reffy remote snapshot",
|
|
235
|
+
"reffy remote workspace create",
|
|
236
|
+
"reffy remote workspace get",
|
|
237
|
+
"reffy remote workspace delete",
|
|
238
|
+
"reffy remote project register",
|
|
239
|
+
"reffy remote project list",
|
|
240
|
+
"reffy diagram render",
|
|
241
|
+
"reffy skill list",
|
|
242
|
+
"reffy skill show",
|
|
243
|
+
"reffy skill create",
|
|
244
|
+
"reffy skill validate",
|
|
245
|
+
];
|
|
246
|
+
/** Reduce a declared command string to its leading non-flag tokens. */
|
|
247
|
+
function commandHead(command) {
|
|
248
|
+
const tokens = command.trim().split(/\s+/);
|
|
249
|
+
const head = [];
|
|
250
|
+
for (const token of tokens) {
|
|
251
|
+
if (token.startsWith("-")) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
head.push(token);
|
|
255
|
+
}
|
|
256
|
+
return head.join(" ");
|
|
257
|
+
}
|
|
258
|
+
export function isKnownCommand(command) {
|
|
259
|
+
const head = commandHead(command);
|
|
260
|
+
if (head === "") {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return KNOWN_COMMANDS.some((known) => head === known || head.startsWith(`${known} `));
|
|
264
|
+
}
|
|
265
|
+
export async function findCommandDrift(repoRoot) {
|
|
266
|
+
const skills = await discoverSkills(repoRoot);
|
|
267
|
+
const drift = [];
|
|
268
|
+
for (const skill of skills) {
|
|
269
|
+
for (const command of skill.commands) {
|
|
270
|
+
if (!isKnownCommand(command)) {
|
|
271
|
+
drift.push({ skill: skill.name, command });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return drift;
|
|
276
|
+
}
|
|
277
|
+
function renderSkillFile(def) {
|
|
278
|
+
const lines = ["---"];
|
|
279
|
+
lines.push(`name: ${def.name}`);
|
|
280
|
+
lines.push(`description: ${def.description}`);
|
|
281
|
+
lines.push(`triggers: [${def.triggers.map((t) => JSON.stringify(t)).join(", ")}]`);
|
|
282
|
+
lines.push(`commands: [${def.commands.map((c) => JSON.stringify(c)).join(", ")}]`);
|
|
283
|
+
lines.push(`managed: ${def.managed ? "true" : "false"}`);
|
|
284
|
+
lines.push("---");
|
|
285
|
+
lines.push("");
|
|
286
|
+
lines.push(def.body.trim());
|
|
287
|
+
lines.push("");
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
export const MANAGED_SKILLS = [
|
|
291
|
+
{
|
|
292
|
+
name: "create-artifact",
|
|
293
|
+
description: "Capture ideation context as a Reffy artifact and register it in the manifest.",
|
|
294
|
+
triggers: ["new artifact", "capture idea", "add note", "reindex"],
|
|
295
|
+
commands: ["reffy reindex", "reffy validate"],
|
|
296
|
+
body: `## When to use this skill
|
|
297
|
+
Use this when you have raw ideation, exploration, or research context to capture before any formal planning.
|
|
298
|
+
|
|
299
|
+
## Steps
|
|
300
|
+
1. Write the context as a markdown file under \`.reffy/artifacts/\` with a clear, stable, kebab-case filename.
|
|
301
|
+
2. Run \`reffy reindex\` to add the new file to \`.reffy/manifest.json\`.
|
|
302
|
+
3. Run \`reffy validate\` to confirm the manifest still satisfies the v1 contract.
|
|
303
|
+
4. Confirm the artifact appears in the manifest with the expected name and id.
|
|
304
|
+
|
|
305
|
+
## Failure modes
|
|
306
|
+
- If \`reffy validate\` reports an invalid manifest, fix the reported entry before continuing — do not hand-edit ids.
|
|
307
|
+
- Keep artifacts exploratory; do not duplicate full proposal or spec content here.`,
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "create-change",
|
|
311
|
+
description: "Turn one or more ideation artifacts into a ReffySpec change proposal.",
|
|
312
|
+
triggers: ["new change", "plan create", "turn artifact into proposal"],
|
|
313
|
+
commands: ["reffy plan create", "reffy plan validate"],
|
|
314
|
+
body: `## When to use this skill
|
|
315
|
+
Use this when ideation has converged and you are ready to scaffold a formal ReffySpec change from one or more artifacts.
|
|
316
|
+
|
|
317
|
+
## Steps
|
|
318
|
+
1. Pick the source artifacts in \`.reffy/artifacts/\` that inform the change.
|
|
319
|
+
2. Run \`reffy plan create --change-id <kebab-id> --artifacts <files> --title "<title>"\`.
|
|
320
|
+
3. Fill in the scaffolded \`proposal.md\`, \`design.md\`, \`tasks.md\`, and \`specs/<capability>/spec.md\` deltas.
|
|
321
|
+
Each delta requirement needs at least one \`#### Scenario:\`.
|
|
322
|
+
4. Run \`reffy plan validate <change-id>\` and resolve every error before implementing.
|
|
323
|
+
|
|
324
|
+
## Failure modes
|
|
325
|
+
- If validation reports a missing scenario block, add at least one scenario to each requirement.
|
|
326
|
+
- Use a unique verb-led, kebab-case \`change-id\`.`,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "archive-change",
|
|
330
|
+
description: "Complete and archive a shipped ReffySpec change, merging its delta into canonical specs.",
|
|
331
|
+
triggers: ["archive change", "ship change", "plan archive", "merge spec"],
|
|
332
|
+
commands: ["reffy plan validate", "reffy plan archive", "reffy spec show"],
|
|
333
|
+
body: `## When to use this skill
|
|
334
|
+
Use this once a change is implemented and verified and you want to fold its spec delta into canonical truth.
|
|
335
|
+
|
|
336
|
+
## Steps
|
|
337
|
+
1. Confirm every task in \`tasks.md\` is checked off.
|
|
338
|
+
2. Run \`reffy plan validate <change-id>\` one last time.
|
|
339
|
+
3. Run \`reffy plan archive <change-id>\` to move the change under \`changes/archive/<date>-<change-id>/\` and merge its delta specs.
|
|
340
|
+
4. Verify the merge with \`reffy spec show <capability>\`.
|
|
341
|
+
|
|
342
|
+
## Failure modes
|
|
343
|
+
- If archive reports unmerged or conflicting requirements, inspect the delta against the canonical spec before retrying.`,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "inspect-specs",
|
|
347
|
+
description: "Ground work in current truth by inspecting canonical specs before changing behavior.",
|
|
348
|
+
triggers: ["inspect specs", "current truth", "spec list", "spec show"],
|
|
349
|
+
commands: ["reffy spec list", "reffy spec show"],
|
|
350
|
+
body: `## When to use this skill
|
|
351
|
+
Use this before drafting a change or implementing behavior, to confirm what the specs already say.
|
|
352
|
+
|
|
353
|
+
## Steps
|
|
354
|
+
1. Run \`reffy spec list\` to enumerate capabilities and their requirement counts.
|
|
355
|
+
2. Run \`reffy spec show <capability>\` to read the requirements and scenarios for a capability.
|
|
356
|
+
3. Reference the relevant spec from your proposal or skill rather than restating its requirements.
|
|
357
|
+
|
|
358
|
+
## Failure modes
|
|
359
|
+
- If a capability is missing, it likely needs a new \`ADDED\` delta in a change rather than an edit to canonical specs.`,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: "sync-remote",
|
|
363
|
+
description: "Publish and inspect the local .reffy/ workspace on a Paseo-backed remote.",
|
|
364
|
+
triggers: ["remote sync", "push workspace", "paseo", "remote status"],
|
|
365
|
+
commands: ["reffy remote init", "reffy remote status", "reffy remote push", "reffy remote snapshot"],
|
|
366
|
+
body: `## When to use this skill
|
|
367
|
+
Use this to link, publish, or inspect the shared remote workspace projection.
|
|
368
|
+
|
|
369
|
+
## Required environment
|
|
370
|
+
- \`PASEO_ENDPOINT\` — the Paseo endpoint URL (never persisted).
|
|
371
|
+
- \`PASEO_TOKEN\` — the bearer token (never persisted by the CLI).
|
|
372
|
+
|
|
373
|
+
Provide them either by exporting them in your shell or by placing them in a
|
|
374
|
+
\`.env\` file at the repo root — every \`reffy remote\` command auto-loads \`.env\`
|
|
375
|
+
(use \`--env-file PATH\` to point at a different file). Exported shell vars take
|
|
376
|
+
precedence over \`.env\`.
|
|
377
|
+
|
|
378
|
+
## Steps
|
|
379
|
+
1. Ensure both values are available via the shell or \`.env\`; the CLI fails fast and names a missing one.
|
|
380
|
+
2. First time: \`reffy remote init --provision\` to create the workspace and mint a token. Save the token immediately.
|
|
381
|
+
3. \`reffy remote status\` to confirm linkage and identity.
|
|
382
|
+
4. \`reffy remote push\` to import the full local \`.reffy/\` tree.
|
|
383
|
+
5. \`reffy remote snapshot\` / \`ls\` / \`cat\` to inspect the remote projection.
|
|
384
|
+
|
|
385
|
+
## Failure modes
|
|
386
|
+
- A missing token makes the stored identifiers in \`.reffy/state/remote.json\` inert — set \`PASEO_TOKEN\`.`,
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "diagnose",
|
|
390
|
+
description: "Diagnose Reffy workspace health and resolve common failure classes.",
|
|
391
|
+
triggers: ["diagnose", "doctor", "validate", "workspace health"],
|
|
392
|
+
commands: ["reffy doctor", "reffy validate"],
|
|
393
|
+
body: `## When to use this skill
|
|
394
|
+
Use this first when a Reffy command misbehaves in an unfamiliar repo.
|
|
395
|
+
|
|
396
|
+
## Steps
|
|
397
|
+
1. Run \`reffy doctor\` for the required/optional check list.
|
|
398
|
+
2. Run \`reffy validate\` to confirm the manifest contract.
|
|
399
|
+
3. Resolve each failure by class:
|
|
400
|
+
- Missing \`.reffy/\` or \`AGENTS.md\`: run \`reffy init\`.
|
|
401
|
+
- Legacy \`.references/\` workspace: run \`reffy migrate\`.
|
|
402
|
+
- Invalid manifest: fix the reported entry, then re-run \`reffy validate\`.
|
|
403
|
+
- Skill command drift: update the stale skill's \`commands\` list.
|
|
404
|
+
|
|
405
|
+
## Failure modes
|
|
406
|
+
- A non-zero exit from \`reffy doctor\` means a required check failed; address required failures before optional warnings.`,
|
|
407
|
+
},
|
|
408
|
+
];
|
|
409
|
+
export function isManagedSkillName(name) {
|
|
410
|
+
return MANAGED_SKILLS.some((skill) => skill.name === name);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Create `.reffy/skills/` and write the built-in managed skills. Managed skill
|
|
414
|
+
* bodies are (re)written in place on every call; unmanaged skills are never
|
|
415
|
+
* touched.
|
|
416
|
+
*/
|
|
417
|
+
export async function scaffoldManagedSkills(repoRoot) {
|
|
418
|
+
const skillsDir = resolveSkillsDir(repoRoot);
|
|
419
|
+
const created_dir = !(await pathExists(skillsDir));
|
|
420
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
421
|
+
const written = [];
|
|
422
|
+
for (const def of MANAGED_SKILLS) {
|
|
423
|
+
const dir = path.join(skillsDir, def.name);
|
|
424
|
+
await fs.mkdir(dir, { recursive: true });
|
|
425
|
+
const entryPath = path.join(dir, SKILL_ENTRY_FILENAME);
|
|
426
|
+
const content = renderSkillFile({ ...def, managed: true });
|
|
427
|
+
await fs.writeFile(entryPath, content, "utf8");
|
|
428
|
+
written.push(def.name);
|
|
429
|
+
}
|
|
430
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => []);
|
|
431
|
+
const preserved_unmanaged = entries
|
|
432
|
+
.filter((entry) => entry.isDirectory() && !isManagedSkillName(entry.name))
|
|
433
|
+
.map((entry) => entry.name)
|
|
434
|
+
.sort();
|
|
435
|
+
return { created_dir, written_skills: written, preserved_unmanaged };
|
|
436
|
+
}
|
|
437
|
+
const UNMANAGED_TEMPLATE_BODY = `## When to use this skill
|
|
438
|
+
Describe the situation in which an agent should follow this skill.
|
|
439
|
+
|
|
440
|
+
## Steps
|
|
441
|
+
1. First step, naming the exact \`reffy\` command to run.
|
|
442
|
+
2. Next step.
|
|
443
|
+
|
|
444
|
+
## Failure modes
|
|
445
|
+
- Describe a likely failure and how to recover from it.`;
|
|
446
|
+
/** Scaffold a new unmanaged skill from a template. */
|
|
447
|
+
export async function createSkill(repoRoot, name) {
|
|
448
|
+
if (!isKebabCase(name)) {
|
|
449
|
+
throw new Error(`Skill name must be kebab-case: "${name}"`);
|
|
450
|
+
}
|
|
451
|
+
if (isManagedSkillName(name)) {
|
|
452
|
+
throw new Error(`"${name}" is a managed skill name and is reserved by reffy init`);
|
|
453
|
+
}
|
|
454
|
+
const skillsDir = resolveSkillsDir(repoRoot);
|
|
455
|
+
const dir = path.join(skillsDir, name);
|
|
456
|
+
const entryPath = path.join(dir, SKILL_ENTRY_FILENAME);
|
|
457
|
+
if (await pathExists(entryPath)) {
|
|
458
|
+
throw new Error(`Skill "${name}" already exists at ${path.relative(repoRoot, entryPath)}`);
|
|
459
|
+
}
|
|
460
|
+
await fs.mkdir(dir, { recursive: true });
|
|
461
|
+
const content = renderSkillFile({
|
|
462
|
+
name,
|
|
463
|
+
description: "One-line summary of what this skill does.",
|
|
464
|
+
triggers: ["describe", "when", "to", "use"],
|
|
465
|
+
commands: [],
|
|
466
|
+
managed: false,
|
|
467
|
+
body: UNMANAGED_TEMPLATE_BODY,
|
|
468
|
+
});
|
|
469
|
+
await fs.writeFile(entryPath, content, "utf8");
|
|
470
|
+
return { name, entry_path: path.relative(repoRoot, entryPath), created: true };
|
|
471
|
+
}
|