ai-runtime-kit 0.5.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.
@@ -6,6 +6,15 @@ DRAFT
6
6
  <!-- Allowed: DRAFT | APPROVED | REJECTED | SUPERSEDED.
7
7
  See .ai/runtime/workflows/bug-fix.md for lifecycle rules. -->
8
8
 
9
+ ## Parent Feature
10
+
11
+ (none — bug-fix workflow)
12
+ <!-- Bug-fix workflow skips Step 0.5 (no feature slicing) — the
13
+ fix is corrective, not a sliced PRD feature. Override with
14
+ a real path only if the bug fix happens to be part of a
15
+ larger PRD-driven effort. See .ai/runtime/INDEX.md
16
+ § Traceability for the full chain. -->
17
+
9
18
  ## 1. Goal
10
19
 
11
20
  Describe the corrective outcome in one paragraph. Focus on what
@@ -6,9 +6,20 @@ Describe the specific engineering goal.
6
6
 
7
7
  ---
8
8
 
9
- ## Related Spec
9
+ ## Parent Spec
10
10
 
11
- -
11
+ `.ai/project/specs/YYYY-MM-DD-<slug>/spec.md`
12
+ <!-- Required. Single path. -->
13
+
14
+ ---
15
+
16
+ ## Parent Plan
17
+
18
+ `.ai/project/plans/YYYY-MM-DD-<slug>/plan.md`
19
+ <!-- Required. Single path. For hot-fix tasks created outside
20
+ the normal plan-first workflow, render
21
+ `(none — direct task)`. See .ai/runtime/INDEX.md
22
+ § Traceability for the full chain. -->
12
23
 
13
24
  ---
14
25
 
@@ -64,6 +75,18 @@ TODO
64
75
 
65
76
  executor
66
77
 
78
+ ## TDD-Applies
79
+
80
+ false
81
+ <!-- Allowed: true | false.
82
+ Rule: true if the task introduces or modifies runtime
83
+ behavior (code that runs); false for documentation, refactor
84
+ with no behavior change, or config / packaging / metadata
85
+ only. When true, follow workflow Step 1.5 — a failing-test
86
+ commit must precede the implementation commit. When false,
87
+ state a one-line reason here so the skip is explicit
88
+ rather than implicit. -->
89
+
67
90
  ## Blocked By
68
91
 
69
92
  None.
@@ -50,6 +50,12 @@ Metrics, User Stories, Out of Scope, Open Questions,
50
50
  Stakeholders. Engineering details (architecture, data flow, code
51
51
  contracts) belong in the downstream spec, not the PRD.
52
52
 
53
+ Read `.ai/runtime/agents/prd-writer.md` to anchor the role — it
54
+ scopes the agent's responsibilities, inputs, outputs, and
55
+ constraints. The procedural depth (the 11-step elicit-then-write
56
+ flow) lives in a project-side skill referenced from the agent
57
+ file; concept lattice is **agent = WHO, skill = HOW.**
58
+
53
59
  Skip this step when:
54
60
 
