@venizia/vulcan 0.2.0-1 → 0.2.0-3
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 +1 -1
- package/base/skills/docs-update/SKILL.md +100 -0
- package/base/skills/docs-update/evals/evals.json +23 -0
- package/package.json +3 -3
- package/src/doctor/skills.ts +48 -11
- package/src/sync/planner.ts +21 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ An unknown key is an error; every error names the file and the field.
|
|
|
49
49
|
|
|
50
50
|
- `rules.md` - the base rules: write boundaries, security, process, build, and the IGNIS coding style V-C-01..V-C-18 (37 rules; it lists V-W-05, V-C-03 and V-C-05 as retired).
|
|
51
51
|
- `slots.json` - rules a project picks a wording for (V-W-01 today).
|
|
52
|
-
- `skills/` - `knowledge-sync`, `docs-writing` (project-neutral; a project-specific skill stays in its project).
|
|
52
|
+
- `skills/` - `knowledge-sync`, `docs-writing`, `docs-update` (project-neutral; a project-specific skill stays in its project).
|
|
53
53
|
- `hooks/` - `session-start.ts` (prints the rules digest) and `guard-writes.ts` (asks before a database write, a migration, a deploy or a mutating HTTP call).
|
|
54
54
|
- `claude/settings.json`, `make/vulcan.mk`, `templates/`.
|
|
55
55
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docs-update
|
|
3
|
+
description: Updates the project's human-facing docs after a code change - refreshes the page that owns a topic, adds a page for a new feature, writes the changelog entry, then runs the docs gates until they pass. Use when the user asks to update the wiki or the docs, document a change or a feature, refresh a page, add a changelog entry, or keep the docs in step with the code.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Read, Write, Edit, Grep, Glob, Bash, Agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Docs update
|
|
9
|
+
|
|
10
|
+
The docs under `knowledge.wiki` are what people read; the knowledge bundle under `knowledge.bundle`
|
|
11
|
+
is what agents read. Never copy a page from one into the other. **The code is ground truth**: when a
|
|
12
|
+
page and the code disagree, the page is the bug. Nothing is committed: the user reviews the diff.
|
|
13
|
+
|
|
14
|
+
`$ARGUMENTS` says what to document - `add a changelog entry for the retry option`, `refresh the
|
|
15
|
+
storage reference`. With no arguments, ask what to document.
|
|
16
|
+
|
|
17
|
+
## Checklist
|
|
18
|
+
|
|
19
|
+
Copy it into your reply and tick it off, in order:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
- [ ] 1. Paths read from the manifest
|
|
23
|
+
- [ ] 2. Code read - the changed source, not a diff summary, not dist/, not another doc
|
|
24
|
+
- [ ] 3. Page picked - the page that owns the topic; a new one only when none does, wired in
|
|
25
|
+
- [ ] 4. Page written and self-reviewed with the docs-writing skill
|
|
26
|
+
- [ ] 5. Changelog entry written (release-worthy changes only)
|
|
27
|
+
- [ ] 6. Gates green (loop until they are)
|
|
28
|
+
- [ ] 7. Reported; nothing committed
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 1. Paths
|
|
32
|
+
|
|
33
|
+
1. `bun x --no-install vulcan doctor` passes, so the manifest is fresh. Fix what it names first.
|
|
34
|
+
2. Read `.agents/vulcan.manifest.json`. Every path comes from it, never from a layout you expect:
|
|
35
|
+
|
|
36
|
+
| Key | What it gives you |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `knowledge.wiki` | the docs root - every page lives under it |
|
|
39
|
+
| `knowledge.changelogs` | the changelog directory |
|
|
40
|
+
| `packages[].path` | where each package's source lives |
|
|
41
|
+
| `packages[].name` | the npm names a changelog entry declares |
|
|
42
|
+
| `name` | the brand, upper case in prose |
|
|
43
|
+
|
|
44
|
+
A key the manifest lacks is a part the project does not have. No `knowledge.wiki`: the human-facing
|
|
45
|
+
docs are the package READMEs (`<packages[].path>/README.md`) and the changelog - ask before creating
|
|
46
|
+
a docs root. No `knowledge.changelogs`: skip step 5 and say so.
|
|
47
|
+
|
|
48
|
+
## 2. Code
|
|
49
|
+
|
|
50
|
+
Read the source the change touched, under `packages[].path`: `git diff HEAD --stat -- <paths>` for
|
|
51
|
+
uncommitted work, `git log -p -- <paths>` for committed work. Open the files themselves. Write down
|
|
52
|
+
every name, signature, option and default you will mention - each one is verified here, not later.
|
|
53
|
+
|
|
54
|
+
## 3. Pick the page
|
|
55
|
+
|
|
56
|
+
1. Find the page that owns the topic: `grep -rln '<symbol or feature>' <knowledge.wiki>`, plus the
|
|
57
|
+
package READMEs. Edit that page. A second page on the same subject is how docs drift.
|
|
58
|
+
2. No page owns it: create one in the section whose pages are the same kind, copying the frontmatter
|
|
59
|
+
and layout of 2-3 siblings. List it where a sibling is listed - the sidebar config, a nav file,
|
|
60
|
+
the section `index.md` (`grep -rn '<sibling file name>' --exclude-dir=node_modules .`).
|
|
61
|
+
|
|
62
|
+
## 4. Write
|
|
63
|
+
|
|
64
|
+
Write and self-review with the [docs-writing](../docs-writing/SKILL.md) skill, its mechanical gates
|
|
65
|
+
included. Then apply the docs rules in `.agents/rules.md` (the generator's limits, the link style).
|
|
66
|
+
Every code sample uses the names and signatures from step 2.
|
|
67
|
+
|
|
68
|
+
## 5. Changelog
|
|
69
|
+
|
|
70
|
+
Only for a release-worthy change: a feature, a fix a user notices, a breaking change.
|
|
71
|
+
|
|
72
|
+
1. **The file.** `<knowledge.changelogs>/YYYY-MM-DD-<slug>.md`, from the directory's `template.md`.
|
|
73
|
+
No template: copy the shape of the newest entry. Keep only the sections that apply. When an
|
|
74
|
+
entry for the coming release already exists, add a bullet to it instead of opening a second.
|
|
75
|
+
2. **The `packages:` list** (Project Contract v1, C3). The frontmatter lists the npm names, from
|
|
76
|
+
`packages[].name`, of every package the entry covers: `packages: ['@scope/core']`. An entry about
|
|
77
|
+
no package, such as repository tooling, carries `packages: []`. Never leave the list out -
|
|
78
|
+
`okf check --strict` fails the entry.
|
|
79
|
+
3. **The content.** What changed, who is affected, how to migrate. It is not a commit log.
|
|
80
|
+
4. **The index.** When the directory's `index.md` lists entries, add a row in its order.
|
|
81
|
+
|
|
82
|
+
## 6. Gates - validate, fix, repeat
|
|
83
|
+
|
|
84
|
+
1. `bun x --no-install vulcan doctor`
|
|
85
|
+
2. `bun x --no-install vulcan x okf check --strict` - the bundle, the house rules, every changelog
|
|
86
|
+
entry's `packages:` list.
|
|
87
|
+
3. The project's own docs build, when it has one. Discover it, never assume a target name: the
|
|
88
|
+
targets `.agents/rules.md` names, `make help`, the Makefile and its `*.mk` files (`grep -n '<the
|
|
89
|
+
wiki directory>'`), the `scripts` of the `package.json` nearest `knowledge.wiki`. Run every docs
|
|
90
|
+
target you find - build, link check, anchor check, docs lint. None found: say so in the report.
|
|
91
|
+
|
|
92
|
+
A gate that fails: fix what it names, go back to the step that caused it, then run every gate again.
|
|
93
|
+
Stop only when all of them pass. Never weaken a gate to make it pass.
|
|
94
|
+
|
|
95
|
+
## 7. Report
|
|
96
|
+
|
|
97
|
+
A fact an agent reads changed too? Update its concept in the knowledge bundle in the same change; a
|
|
98
|
+
full re-audit is the `knowledge-sync` skill's job. Report in the user's language: the pages changed
|
|
99
|
+
and created, the changelog entry and its `packages:` list, each gate with its result, anything
|
|
100
|
+
skipped and why. **Do not commit.** Leave the diff for the user.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "docs-update",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 0,
|
|
6
|
+
"prompt": "I changed how one of our packages behaves - see the uncommitted diff, or the last commit if the tree is clean. Update the docs to match and add the changelog entry. Do not commit.",
|
|
7
|
+
"expected_output": "Reads knowledge.wiki, knowledge.changelogs and packages[].path from .agents/vulcan.manifest.json; reads the changed source itself; edits the existing page that owns the topic instead of opening a duplicate; writes a YYYY-MM-DD-<slug>.md entry from the changelog template whose packages: list carries the npm names it covers; runs vulcan doctor, okf check --strict and the docs build it discovered, looping until all pass; nothing committed",
|
|
8
|
+
"files": [".agents/vulcan.manifest.json"]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": 1,
|
|
12
|
+
"prompt": "Add a changelog entry for last week's change to the repository's CI and release tooling. It touches no package. Do not commit.",
|
|
13
|
+
"expected_output": "One dated entry under the manifest's knowledge.changelogs, shaped by the directory's template, carrying packages: [] in its frontmatter, added to the changelog index when one lists entries; okf check --strict passes; nothing committed",
|
|
14
|
+
"files": [".agents/vulcan.manifest.json"]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": 2,
|
|
18
|
+
"prompt": "The docs for the first package in .agents/vulcan.manifest.json are out of date. Refresh them against the current source. No changelog entry - nothing shipped. Do not commit.",
|
|
19
|
+
"expected_output": "Finds the page or README that owns that package (under knowledge.wiki, or <packages[].path>/README.md when the manifest has no wiki); verifies every name, signature and default against the source under packages[].path and fixes each drift; creates no new page and no changelog entry; runs the gates; reports each claim that changed with its source file; nothing committed",
|
|
20
|
+
"files": [".agents/vulcan.manifest.json"]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/vulcan",
|
|
3
|
-
"version": "0.2.0-
|
|
3
|
+
"version": "0.2.0-3",
|
|
4
4
|
"description": "VULCAN - one setup, one workflow for every project: init, sync, doctor, upgrade, and `vulcan x` for the shipped tools. Bun-only.",
|
|
5
5
|
"homepage": "https://github.com/VENIZIA-AI/vulcan/tree/main/packages/cli",
|
|
6
6
|
"repository": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"test": "NODE_ENV=test bun test --env-file=.env.test"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@venizia/vulcan-atlas": "0.2.0-
|
|
48
|
-
"@venizia/vulcan-okf": "0.2.0-
|
|
47
|
+
"@venizia/vulcan-atlas": "0.2.0-3",
|
|
48
|
+
"@venizia/vulcan-okf": "0.2.0-3",
|
|
49
49
|
"zod": "^4.5.4"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
package/src/doctor/skills.ts
CHANGED
|
@@ -33,13 +33,46 @@ const filesOf = (opts: { directory: string }): string[] =>
|
|
|
33
33
|
const lineCountOf = (opts: { text: string }): number =>
|
|
34
34
|
opts.text.replace(/\n$/, '').split('\n').length;
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const parseYaml = (opts: { text: string }): { value?: unknown; error?: string } => {
|
|
37
|
+
try {
|
|
38
|
+
return { value: Bun.YAML.parse(opts.text) };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The frontmatter's fields, or why they cannot be read. Bun's YAML error names no line, so the line
|
|
46
|
+
* is the first one at which the block stops parsing - counted in the file, the opening `---` is 1.
|
|
47
|
+
*/
|
|
48
|
+
const frontmatterOf = (opts: {
|
|
49
|
+
text: string;
|
|
50
|
+
}): { data: Record<string, unknown>; error?: string } => {
|
|
37
51
|
const block = FRONTMATTER.exec(opts.text)?.[1];
|
|
38
|
-
const parsed: unknown = block === undefined ? undefined : Bun.YAML.parse(block);
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
if (block === undefined) {
|
|
54
|
+
return { data: {} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parsed = parseYaml({ text: block });
|
|
58
|
+
|
|
59
|
+
if (parsed.error !== undefined) {
|
|
60
|
+
const blockLines = block.split('\n');
|
|
61
|
+
const failsAt = blockLines.findIndex(
|
|
62
|
+
(_line, index) =>
|
|
63
|
+
parseYaml({ text: blockLines.slice(0, index + 1).join('\n') }).error !== undefined,
|
|
64
|
+
);
|
|
65
|
+
const where = failsAt === -1 ? '' : ` at line ${failsAt + 2}`;
|
|
66
|
+
|
|
67
|
+
return { data: {}, error: `the frontmatter is not valid YAML${where} (${parsed.error})` };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
data:
|
|
72
|
+
typeof parsed.value === 'object' && parsed.value !== null
|
|
73
|
+
? Object.fromEntries(Object.entries(parsed.value))
|
|
74
|
+
: {},
|
|
75
|
+
};
|
|
43
76
|
};
|
|
44
77
|
|
|
45
78
|
/** Every rule one skill directory breaks, each a message naming the file. */
|
|
@@ -53,19 +86,23 @@ export const lintSkill = (opts: { directory: string }): string[] => {
|
|
|
53
86
|
const problems: string[] = [];
|
|
54
87
|
const text = readFileSync(skillFile, 'utf8');
|
|
55
88
|
const frontmatter = frontmatterOf({ text });
|
|
56
|
-
const { name, description } = frontmatter;
|
|
89
|
+
const { name, description } = frontmatter.data;
|
|
57
90
|
|
|
58
|
-
|
|
91
|
+
// One file a project cannot parse is a finding about that file - the other checks still run.
|
|
92
|
+
if (frontmatter.error !== undefined) {
|
|
93
|
+
problems.push(`SKILL.md: ${frontmatter.error}`);
|
|
94
|
+
} else if (typeof name !== 'string' || !NAME_PATTERN.test(name) || RESERVED_NAME.test(name)) {
|
|
59
95
|
problems.push(
|
|
60
96
|
`SKILL.md: name '${String(name)}' - at most 64 characters of a-z, 0-9 and hyphens, never 'claude' or 'anthropic'`,
|
|
61
97
|
);
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
101
|
+
frontmatter.error === undefined &&
|
|
102
|
+
(typeof description !== 'string' ||
|
|
103
|
+
description.trim().length === 0 ||
|
|
104
|
+
description.length > MAX_DESCRIPTION ||
|
|
105
|
+
XML_TAG.test(description))
|
|
69
106
|
) {
|
|
70
107
|
problems.push('SKILL.md: the description must be 1-1024 characters with no XML tags');
|
|
71
108
|
}
|
package/src/sync/planner.ts
CHANGED
|
@@ -225,7 +225,27 @@ const planSkillLinks = (opts: { root: string }): ISyncAction[] => {
|
|
|
225
225
|
: { path, status: SyncStatuses.CREATED, apply: link };
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
// A link into the shipped skills whose skill this VULCAN no longer has (a downgrade, a retired
|
|
229
|
+
// skill) is sync's own and dead: taken back. Any other link or directory stays.
|
|
230
|
+
const shippedRoots = [stableSkills, join(BASE_DIRECTORY, SyncConstants.SKILLS_DIRECTORY)];
|
|
231
|
+
const linked = new Set(sources.map(source => basename(source)));
|
|
232
|
+
const stale = (existsSync(target) ? readdirSync(target) : [])
|
|
233
|
+
.filter(name => !linked.has(name))
|
|
234
|
+
.map(name => join(target, name))
|
|
235
|
+
.filter(
|
|
236
|
+
path =>
|
|
237
|
+
isSymlink({ path }) &&
|
|
238
|
+
!existsSync(path) &&
|
|
239
|
+
shippedRoots.some(root => readlinkSync(path).startsWith(`${root}/`)),
|
|
240
|
+
)
|
|
241
|
+
.map((path): ISyncAction => ({
|
|
242
|
+
path,
|
|
243
|
+
status: SyncStatuses.REMOVED,
|
|
244
|
+
note: 'a shipped skill this version no longer has',
|
|
245
|
+
apply: () => rmSync(path),
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
return [...links, ...shadowing, ...stale];
|
|
229
249
|
};
|
|
230
250
|
|
|
231
251
|
/** The committed project manifest (Project Contract v1, C1), rendered from the config. */
|