ai-runtime-kit 0.9.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -10,6 +10,81 @@ kit.
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.10.0] - 2026-05-14
14
+
15
+ **First M4 data point.** Kit's first `src/**` feature shipped
16
+ under the v0.6.0+ PRD → Feature → Spec → Plan → Task → TDD →
17
+ Implement → Verify → Review workflow with `TDD-Applies: true`
18
+ tasks. Two TDD pairs (test-commit-before-impl-commit) at 100%
19
+ test-first ordering.
20
+
21
+ ### Added
22
+ - **`validate` command** — checks `.ai/project/` tree
23
+ structural integrity. Walks every artifact (PRD / feature /
24
+ spec / plan / task / review) and verifies:
25
+ - Required `## Parent <Type>` sections per v0.9.0 INDEX §
26
+ Traceability rules.
27
+ - Cited parent paths resolve to real files on disk.
28
+ - `Status` values are in the allowed set per artifact type.
29
+ Reports errors (missing-parent / empty-parent /
30
+ unresolved-parent) and warnings (unexpected-status). Exits
31
+ 0 on clean, 1 on any error.
32
+ - `--json` flag for machine-parseable output.
33
+ - `--cwd <dir>` flag for non-default project root.
34
+ - `src/validate.js` (NEW) — pure validator module.
35
+ - `test/validate.test.js` (NEW) — 6 unit tests (clean tree,
36
+ missing parent, broken path, `(none — ...)` rendering,
37
+ JSON output, live dogfood smoke).
38
+
39
+ ### Changed
40
+ - `bin/cli.js` — dispatcher gains `validate` subcommand;
41
+ HELP lists it alongside `init` and `upgrade`.
42
+ - 7 historical specs (v0.4.0 / v0.5.0 / v0.5.1 / v0.6.0 /
43
+ v0.7.0 / v0.8.0 / v0.8.1) retrofitted with `## Parent
44
+ Feature` sections (pre-v0.6.0 use `(none —
45
+ pre-feature-layer)`; v0.8.1 uses `(none —
46
+ engineering-only)`; the rest cite real feature paths).
47
+ - 7 historical reviews retrofitted with `## Parent Spec`
48
+ sections. v0.9.0's spec/review already had theirs (the
49
+ convention F4 shipped was in use immediately on its own
50
+ review). Retrofit is project-side and not subject to
51
+ preflight.
52
+
53
+ ### Process
54
+ - **Kit's first `src/**` feature** under the new pipeline.
55
+ Branch was `feat/validate-cli` (not `chore/runtime-*`)
56
+ because no `runtime/**` paths in scope.
57
+ - **First non-runtime-scoped feature in the v0.6.0+
58
+ workflow** — preflight hook did not fire (correctly; no
59
+ `runtime/**` paths to gate on). The kit's discipline now
60
+ cleanly distinguishes governance-scoped (preflight gated)
61
+ from kit-code-scoped (regular feature branches).
62
+ - **First M4 data point.** Two TDD pairs:
63
+ - T1 (src/validate.js): test commit 16:07:26 → impl
64
+ 16:08:21 (Δ +55s).
65
+ - T2 (bin/cli.js): test commit 16:09:47 → impl 16:11:53
66
+ (Δ +2m6s).
67
+ Both pairs satisfy "test-commit precedes impl-commit." M4
68
+ score for this feature: 2/2 = 100%.
69
+ - **Validator caught a real concern during dogfood.** The
70
+ first run reported `TASK_STATUS.md` as a missing-parent
71
+ error; this file is a top-level status tracker (output of
72
+ `init`), not a task instance. Fix landed in C4: validator
73
+ now skips `EXCLUDED_FILENAMES`. The tool catching its own
74
+ edge case in its first ship cycle is the dogfood loop
75
+ working as intended.
76
+ - **Spec amended mid-implementation** (third occurrence —
77
+ v0.5.1, v0.7.0, and now v0.10.0). Original spec excluded
78
+ retrofitting historical artifacts; the validator's
79
+ credibility depends on its first dogfood pass being
80
+ clean, so the spec was amended pre-impl to include
81
+ retrofit work. Recorded in spec § Status with a comment.
82
+ - **24/24 tests pass** (18 prior + 6 new).
83
+ - **`validate` against this repo's `.ai/` tree**:
84
+ 3 PRDs / 5 features / 9 specs / 0 plans / 0 tasks /
85
+ 8 reviews / **0 errors / 0 warnings / Result: PASS**.
86
+ VM1 satisfied live on first ship.
87
+
13
88
  ## [0.9.0] - 2026-05-14
