@naswerks/engineering-loop 0.5.0 → 0.5.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/references/docs-workflow.md +6 -4
- package/references/engineering-loop.md +1 -1
- package/skills/docs-status/SKILL.md +2 -1
- package/skills/init/SKILL.md +2 -2
- package/skills/init/scripts/doctor.mjs +5 -6
- package/skills/init/scripts/owned.mjs +26 -0
- package/skills/init/scripts/refresh.mjs +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naswerks",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The engineering loop: a knowledge loop (docs-*) and an execution loop (spec-*) for repositories driven by hosted agent sessions, plus init to make a repository ready for both.",
|
|
5
5
|
"author": { "name": "naswerks" },
|
|
6
6
|
"license": "MIT",
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ version is the one in `package.json` and `.claude-plugin/plugin.json`, which mus
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.5.1] - 2026-09-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- A doc on a curated menu meets the loop's standard when it names the code it describes in either form: a
|
|
13
|
+
`## Key Files` section, or a frontmatter `related_files` list. The doctor checked for the heading alone and
|
|
14
|
+
flagged docs that name their code in frontmatter.
|
|
15
|
+
- A patch release never asks a copy of the method docs to change, so `init doctor` and `init refresh` read a
|
|
16
|
+
stamp as behind only when its minor version is.
|
|
17
|
+
|
|
8
18
|
## [0.5.0] - 2026-09-24
|
|
9
19
|
|
|
10
20
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naswerks/engineering-loop",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The engineering loop as a Claude Code plugin: a knowledge loop (docs-*), an execution loop (spec-*), and init for a fresh repository. The package root is the plugin root.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- naswerks-loop: version=0.5.
|
|
1
|
+
<!-- naswerks-loop: version=0.5.1 -->
|
|
2
2
|
# Docs Workflow
|
|
3
3
|
|
|
4
4
|
> How knowledge flows through `docs/`: the loop, the folder rules, the templates, and the `docs-*` skills.
|
|
@@ -110,8 +110,9 @@ it once and never again. A line near its top says which of two shapes it has, an
|
|
|
110
110
|
| `<!-- naswerks-loop: menu=curated; index={path} -->` | the docs this repository's seats read; `{path}` indexes every doc | into `{path}`, the way that index is kept, and onto the menu only when it meets the loop's standard and a seat should read it |
|
|
111
111
|
|
|
112
112
|
**The loop's standard for a doc on a curated menu:** it exists, it carries the header § Doc metadata
|
|
113
|
-
prescribes at a decided status (not the word for `draft`), and it
|
|
114
|
-
checks exactly that, for every doc
|
|
113
|
+
prescribes at a decided status (not the word for `draft`), and it names the code it describes — a
|
|
114
|
+
`## Key Files` section, or a frontmatter `related_files` list. `init doctor` checks exactly that, for every doc
|
|
115
|
+
a curated menu names.
|
|
115
116
|
|
|
116
117
|
**Silence.** A section that says nothing about an item leaves the loop's default: a menu with no shape line
|
|
117
118
|
lists every living doc (`menu=all`), no gate runs on a commit, the base branch is the remote's
|
|
@@ -148,7 +149,8 @@ Keep what the skills need to find, change the words to yours:
|
|
|
148
149
|
the menu then lists only the docs its seats read, each meeting the loop's standard, and every other doc
|
|
149
150
|
stays in the repository's own index, kept the way it already is.
|
|
150
151
|
|
|
151
|
-
**The version stamp** on line 1 names the pack version whose repository-owned sections this copy carries.
|
|
152
|
+
**The version stamp** on line 1 names the pack version whose repository-owned sections this copy carries. A
|
|
153
|
+
patch release never asks a copy to change, so a stamp is behind only when its minor version is.
|
|
152
154
|
The loop text around them is this repository's copy from whenever it was taken: `init doctor` never grades
|
|
153
155
|
it, and `init refresh` never touches it — refresh adds a section or a rails item a newer pack reads, and
|
|
154
156
|
restamps.
|
|
@@ -39,7 +39,8 @@ just counts.
|
|
|
39
39
|
- **menu drift** — read the menu's shape line (`rg -n 'naswerks-loop: menu=' docs/_meta/doc-index.md`;
|
|
40
40
|
none means `menu=all`). On `menu=all`: a living doc the menu does not name. On `menu=curated`: a doc the
|
|
41
41
|
menu names that falls short of the loop's standard (`docs/_meta/docs-workflow.md` § The
|
|
42
|
-
repository-owned sections — a header at a decided status and a `## Key Files`
|
|
42
|
+
repository-owned sections — a header at a decided status, and its code named in a `## Key Files`
|
|
43
|
+
section or a frontmatter `related_files` list).
|
|
43
44
|
|
|
44
45
|
5. **Open issues.** Grep living docs for an `## Open Issues` section; list which docs/features carry
|
|
45
46
|
unresolved, code-verified problems (and how many) so known risks surface at orientation instead of
|
package/skills/init/SKILL.md
CHANGED
|
@@ -190,13 +190,13 @@ FAIL, so a CI job or a control plane can call it.
|
|
|
190
190
|
| Row | FAIL | DEGRADE | ok |
|
|
191
191
|
|---|---|---|---|
|
|
192
192
|
| each of the eight folders | missing | README missing | present |
|
|
193
|
-
| `_meta/docs-workflow.md`, `_meta/engineering-loop.md` | missing, or no stamp on line 1 (or on the first line after a frontmatter block) | stamp behind the pack's
|
|
193
|
+
| `_meta/docs-workflow.md`, `_meta/engineering-loop.md` | missing, or no stamp on line 1 (or on the first line after a frontmatter block) | stamp's minor version behind the pack's (`init refresh` restamps; a patch never asks a copy to change) | stamp's minor equals the pack's |
|
|
194
194
|
| each repository-owned heading, in its file | — | unmarked (an earlier spelling) or missing — `init refresh` marks or adds it | present |
|
|
195
195
|
| each item of § This repository's rails (repository-owned) | — | missing, or still its absent-line | recorded |
|
|
196
196
|
| `_meta/doc-index.md` and its three slot sections | missing | a slot carries only the absent-line | filled |
|
|
197
197
|
| the menu's shape line | an unknown shape | a curated menu names no index, or an index that does not exist | `menu=all` (also a menu with no shape line), or `menu=curated` with its index |
|
|
198
198
|
| every living doc a `menu=all` menu should list | — | a doc under the four living folders the menu does not name | named |
|
|
199
|
-
| every doc a menu names | the path does not exist (a row saying `not detected` is exempt) | on a curated menu: no status in either header form, the word `draft`, or no `## Key Files` | meets the loop's standard |
|
|
199
|
+
| every doc a menu names | the path does not exist (a row saying `not detected` is exempt) | on a curated menu: no status in either header form, the word `draft`, or no named code (a `## Key Files` section or a frontmatter `related_files` list) | meets the loop's standard |
|
|
200
200
|
| the four floor docs | missing | the word `draft` (read from either header form), or an `<!-- init: -->` left in | a decided status |
|
|
201
201
|
| the seven standard docs | absent AND no doc-index row saying `not detected` | the word `draft` when present; absent with its row | present and decided, or absent with its row |
|
|
202
202
|
| `CLAUDE.md` / `AGENTS.md` stanza · `.claude/settings.json` pin | — | missing | present |
|
|
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
5
5
|
import { join, resolve, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import { OWNED, compareVersions, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
|
|
8
|
+
import { OWNED, compareVersions, compareMinor, frontmatterListCount, linesOf, bare, h2Index, findSection, railsItems } from './owned.mjs';
|
|
9
9
|
|
|
10
10
|
const repo = resolve(process.argv[2] || process.cwd());
|
|
11
11
|
const packRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
@@ -40,9 +40,8 @@ for (const name of ['docs-workflow.md', 'engineering-loop.md']) {
|
|
|
40
40
|
const m = STAMP.exec(firstBodyLine(text));
|
|
41
41
|
if (!m) { row('FAIL', `docs/_meta/${name}`, 'no naswerks-loop stamp on line 1 (or on the first line after a frontmatter block) — an unstamped copy cannot be told from a stale one'); continue; }
|
|
42
42
|
if (!packVersion) { row('DEGRADE', `docs/_meta/${name}`, `stamped ${m[1]}; the pack's own version could not be read`); continue; }
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
else row('ok', `docs/_meta/${name}`, `stamp ${m[1]}`);
|
|
43
|
+
if (compareMinor(m[1], packVersion) < 0) row('DEGRADE', `docs/_meta/${name}`, `stamp ${m[1]} is behind the pack ${packVersion} — run init refresh: it adds this version's repository-owned sections and restamps, and never touches the loop text`);
|
|
44
|
+
else row('ok', `docs/_meta/${name}`, compareVersions(m[1], packVersion) < 0 ? `stamp ${m[1]} (the pack is ${packVersion}: a patch never asks a copy to change)` : `stamp ${m[1]}`);
|
|
46
45
|
|
|
47
46
|
// ── 2b. the repository-owned sections: present under the heading a skill looks them up by ─────────
|
|
48
47
|
const lines = linesOf(text);
|
|
@@ -123,7 +122,7 @@ function docRow(relPath, required) {
|
|
|
123
122
|
// every living doc; menu=curated lists the docs the seats read and names the index that lists every doc. A
|
|
124
123
|
// menu with no shape line is menu=all. Every doc a menu names must exist (a row saying `not detected` is
|
|
125
124
|
// exempt); a curated menu's docs must also meet the loop's standard: a status in either header form, not
|
|
126
|
-
// draft, and a `## Key Files` section.
|
|
125
|
+
// draft, and names the code it describes: a `## Key Files` section, or a frontmatter `related_files` list.
|
|
127
126
|
{
|
|
128
127
|
const menu = read(join(repo, 'docs', '_meta', 'doc-index.md'));
|
|
129
128
|
if (menu !== null) {
|
|
@@ -151,7 +150,7 @@ function docRow(relPath, required) {
|
|
|
151
150
|
const st = statusOf(text);
|
|
152
151
|
if (st === null) short.push('no status in either header form');
|
|
153
152
|
else if (st.value === 'draft') short.push('status draft');
|
|
154
|
-
if (!/^## Key Files\b/m.test(text)) short.push('no ## Key Files');
|
|
153
|
+
if (!/^## Key Files\b/m.test(text) && frontmatterListCount(text, 'related_files') === 0) short.push('names no code (no ## Key Files section, no related_files list)');
|
|
155
154
|
row(short.length ? 'DEGRADE' : 'ok', `menu: docs/${p}`, short.length ? `below the loop's standard: ${short.join(', ')}` : "meets the loop's standard");
|
|
156
155
|
}
|
|
157
156
|
} else if (shape === 'all') {
|
|
@@ -27,6 +27,32 @@ export function compareVersions(a, b) {
|
|
|
27
27
|
return 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// A patch release never changes what a copy of the method docs carries, so a stamp is behind only when its
|
|
31
|
+
// major.minor is.
|
|
32
|
+
export function compareMinor(a, b) {
|
|
33
|
+
const pa = a.split(/[.-]/).map((x) => parseInt(x, 10) || 0), pb = b.split(/[.-]/).map((x) => parseInt(x, 10) || 0);
|
|
34
|
+
for (let i = 0; i < 2; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// How many entries a frontmatter key lists: an inline `[a, b]`, indented `- item` lines, or one scalar. Zero
|
|
39
|
+
// when the key is absent or empty.
|
|
40
|
+
export function frontmatterListCount(text, key) {
|
|
41
|
+
const t = text.replace(/\r\n/g, '\n');
|
|
42
|
+
if (!t.startsWith('---\n')) return 0;
|
|
43
|
+
const end = t.indexOf('\n---', 4);
|
|
44
|
+
if (end < 0) return 0;
|
|
45
|
+
const lines = t.slice(4, end).split('\n');
|
|
46
|
+
const i = lines.findIndex((l) => l.startsWith(`${key}:`));
|
|
47
|
+
if (i < 0) return 0;
|
|
48
|
+
const inline = lines[i].slice(key.length + 1).trim();
|
|
49
|
+
if (inline.startsWith('[')) return inline.replace(/^\[|\]$/g, '').split(',').map((x) => x.trim()).filter(Boolean).length;
|
|
50
|
+
if (inline) return 1;
|
|
51
|
+
let n = 0;
|
|
52
|
+
for (let j = i + 1; j < lines.length && /^\s+-\s*\S/.test(lines[j]); j++) n++;
|
|
53
|
+
return n;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
// A file as lines that keep their own terminators, so an untouched line is written back byte for byte.
|
|
31
57
|
export function linesOf(text) {
|
|
32
58
|
return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join, resolve, dirname } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { OWNED, STAMP, compareVersions, linesOf, bare, eolOf, h2Index, findSection, railsItems, stampIndex, mapOf } from './owned.mjs';
|
|
9
|
+
import { OWNED, STAMP, compareVersions, compareMinor, linesOf, bare, eolOf, h2Index, findSection, railsItems, stampIndex, mapOf } from './owned.mjs';
|
|
10
10
|
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
12
|
const write = args.includes('--write');
|
|
@@ -92,13 +92,14 @@ for (const file of ['docs-workflow.md', 'engineering-loop.md']) {
|
|
|
92
92
|
const stampAt = stampIndex(lines);
|
|
93
93
|
const current = STAMP.exec(bare(lines[stampAt]))[2];
|
|
94
94
|
const cmp = compareVersions(current, packVersion);
|
|
95
|
-
if (
|
|
95
|
+
if (compareMinor(current, packVersion) < 0) {
|
|
96
96
|
const raw = lines[stampAt];
|
|
97
97
|
const bom = raw.startsWith('') ? '' : '';
|
|
98
98
|
lines[stampAt] = bom + raw.slice(bom.length).replace(STAMP, `$1${packVersion}$3`);
|
|
99
99
|
row(shown, 'stamp', `${current} to ${packVersion}`);
|
|
100
100
|
changed = true;
|
|
101
101
|
} else if (cmp > 0) row(shown, 'stamp', `${current} — ahead of this pack; left as it is`);
|
|
102
|
+
else if (cmp < 0) row(shown, 'stamp', `${current} — the pack is ${packVersion}, a patch: a patch never asks a copy to change`);
|
|
102
103
|
else row(shown, 'stamp', `${current} — current`);
|
|
103
104
|
|
|
104
105
|
if (changed) {
|