55
61
  - the change is corrective (use `bug-fix.md` instead — bug fixes
@@ -60,9 +66,43 @@ Skip this step when:
60
66
  intent.
61
67
 
62
68
  PRD lifecycle mirrors specs: `DRAFT → APPROVED → REJECTED →
63
- SUPERSEDED`. An APPROVED PRD authorizes spec drafting. The
64
- downstream spec MUST reference its PRD by path in §1 Goal so
65
- review can verify the spec satisfies the PRD.
69
+ SUPERSEDED`. An APPROVED PRD authorizes feature slicing (Step
70
+ 0.5).
71
+
72
+ ### 0.5. Slice into Features
73
+
74
+ When Step 0 ran (a PRD exists), slice it into **≥1 feature
75
+ docs** under:
76
+
77
+ ```txt
78
+ .ai/project/features/YYYY-MM-DD-<slug>/feature.md
79
+ ```
80
+
81
+ One feature per discrete capability that can be specced and
82
+ implemented independently. The feature doc answers "what slice
83
+ of the PRD does this satisfy, and what does done look like" —
84
+ mid-level between PRD ("what & why") and spec ("how").
85
+ Engineering details (architecture, contracts, test plan) belong
86
+ in the spec, not the feature.
87
+
88
+ Use `.ai/runtime/features/_template.md` as the starting point.
89
+ Each feature must cite its parent PRD in `## Parent PRD`; the
90
+ PRD's `## Downstream Spec` section lists the features that
91
+ derived from it.
92
+
93
+ **Mandatory whenever Step 0 ran.** Same skip criteria as Step 0:
94
+ bug-fix workflow and engineering-only changes skip Step 0
95
+ entirely, so they skip Step 0.5 too.
96
+
97
+ Single-feature PRDs still produce **one feature doc with the
98
+ full template structure** — sections may contain "see parent
99
+ PRD" pointers when the content is fully captured upstream, but
100
+ no 5-line stub allowance (consistent shape protects
101
+ traceability tooling and audit clarity).
102
+
103
+ Feature lifecycle mirrors specs: `DRAFT → APPROVED → REJECTED →
104
+ SUPERSEDED`. An APPROVED feature authorizes spec drafting for
105
+ that feature.
66
106
 
67
107
  ### 1. Define Spec
68
108
 
@@ -72,9 +112,12 @@ Create a feature spec under:
72
112
  .ai/project/specs/YYYY-MM-DD-feature-name/spec.md
73
113
  ```
74
114
 
75
- If the spec is downstream of a PRD (Step 0), §1 Goal must cite
76
- the PRD path so reviewers can check that the spec covers the
77
- PRD's requirements without quietly expanding scope.
115
+ If the spec is downstream of a feature (Step 0.5), §1 Goal must
116
+ cite the feature path (the feature in turn cites its parent
117
+ PRD, so the chain assembles upward). Reviewers walk the chain to
118
+ check that the spec covers the feature's requirements without
119
+ quietly expanding scope and that the feature covers its share of
120
+ the PRD's metrics.
78
121
 
79
122
  Spec must include:
80
123
 
@@ -86,8 +129,45 @@ Spec must include:
86
129
  - Verification Commands
87
130
  - Rollback Plan
88
131
 
132
+ ### 1.5. TDD Phase (per task, when applicable)
133
+
134
+ For each task produced under the spec's plan: if the task
135
+ carries `TDD-Applies: true` (see
136
+ `.ai/runtime/tasks/_template.md` § TDD-Applies), a **failing-
137
+ test commit must land before** that task's implementation
138
+ commit. The role file for this step is
139
+ `.ai/runtime/agents/tdd-writer.md`.
140
+
141
+ **Trigger.** Task's `TDD-Applies` value is `true`. The
142
+ boundary follows parent-PRD-style rules: behavior-changing
143
+ tasks → `true`; doc / refactor-with-no-behavior-change /
144
+ config-only → `false`.
145
+
146
+ **Required artifact.** A separate commit whose diff contains
147
+ only the failing test(s) for the task. Verify red (test
148
+ actually fails) before declaring the step done. The
149
+ implementation commit lands on a distinct commit hash with a
150
+ later timestamp, satisfying the test that was just authored.
151
+
152
+ **Skip rule.** Tasks with `TDD-Applies: false` skip this step
153
+ entirely. The skip is explicit on the task itself (the
154
+ `## TDD-Applies` section in the task file states the boolean
155
+ plus a one-line reason); never implicit.
156
+
157
+ **Per-task semantics.** This is not a single workflow-wide
158
+ pause. Step 1.5 runs once *per applicable task*, interleaved
159
+ with that task's implementation. A feature with N
160
+ TDD-applicable tasks fires Step 1.5 N times.
161
+
89
162
  ### 2. Execute with Claude Code
90
163
 
164
+ > Step 1.5 is a per-task prerequisite for any task with
165
+ > `TDD-Applies: true`. The execute step below covers plan
166
+ > authoring, task production, implementation, and verification
167
+ > as one umbrella; the TDD discipline runs *inside* that
168
+ > umbrella, per applicable task, before each task's
169
+ > implementation commit.
170
+
91
171
  Claude Code must read:
92
172
 
93
173
  - `.ai/runtime/agents/executor.md`
package/src/init.js CHANGED
@@ -11,6 +11,7 @@ const { isPathGitignored } = require('./git');
11
11
 
12
12
  const PROJECT_SKELETON_DIRS = [
13
13
  'prds',
14
+ 'features',
14
15
  'specs',
15
16
  'plans',
16
17
  'tasks',
@@ -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 };