14
89
 
15
90
  **Closes the v0.6.0 nine-phase-workflow PRD's 4-feature
package/README.md CHANGED
@@ -10,6 +10,16 @@ npx ai-runtime-kit upgrade # existing kit consumer
10
10
 
11
11
  ## Status
12
12
 
13
+ **v0.10.0** — Adds the **`validate` command** — checks
14
+ `.ai/project/` tree structural integrity (every artifact carries
15
+ its required `## Parent <Type>` section; every cited parent path
16
+ resolves on disk; Status values are valid). Reports errors and
17
+ warnings; exits 0 on clean / 1 on errors; `--json` for machine
18
+ output. **First `src/**` feature shipped under the v0.6.0+
19
+ PRD → Feature → Spec → Plan → Task → TDD → Implement → Verify
20
+ → Review pipeline; first M4 data point — 2/2 TDD pairs at
21
+ 100% test-first ordering.**
22
+
13
23
  **v0.9.0** — Formalizes the **`## Parent <Type>` traceability
14
24
  convention** across all artifact templates. Every kit artifact
15
25
  (spec / plan / task / review) now carries a structural
package/bin/cli.js CHANGED
@@ -10,6 +10,7 @@ Usage: ai-runtime-kit <command> [options]
10
10
  Commands:
11
11
  init Initialize .ai/ runtime in the current directory.
12
12
  upgrade Upgrade .ai/runtime/ to the kit's current version.
13
+ validate Check .ai/project/ tree structural integrity.
13
14
  --help, -h Show this help.
14
15
  --version Show kit version.
15
16
 
@@ -38,6 +39,9 @@ async function main() {
38
39
  case 'upgrade':
39
40
  await require('../src/upgrade').run(rest);
40
41
  return;
42
+ case 'validate':
43
+ require('../src/validate').run(rest);
44
+ return;
41
45
  default:
42
46
  console.error(`ai-runtime-kit: unknown command '${cmd}'`);
43
47
  console.error('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-runtime-kit",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Reusable AI engineering runtime: BOOTSTRAP, workflows, safety, hooks, and templates for software projects driven by AI agents.",
5
5
  "bin": {
6
6
  "ai-runtime-kit": "./bin/cli.js"
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { parseArgs } = require('node:util');
6
+
7
+ // Per v0.9.0 INDEX § Traceability:
8
+ // PRD — no parent required (chain root)
9
+ // Feature — ## Parent PRD
10
+ // Spec — ## Parent Feature
11
+ // Plan — ## Parent Spec
12
+ // Task — ## Parent Spec + ## Parent Plan
13
+ // Review — ## Parent Spec
14
+ const ARTIFACT_RULES = {
15
+ prds: { dir: 'prds', required: [] },
16
+ features: { dir: 'features', required: ['Parent PRD'] },
17
+ specs: { dir: 'specs', required: ['Parent Feature'] },
18
+ plans: { dir: 'plans', required: ['Parent Spec'] },
19
+ tasks: { dir: 'tasks', required: ['Parent Spec', 'Parent Plan'] },
20
+ reviews: { dir: 'reviews', required: ['Parent Spec'] },
21
+ };
22
+
23
+ const VALID_STATUSES = {
24
+ prds: ['DRAFT', 'APPROVED', 'REJECTED', 'SUPERSEDED'],
25
+ features: ['DRAFT', 'APPROVED', 'REJECTED', 'SUPERSEDED'],
26
+ specs: ['DRAFT', 'APPROVED', 'REJECTED', 'SUPERSEDED'],
27
+ plans: ['PLANNED', 'IN_PROGRESS', 'DONE'],
28
+ tasks: ['TODO', 'IN_PROGRESS', 'IN_REVIEW', 'DONE'],
29
+ };
30
+
31
+ // Files within an artifact dir that are NOT instances of that
32
+ // artifact type. TASK_STATUS.md is a top-level status tracker
33
+ // produced by `init`, not a per-task instance.
34
+ const EXCLUDED_FILENAMES = new Set(['TASK_STATUS.md']);
35
+
36
+ function walkMd(dir) {
37
+ if (!fs.existsSync(dir)) return [];
38
+ const out = [];
39
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
40
+ if (EXCLUDED_FILENAMES.has(entry.name)) continue;
41
+ const p = path.join(dir, entry.name);
42
+ if (entry.isDirectory()) out.push(...walkMd(p));
43
+ else if (entry.isFile() && entry.name.endsWith('.md')) out.push(p);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function parseSections(content) {
49
+ // Returns { sectionName: bodyString }.
50
+ // Sections are top-level `## ` headings.
51
+ const lines = content.split('\n');
52
+ const sections = {};
53
+ let cur = null;
54
+ let buf = [];
55
+ for (const line of lines) {
56
+ const m = line.match(/^##\s+(.+?)\s*$/);
57
+ if (m) {
58
+ if (cur !== null) sections[cur] = buf.join('\n').trim();
59
+ cur = m[1].trim();
60
+ buf = [];
61
+ } else if (cur !== null) {
62
+ buf.push(line);
63
+ }
64
+ }
65
+ if (cur !== null) sections[cur] = buf.join('\n').trim();
66
+ return sections;
67
+ }
68
+
69
+ function firstValueLine(body) {
70
+ // Returns first non-empty, non-comment line; strips surrounding backticks.
71
+ for (const raw of body.split('\n')) {
72
+ const line = raw.trim();
73
+ if (!line) continue;
74
+ if (line.startsWith('<!--')) continue;
75
+ return line.replace(/^`/, '').replace(/`$/, '');
76
+ }
77
+ return null;
78
+ }
79
+
80
+ function isNoneRendering(value) {
81
+ return value !== null && value.startsWith('(none');
82
+ }
83
+
84
+ function validate(projectRoot, _options = {}) {
85
+ const errors = [];
86
+ const warnings = [];
87
+ const summary = { prds: 0, features: 0, specs: 0, plans: 0, tasks: 0, reviews: 0 };
88
+
89
+ const aiProject = path.join(projectRoot, '.ai', 'project');
90
+ if (!fs.existsSync(aiProject)) {
91
+ return { errors, warnings, summary };
92
+ }
93
+
94
+ for (const [artifact, rule] of Object.entries(ARTIFACT_RULES)) {
95
+ const dir = path.join(aiProject, rule.dir);
96
+ const files = walkMd(dir);
97
+ summary[artifact] = files.length;
98
+
99
+ for (const file of files) {
100
+ const relFile = path.relative(projectRoot, file);
101
+ const content = fs.readFileSync(file, 'utf8');
102
+ const sections = parseSections(content);
103
+
104
+ for (const parentName of rule.required) {
105
+ const sectionBody = sections[parentName];
106
+ if (sectionBody === undefined) {
107
+ errors.push({
108
+ artifact,
109
+ file: relFile,
110
+ type: 'missing-parent',
111
+ message: `Missing required ## ${parentName} section`,
112
+ });
113
+ continue;
114
+ }
115
+ const value = firstValueLine(sectionBody);
116
+ if (value === null) {
117
+ errors.push({
118
+ artifact,
119
+ file: relFile,
120
+ type: 'empty-parent',
121
+ message: `## ${parentName} section is empty`,
122
+ });
123
+ continue;
124
+ }
125
+ if (isNoneRendering(value)) continue;
126
+ const resolved = path.join(projectRoot, value);
127
+ if (!fs.existsSync(resolved)) {
128
+ errors.push({
129
+ artifact,
130
+ file: relFile,
131
+ type: 'unresolved-parent',
132
+ message: `## ${parentName} points to non-existent path: ${value}`,
133
+ });
134
+ }
135
+ }
136
+
137
+ const allowedStatuses = VALID_STATUSES[artifact];
138
+ if (allowedStatuses && sections.Status !== undefined) {
139
+ const statusValue = firstValueLine(sections.Status);
140
+ if (statusValue !== null && !allowedStatuses.includes(statusValue)) {
141
+ warnings.push({
142
+ artifact,
143
+ file: relFile,
144
+ type: 'unexpected-status',
145
+ message: `Status "${statusValue}" not in allowed values: ${allowedStatuses.join(', ')}`,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ return { errors, warnings, summary };
153
+ }
154
+
155
+ function run(argv) {
156
+ let parsed;
157
+ try {
158
+ parsed = parseArgs({
159
+ args: argv,
160
+ options: {
161
+ cwd: { type: 'string' },
162
+ json: { type: 'boolean', default: false },
163
+ help: { type: 'boolean', short: 'h', default: false },
164
+ },
165
+ strict: true,
166
+ allowPositionals: false,
167
+ });
168
+ } catch (e) {
169
+ console.error(`validate: ${e.message}`);
170
+ process.exit(1);
171
+ }
172
+
173
+ if (parsed.values.help) {
174
+ printHelp();
175
+ return;
176
+ }
177
+
178
+ const cwd = path.resolve(parsed.values.cwd ?? process.cwd());
179
+ const result = validate(cwd);
180
+ const status = result.errors.length === 0 ? 'PASS' : 'FAIL';
181
+
182
+ if (parsed.values.json) {
183
+ console.log(JSON.stringify({ ...result, result: status }, null, 2));
184
+ } else {
185
+ console.log(`Validating .ai/project/ at ${cwd}\n`);
186
+ for (const [name, count] of Object.entries(result.summary)) {
187
+ console.log(` ${name.padEnd(10)} ${count}`);
188
+ }
189
+ console.log('');
190
+ if (result.errors.length > 0) {
191
+ console.log(`Errors: ${result.errors.length}`);
192
+ for (const e of result.errors) {
193
+ console.log(` - ${e.file}: ${e.type}: ${e.message}`);
194
+ }
195
+ } else {
196
+ console.log('Errors: none');
197
+ }
198
+ if (result.warnings.length > 0) {
199
+ console.log(`Warnings: ${result.warnings.length}`);
200
+ for (const w of result.warnings) {
201
+ console.log(` - ${w.file}: ${w.type}: ${w.message}`);
202
+ }
203
+ } else {
204
+ console.log('Warnings: none');
205
+ }
206
+ console.log('');
207
+ console.log(
208
+ `Result: ${status === 'PASS' ? 'PASS (clean tree)' : `FAIL (${result.errors.length} errors)`}`,
209
+ );
210
+ }
211
+
212
+ process.exit(result.errors.length === 0 ? 0 : 1);
213
+ }
214
+
215
+ function printHelp() {
216
+ console.log(`ai-runtime-kit validate [options]
217
+
218
+ Validate the .ai/project/ tree's structural integrity. Walks
219
+ every artifact and checks that:
220
+
221
+ - Required ## Parent <Type> sections are present per the
222
+ v0.9.0 INDEX § Traceability rules.
223
+ - Cited parent paths resolve to real files on disk.
224
+ - Status values are in the allowed set per artifact type.
225
+
226
+ Options:
227
+ --cwd <dir> Target project root (default: process.cwd())
228
+ --json Output structured JSON instead of human text
229
+ -h, --help Show this help
230
+
231
+ Exits 0 on no errors (warnings allowed and printed); 1 on any
232
+ error. See .ai/runtime/INDEX.md § Traceability for the rules.`);
233
+ }
234
+
235
+ module.exports = { validate, run, printHelp };