fullstack-agentic-flow 2.0.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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -0
  3. package/dist/src/cli/args.js +74 -0
  4. package/dist/src/cli/commands/doctor.js +133 -0
  5. package/dist/src/cli/commands/init.js +103 -0
  6. package/dist/src/cli/commands/shared.js +20 -0
  7. package/dist/src/cli/commands/tools.js +21 -0
  8. package/dist/src/cli/commands/update.js +21 -0
  9. package/dist/src/cli/main.js +6 -0
  10. package/dist/src/cli/output.js +11 -0
  11. package/dist/src/cli/prompter.js +13 -0
  12. package/dist/src/cli/report.js +13 -0
  13. package/dist/src/cli/run.js +48 -0
  14. package/dist/src/domain/canon-mapping.js +26 -0
  15. package/dist/src/domain/claude-md-mode.js +17 -0
  16. package/dist/src/domain/enum-parse.js +9 -0
  17. package/dist/src/domain/errors.js +13 -0
  18. package/dist/src/domain/pipeline-config.js +58 -0
  19. package/dist/src/domain/runtime.js +19 -0
  20. package/dist/src/domain/tool.js +129 -0
  21. package/dist/src/domain/topology.js +39 -0
  22. package/dist/src/install/adapters.js +39 -0
  23. package/dist/src/install/applier.js +150 -0
  24. package/dist/src/install/detect.js +70 -0
  25. package/dist/src/install/managed-section.js +35 -0
  26. package/dist/src/install/manifest.js +63 -0
  27. package/dist/src/install/planned-file.js +77 -0
  28. package/dist/src/install/planner.js +139 -0
  29. package/dist/src/install/rules-preset.js +37 -0
  30. package/dist/src/install/setup-guide.js +60 -0
  31. package/dist/src/io/fs.js +39 -0
  32. package/dist/src/io/paths.js +6 -0
  33. package/dist/src/io/shell.js +25 -0
  34. package/dist/src/registry/commands.js +52 -0
  35. package/dist/src/registry/subagents.js +52 -0
  36. package/dist/src/registry/tools.js +99 -0
  37. package/package.json +53 -0
  38. package/payload/ai-agents/agents/00-bootstrap.md +473 -0
  39. package/payload/ai-agents/agents/00a-scaffold.md +295 -0
  40. package/payload/ai-agents/agents/00b-module-scaffold.md +108 -0
  41. package/payload/ai-agents/agents/01-intake.md +308 -0
  42. package/payload/ai-agents/agents/01b-impact-analysis.md +274 -0
  43. package/payload/ai-agents/agents/02a-dependency-map.md +247 -0
  44. package/payload/ai-agents/agents/02b-backend-contract.md +434 -0
  45. package/payload/ai-agents/agents/02c-ui-contract.md +367 -0
  46. package/payload/ai-agents/agents/02d-interface-seam.md +373 -0
  47. package/payload/ai-agents/agents/03a-backend-sequence.md +233 -0
  48. package/payload/ai-agents/agents/03b-ui-sequence.md +230 -0
  49. package/payload/ai-agents/agents/04a-backend-implementer.md +290 -0
  50. package/payload/ai-agents/agents/04b-ui-implementer.md +302 -0
  51. package/payload/ai-agents/agents/05-ci-validator.md +125 -0
  52. package/payload/ai-agents/agents/06-qa-spec.md +249 -0
  53. package/payload/ai-agents/agents/07-security.md +228 -0
  54. package/payload/ai-agents/agents/08-performance.md +208 -0
  55. package/payload/ai-agents/agents/09a-feature-notes.md +156 -0
  56. package/payload/ai-agents/agents/09b-release.md +111 -0
  57. package/payload/ai-agents/architecture-context.template.md +554 -0
  58. package/payload/ai-agents/archive/README.md +22 -0
  59. package/payload/ai-agents/canon/backend-canon.md +295 -0
  60. package/payload/ai-agents/canon/frontend-canon.md +47 -0
  61. package/payload/ai-agents/canon/typed-data.md +158 -0
  62. package/payload/ai-agents/observations/README.md +11 -0
  63. package/payload/ai-agents/scripts/check-test-with-change.mjs +103 -0
  64. package/payload/ai-agents/scripts/check-typed-boundaries.mjs +329 -0
  65. package/payload/ai-agents/scripts/lib/config.mjs +63 -0
  66. package/payload/ai-agents/scripts/lib/glob.mjs +40 -0
  67. package/payload/ai-agents/state/README.md +108 -0
  68. package/payload/ai-agents/state/current-stage.md +10 -0
  69. package/payload/ai-agents/templates/README.md +22 -0
  70. package/payload/ai-agents/templates/ci/github-actions.rules.yml +35 -0
  71. package/payload/ai-agents/templates/ci/gitlab-ci.rules.yml +30 -0
  72. package/payload/ai-agents/toolchain.md +214 -0
  73. package/payload/commands/bootstrap.md +17 -0
  74. package/payload/commands/contract.md +64 -0
  75. package/payload/commands/finalize.md +26 -0
  76. package/payload/commands/impact.md +25 -0
  77. package/payload/commands/implement.md +58 -0
  78. package/payload/commands/intake.md +16 -0
  79. package/payload/commands/perf.md +20 -0
  80. package/payload/commands/qa.md +15 -0
  81. package/payload/commands/release.md +22 -0
  82. package/payload/commands/resume.md +52 -0
  83. package/payload/commands/scaffold-module.md +20 -0
  84. package/payload/commands/scaffold.md +35 -0
  85. package/payload/commands/seam.md +28 -0
  86. package/payload/commands/security.md +16 -0
  87. package/payload/commands/sequence.md +22 -0
  88. package/payload/commands/status.md +37 -0
  89. package/payload/legacy/v1-checksums.json +197 -0
  90. package/payload/root/contracts-README.md +19 -0
  91. package/payload/root/pipeline-section.md +33 -0
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * Rule T1 (canon/typed-data.md): no untyped array, map, `mixed`, or `any` in a
5
+ * signature that crosses a boundary. Diff-based: only added lines are judged,
6
+ * so enabling the rule never fails untouched history.
7
+ *
8
+ * Usage:
9
+ * node .ai-agents/scripts/check-typed-boundaries.mjs --base origin/main # CI
10
+ * node .ai-agents/scripts/check-typed-boundaries.mjs --staged # pre-commit
11
+ *
12
+ * Paths come from .ai-agents/rules.config.json → typedBoundaries.
13
+ * Suppress one line with a trailing comment: typed-boundary-ok: <reason>
14
+ */
15
+ import { execFileSync } from 'node:child_process';
16
+ import { readFileSync, existsSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { pathToFileURL } from 'node:url';
19
+
20
+ import { matchesAny } from './lib/glob.mjs';
21
+ import { loadRulesConfig } from './lib/config.mjs';
22
+
23
+ /**
24
+ * @typedef {object} Violation
25
+ * @property {string} file
26
+ * @property {number} line
27
+ * @property {'T1'} rule
28
+ * @property {string} found
29
+ */
30
+
31
+ /** @typedef {'php' | 'ts' | 'py'} Language */
32
+
33
+ const SUPPRESSION = /typed-boundary-ok:\s*\S/;
34
+ const HINT = 'use a named Data class or typed collection (see .ai-agents/canon/typed-data.md)';
35
+
36
+ /**
37
+ * @param {string} path
38
+ * @returns {Language | null}
39
+ */
40
+ export function languageOf(path) {
41
+ if (path.endsWith('.php')) return 'php';
42
+ if (/\.(ts|tsx|mts|cts)$/.test(path) && !path.endsWith('.d.ts')) return 'ts';
43
+ if (path.endsWith('.py')) return 'py';
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * @param {string} line
49
+ * @returns {boolean}
50
+ */
51
+ function isCommentLine(line) {
52
+ return /^\s*(\/\/|\*|\/\*|#)/.test(line);
53
+ }
54
+
55
+ // ---------------------------------------------------------------- PHP
56
+
57
+ /**
58
+ * Line ranges (1-based, inclusive) of named function/method signatures.
59
+ * @param {ReadonlyArray<string>} lines
60
+ * @returns {ReadonlyArray<{ start: number, end: number }>}
61
+ */
62
+ function phpSignatures(lines) {
63
+ /** @type {Array<{ start: number, end: number }>} */
64
+ const ranges = [];
65
+ for (let i = 0; i < lines.length; i += 1) {
66
+ const text = lines[i] ?? '';
67
+ const head = /\bfunction\s+&?[A-Za-z_]\w*\s*\(/.exec(text);
68
+ if (!head || isCommentLine(text)) continue;
69
+ let depth = 0;
70
+ let closed = false;
71
+ let end = i;
72
+ scan: for (let j = i; j < lines.length; j += 1) {
73
+ const segment = j === i ? text.slice(head.index) : (lines[j] ?? '');
74
+ for (const char of segment) {
75
+ if (char === '(') depth += 1;
76
+ else if (char === ')') {
77
+ depth -= 1;
78
+ if (depth === 0) closed = true;
79
+ } else if (closed && (char === '{' || char === ';')) {
80
+ end = j;
81
+ break scan;
82
+ }
83
+ }
84
+ end = j;
85
+ }
86
+ ranges.push({ start: i + 1, end: end + 1 });
87
+ i = end;
88
+ }
89
+ return ranges;
90
+ }
91
+
92
+ /**
93
+ * Nearest docblock text above a 1-based line, skipping blank and attribute lines.
94
+ * @param {ReadonlyArray<string>} lines
95
+ * @param {number} lineNumber
96
+ * @returns {string}
97
+ */
98
+ function docblockAbove(lines, lineNumber) {
99
+ let index = lineNumber - 2;
100
+ while (index >= 0 && /^\s*(#\[.*\])?\s*$/.test(lines[index] ?? '')) index -= 1;
101
+ if (index < 0 || !/\*\/\s*$/.test(lines[index] ?? '')) return '';
102
+ /** @type {Array<string>} */
103
+ const block = [];
104
+ while (index >= 0) {
105
+ const current = lines[index] ?? '';
106
+ block.unshift(current);
107
+ if (current.includes('/**')) break;
108
+ index -= 1;
109
+ }
110
+ return block.join('\n');
111
+ }
112
+
113
+ /**
114
+ * @param {string} typeText
115
+ * @returns {string | null}
116
+ */
117
+ function untypedPhpType(typeText) {
118
+ const parts = typeText.replace(/^\?/, '').split(/[|&]/).map((part) => part.replace(/^\?/, '').toLowerCase());
119
+ return parts.find((part) => part === 'array' || part === 'mixed' || part === 'iterable') ?? null;
120
+ }
121
+
122
+ /**
123
+ * @param {string} file
124
+ * @param {ReadonlyArray<string>} lines
125
+ * @param {ReadonlySet<number>} added
126
+ * @returns {Array<Violation>}
127
+ */
128
+ function phpViolations(file, lines, added) {
129
+ /** @type {Array<Violation>} */
130
+ const found = [];
131
+ for (const signature of phpSignatures(lines)) {
132
+ const docblock = docblockAbove(lines, signature.start);
133
+ for (let n = signature.start; n <= signature.end; n += 1) {
134
+ const text = lines[n - 1] ?? '';
135
+ if (!added.has(n) || SUPPRESSION.test(text) || isCommentLine(text)) continue;
136
+ const promoted = /\b(public|protected|private)\b/.test(text) && !/\bfunction\b/.test(text);
137
+
138
+ for (const match of text.matchAll(/([\w\\|?&]+)\s+(?:&\s*)?(?:\.\.\.)?\$(\w+)/g)) {
139
+ const typeText = match[1] ?? '';
140
+ const name = match[2] ?? '';
141
+ const bad = untypedPhpType(typeText);
142
+ if (!bad) continue;
143
+ const genericParam = new RegExp(`@param\\s+\\S*<[^\\n]*\\$${name}\\b`).test(docblock);
144
+ if (promoted && bad === 'array' && genericParam) continue;
145
+ found.push({ file, line: n, rule: 'T1', found: `${typeText} $${name}` });
146
+ break;
147
+ }
148
+ if (found.at(-1)?.line === n) continue;
149
+
150
+ const returns = /\)\s*:\s*([\w\\|?]+)/.exec(text);
151
+ const badReturn = returns ? untypedPhpType(returns[1] ?? '') : null;
152
+ if (returns && badReturn) {
153
+ found.push({ file, line: n, rule: 'T1', found: `returns ${returns[1]}` });
154
+ }
155
+ }
156
+ }
157
+ return found;
158
+ }
159
+
160
+ // ---------------------------------------------------------------- TypeScript
161
+
162
+ /** @type {ReadonlyArray<RegExp>} */
163
+ const TS_PATTERNS = [
164
+ /Record<\s*string\s*,\s*(unknown|any)\s*>/,
165
+ /Map<\s*string\s*,\s*(unknown|any)\s*>/,
166
+ /:\s*\{\s*\[\s*\w+\s*:\s*string\s*\]\s*:\s*(unknown|any)\b/,
167
+ /:\s*any\b/,
168
+ /\bany\[\]/,
169
+ /<\s*any\s*>/,
170
+ /\bas\s+any\b/,
171
+ /:\s*object\b/,
172
+ ];
173
+
174
+ /**
175
+ * @param {string} file
176
+ * @param {ReadonlyArray<string>} lines
177
+ * @param {ReadonlySet<number>} added
178
+ * @returns {Array<Violation>}
179
+ */
180
+ function tsViolations(file, lines, added) {
181
+ /** @type {Array<Violation>} */
182
+ const found = [];
183
+ lines.forEach((raw, index) => {
184
+ const n = index + 1;
185
+ if (!added.has(n) || isCommentLine(raw) || SUPPRESSION.test(raw)) return;
186
+ const text = raw.replace(/\s\/\/.*$/, '');
187
+ for (const pattern of TS_PATTERNS) {
188
+ const match = pattern.exec(text);
189
+ if (match) {
190
+ found.push({ file, line: n, rule: 'T1', found: match[0].trim() });
191
+ return;
192
+ }
193
+ }
194
+ });
195
+ return found;
196
+ }
197
+
198
+ // ---------------------------------------------------------------- Python
199
+
200
+ /** @type {ReadonlyArray<RegExp>} */
201
+ const PY_PATTERNS = [
202
+ /\b(dict|Dict|Mapping|MutableMapping)\[\s*str\s*,\s*(Any|object)\s*\]/,
203
+ /->\s*(dict|list|Any|object)\s*:/,
204
+ /:\s*(dict|list)\s*[,)=]/,
205
+ /:\s*Any\b/,
206
+ ];
207
+
208
+ /**
209
+ * @param {string} file
210
+ * @param {ReadonlyArray<string>} lines
211
+ * @param {ReadonlySet<number>} added
212
+ * @returns {Array<Violation>}
213
+ */
214
+ function pyViolations(file, lines, added) {
215
+ /** @type {Array<Violation>} */
216
+ const found = [];
217
+ lines.forEach((raw, index) => {
218
+ const n = index + 1;
219
+ if (!added.has(n) || /^\s*#/.test(raw) || SUPPRESSION.test(raw)) return;
220
+ const text = raw.replace(/\s#.*$/, '');
221
+ for (const pattern of PY_PATTERNS) {
222
+ const match = pattern.exec(text);
223
+ if (match) {
224
+ found.push({ file, line: n, rule: 'T1', found: match[0].trim() });
225
+ return;
226
+ }
227
+ }
228
+ });
229
+ return found;
230
+ }
231
+
232
+ // ---------------------------------------------------------------- API
233
+
234
+ /**
235
+ * @param {string} file repository-relative path
236
+ * @param {string} content full file content
237
+ * @param {ReadonlySet<number>} added 1-based line numbers added in the diff
238
+ * @returns {Array<Violation>}
239
+ */
240
+ export function findViolations(file, content, added) {
241
+ const language = languageOf(file);
242
+ if (language === null) return [];
243
+ const lines = content.split('\n');
244
+ switch (language) {
245
+ case 'php':
246
+ return phpViolations(file, lines, added);
247
+ case 'ts':
248
+ return tsViolations(file, lines, added);
249
+ case 'py':
250
+ return pyViolations(file, lines, added);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Added line numbers per file, from `git diff -U0` output.
256
+ * @param {string} diff
257
+ * @returns {Map<string, Set<number>>}
258
+ */
259
+ export function parseAddedLines(diff) {
260
+ /** @type {Map<string, Set<number>>} */
261
+ const result = new Map();
262
+ /** @type {string | null} */
263
+ let file = null;
264
+ let next = 0;
265
+ for (const line of diff.split('\n')) {
266
+ if (line.startsWith('+++ ')) {
267
+ const target = line.slice(4).trim();
268
+ file = target === '/dev/null' ? null : target.replace(/^b\//, '');
269
+ continue;
270
+ }
271
+ if (line.startsWith('--- ') || line.startsWith('diff --git')) continue;
272
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
273
+ if (hunk) {
274
+ next = Number(hunk[1]);
275
+ continue;
276
+ }
277
+ if (file === null) continue;
278
+ if (line.startsWith('+')) {
279
+ const lines = result.get(file) ?? new Set();
280
+ lines.add(next);
281
+ result.set(file, lines);
282
+ next += 1;
283
+ } else if (line.startsWith(' ')) {
284
+ next += 1;
285
+ }
286
+ }
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * @param {ReadonlyArray<string>} argv
292
+ * @returns {number} exit code
293
+ */
294
+ export function main(argv) {
295
+ const root = process.cwd();
296
+ const config = loadRulesConfig(root).typedBoundaries;
297
+ if (config.paths.length === 0) {
298
+ console.log('T1 typed-boundaries: not configured (typedBoundaries.paths is empty) — skipping.');
299
+ return 0;
300
+ }
301
+ const baseIndex = argv.indexOf('--base');
302
+ const range = argv.includes('--staged')
303
+ ? ['--cached']
304
+ : [`${baseIndex >= 0 ? argv[baseIndex + 1] ?? 'origin/main' : 'origin/main'}...HEAD`];
305
+ const diff = execFileSync('git', ['diff', '-U0', '--no-color', '--diff-filter=AMR', ...range], {
306
+ encoding: 'utf8',
307
+ maxBuffer: 64 * 1024 * 1024,
308
+ });
309
+
310
+ /** @type {Array<Violation>} */
311
+ const violations = [];
312
+ for (const [file, added] of parseAddedLines(diff)) {
313
+ if (!matchesAny(file, config.paths) || matchesAny(file, config.allow)) continue;
314
+ const full = join(root, file);
315
+ if (!existsSync(full)) continue;
316
+ violations.push(...findViolations(file, readFileSync(full, 'utf8'), added));
317
+ }
318
+
319
+ for (const v of violations) {
320
+ console.log(`${v.rule} ${v.file}:${v.line} — ${v.found} — ${HINT}`);
321
+ }
322
+ console.log(`T1 typed-boundaries: ${violations.length} violation(s).`);
323
+ return violations.length === 0 ? 0 : 1;
324
+ }
325
+
326
+ const invoked = process.argv[1];
327
+ if (invoked !== undefined && import.meta.url === pathToFileURL(invoked).href) {
328
+ process.exit(main(process.argv.slice(2)));
329
+ }
@@ -0,0 +1,63 @@
1
+ // @ts-check
2
+ import { readFileSync, existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ /**
6
+ * @typedef {object} TypedBoundariesConfig
7
+ * @property {ReadonlyArray<string>} paths Globs the T1 check applies to
8
+ * @property {ReadonlyArray<string>} allow Globs exempt from the check
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} TestWithChangeConfig
13
+ * @property {ReadonlyArray<string>} sourcePaths
14
+ * @property {ReadonlyArray<string>} testPaths
15
+ * @property {string} exemptionTag
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} RulesConfig
20
+ * @property {TypedBoundariesConfig} typedBoundaries
21
+ * @property {TestWithChangeConfig} testWithChange
22
+ */
23
+
24
+ /**
25
+ * @param {unknown} value
26
+ * @param {string} field
27
+ * @returns {ReadonlyArray<string>}
28
+ */
29
+ function stringList(value, field) {
30
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
31
+ throw new Error(`rules.config.json: ${field} must be a list of strings`);
32
+ }
33
+ return /** @type {ReadonlyArray<string>} */ (value);
34
+ }
35
+
36
+ /**
37
+ * Parse edge for rules.config.json (typed-data T5): read, validate, return a named shape.
38
+ * @param {string} repoRoot
39
+ * @returns {RulesConfig}
40
+ */
41
+ export function loadRulesConfig(repoRoot) {
42
+ const path = join(repoRoot, '.ai-agents', 'rules.config.json');
43
+ if (!existsSync(path)) {
44
+ throw new Error(`Missing ${path}. Run the installer, then /bootstrap to configure it.`);
45
+ }
46
+ /** @type {{ typedBoundaries?: { paths?: unknown, allow?: unknown }, testWithChange?: { sourcePaths?: unknown, testPaths?: unknown, exemptionTag?: unknown } }} */
47
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
48
+ const tag = raw.testWithChange?.exemptionTag ?? 'no-test';
49
+ if (typeof tag !== 'string') {
50
+ throw new Error('rules.config.json: testWithChange.exemptionTag must be a string');
51
+ }
52
+ return {
53
+ typedBoundaries: {
54
+ paths: stringList(raw.typedBoundaries?.paths ?? [], 'typedBoundaries.paths'),
55
+ allow: stringList(raw.typedBoundaries?.allow ?? [], 'typedBoundaries.allow'),
56
+ },
57
+ testWithChange: {
58
+ sourcePaths: stringList(raw.testWithChange?.sourcePaths ?? [], 'testWithChange.sourcePaths'),
59
+ testPaths: stringList(raw.testWithChange?.testPaths ?? [], 'testWithChange.testPaths'),
60
+ exemptionTag: tag,
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,40 @@
1
+ // @ts-check
2
+ /**
3
+ * Minimal glob matching for rule configuration. Supports `**` (any depth,
4
+ * including zero directories), `*` (within one path segment), and `?`.
5
+ * Paths are always forward-slash, repository-relative.
6
+ */
7
+
8
+ /**
9
+ * @param {string} glob
10
+ * @returns {RegExp}
11
+ */
12
+ export function globToRegExp(glob) {
13
+ let pattern = '';
14
+ for (let i = 0; i < glob.length; i += 1) {
15
+ const char = glob.charAt(i);
16
+ if (char === '*') {
17
+ if (glob.charAt(i + 1) === '*') {
18
+ const followedBySlash = glob.charAt(i + 2) === '/';
19
+ pattern += followedBySlash ? '(?:.*/)?' : '.*';
20
+ i += followedBySlash ? 2 : 1;
21
+ } else {
22
+ pattern += '[^/]*';
23
+ }
24
+ } else if (char === '?') {
25
+ pattern += '[^/]';
26
+ } else {
27
+ pattern += char.replace(/[.+^${}()|[\]\\]/g, '\\$&');
28
+ }
29
+ }
30
+ return new RegExp(`^${pattern}$`);
31
+ }
32
+
33
+ /**
34
+ * @param {string} path
35
+ * @param {ReadonlyArray<string>} globs
36
+ * @returns {boolean}
37
+ */
38
+ export function matchesAny(path, globs) {
39
+ return globs.some((glob) => globToRegExp(glob).test(path));
40
+ }
@@ -0,0 +1,108 @@
1
+ # State
2
+
3
+ Files here are written by agents and read by other agents. They hold the
4
+ **current** feature only. Before the branch merges, agent 09a archives them to
5
+ `.ai-agents/archive/{slug}/` and resets this directory, so the reset travels
6
+ with the feature instead of waiting for someone to do it afterwards.
7
+
8
+ This directory is why the pipeline survives losing its conversation. The state is
9
+ on disk, not in anyone's context window — you can close the session mid-feature,
10
+ come back next week, run `/resume`, and pick up exactly where you stopped.
11
+
12
+ ## Files
13
+
14
+ | File | Written by | Read by |
15
+ |------|-----------|---------|
16
+ | `current-feature.md` | 01 intake | 01b, 02a, 02b, 02c, 02d, 03a, 03b, 06, 07, 08, 09a |
17
+ | `impact.md` | 01b impact (predict, then verify) | 02a, 02b, 02c, 03a, 04a, 04b, 06, 07, 08, 09a |
18
+ | `tdd-log.md` | 00a, 00b, 04a, 04b | Gate 2 reviewer, 06, 09a |
19
+ | `current-plan.md` | 02a (deps), 03a (backend tasks), 03b (UI tasks) | 04a, 04b |
20
+ | `contract-backend.md` | 02b | 02d, 03a, 04a, 06, 07, 08, 09a |
21
+ | `contract-ui.md` | 02c | 02d, 03b, 04b, 06, 08, 09a |
22
+ | `contract-seam.md` | 02d | 03a, 03b, **04a, 04b**, 06, 07 |
23
+ | `current-stage.md` | every agent | every agent |
24
+ | `decisions-log.md` | 04a, 04b | 09a |
25
+ | `scaffold-decisions.md` | 00a scaffold | 00 bootstrap |
26
+
27
+ Outside `state/`:
28
+
29
+ | Path | Written by | Read by |
30
+ |------|-----------|---------|
31
+ | `contracts/{slug}/` | 02d (`export` mode), `/seam import` | 02d, 03b, 04b, CI job 10 — survives release |
32
+ | `.ai-agents/changes/{slug}.md` | 09a `/finalize` | 09b `/release` — the feature's own release note |
33
+ | `.ai-agents/observations/` | task-observer | 09 changelog (retrospective) |
34
+ | `.ai-agents/pipeline.config.json` | installer | every agent (topology, runtimes, tools) |
35
+
36
+ `scaffold-decisions.md` only exists on repositories that started empty. It
37
+ records which stack decisions were actively chosen and which were merely
38
+ defaulted, so bootstrap can carry the defaulted ones into the context marked as
39
+ weakly held rather than enshrining an unexamined default as convention.
40
+
41
+ `contract-seam.md` is the frozen seam. Both implementers build against it, and it
42
+ outranks either contract where they differ. When it needs to change, it changes
43
+ here and both tracks are told — never on one side only.
44
+
45
+ When idle, only `current-stage.md` exists, reading `Stage: idle`.
46
+
47
+ ## Stages
48
+
49
+ | Stage | Meaning | Next |
50
+ |-------|---------|------|
51
+ | *(empty repo)* | Nothing to document yet | `/scaffold` |
52
+ | `scaffolded` | Vertical slice built, Gate A open | Review the slice, then `/bootstrap` |
53
+ | *(no architecture context)* | Repo has code but is not set up | `/bootstrap` |
54
+ | `bootstrapped` | Context written, Gate B open | Review it, then `/intake` |
55
+ | `idle` | Nothing in progress | `/intake` |
56
+ | `intake-complete` | Gate 0 open | `approve` / `edit` / `split` |
57
+ | `gate-0-approved` | Ready to measure impact | `/impact` |
58
+ | `impact-assessed` | Risk class set | `/contract` |
59
+ | `contracts-complete` | 02a, 02b, 02c, 02d all done — Gate 1 open | Review seam first, then `approve` |
60
+ | `gate-1-approved` | Ready to sequence | `/sequence` |
61
+ | `sequence-complete` | Both tracks sequenced | `/implement` |
62
+ | `backend-task-{n}-committed` | Gate 2 open | Review diff, push |
63
+ | `ui-task-{n}-committed` | Gate 2 open | Review diff, push |
64
+ | `{track}-task-{n}-ci-failed` | CI failed | Fix, or `/implement --retry-task {track} {n}` |
65
+ | `{track}-task-{n}-merged` | Task merged | `/implement` for the next available |
66
+ | *(all tasks committed)* | Implementation done | `/impact --verify` |
67
+ | `impact-verified` | Actual impact measured | `/qa` and `/security` (and `/perf`, mandatory at L2+) |
68
+ | `review-complete` | Gate 3 open | `approve-merge` / `block` |
69
+ | `gate-3-approved` | Reviews passed | `/finalize` — note, archive, state reset |
70
+ | `finalized` | Branch is finished | Push and merge; then `/intake`. Nothing runs against merged code |
71
+ | `idle` | Reset by `/finalize` | `/intake` |
72
+
73
+ Individual agent completions are appended as lines rather than replacing the
74
+ stage, so parallel agents do not overwrite each other:
75
+
76
+ ```markdown
77
+ **02b backend-contract**: complete at 2026-08-18T09:14:22Z
78
+ **02c ui-contract**: complete at 2026-08-18T09:19:08Z
79
+ ```
80
+
81
+ The overall stage advances only when every agent in that step has finished.
82
+
83
+ ## Flags
84
+
85
+ Appended by agents and surfaced by `/status` and `/resume`:
86
+
87
+ ```markdown
88
+ **Critical security findings**: 2 — MERGE BLOCKED
89
+ **Critical performance findings**: 1 — degrades at 500k rows
90
+ **Escalated decisions**: 1 — unresolved in contract-seam.md
91
+ **Risk class**: L2
92
+ **Required**: /perf mandatory; characterisation tests first
93
+ **Escapes**: 2 — must be covered or accepted before merge
94
+ ```
95
+
96
+ ## Editing by hand
97
+
98
+ You can edit these files between agent runs — to correct a wrong assumption in a
99
+ contract, or to reorder tasks. The next agent picks up your edits.
100
+
101
+ Two things to know:
102
+
103
+ - Editing `contract-backend.md` or `contract-ui.md` after the seam is frozen
104
+ makes them disagree with `contract-seam.md`. If the change touches the API,
105
+ re-run `/contract` so 02d re-freezes it.
106
+ - Do not edit application source to match a state file edit. Re-run the
107
+ appropriate agent instead — the point of the pipeline is that the code follows
108
+ from the documents.
@@ -0,0 +1,10 @@
1
+ # Current Stage
2
+
3
+ **Stage**: idle
4
+ **Next action**: Depends on what is in this repository.
5
+
6
+ - **Empty, or only framework default output** → `/scaffold`. There is nothing for
7
+ `/bootstrap` to read yet, and a context written from intentions rather than
8
+ code produces exemplars nobody should imitate.
9
+ - **Has real code, no `architecture-context.md`** → `/bootstrap`.
10
+ - **Context exists and is filled in** → `/intake` to start a feature.
@@ -0,0 +1,22 @@
1
+ # Templates
2
+
3
+ Starter files agent 00 (`/bootstrap`) adapts to the project, and reference
4
+ material the implementers read for pattern rather than copy verbatim.
5
+
6
+ Bootstrap writes the CI configuration directly to the path recorded in
7
+ architecture context §3, wiring up whichever of the jobs in
8
+ `agents/05-ci-validator.md` apply to the project's tooling.
9
+
10
+ Add templates here as the team identifies patterns worth standardising — a
11
+ module skeleton, a screen skeleton, a test harness. Anything here should be
12
+ something a human would otherwise copy-paste and then edit.
13
+
14
+ ## CI templates
15
+
16
+ - `ci/gitlab-ci.rules.yml` — include from `.gitlab-ci.yml`
17
+ - `ci/github-actions.rules.yml` — copy into `.github/workflows/`
18
+
19
+ Both run the T1 typed-boundaries and TDD-1 test-with-change checks on merge
20
+ requests, plus the impeccable design detector as a non-blocking job. `/bootstrap`
21
+ wires the one that matches §3 and adds the repo's own lint, type-check, test,
22
+ and build jobs.
@@ -0,0 +1,35 @@
1
+ # Pipeline rule jobs for GitHub Actions — copy to .github/workflows/pipeline-rules.yml.
2
+ # /bootstrap adjusts it to the repository (see agents/05-ci-validator.md).
3
+ name: pipeline-rules
4
+ on:
5
+ pull_request:
6
+
7
+ jobs:
8
+ rules:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ fetch-depth: 0
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 22
17
+ - name: T1 typed boundaries
18
+ run: node .ai-agents/scripts/check-typed-boundaries.mjs --base "origin/${{ github.base_ref }}"
19
+ - name: TDD-1 test with change
20
+ run: node .ai-agents/scripts/check-test-with-change.mjs --base "origin/${{ github.base_ref }}"
21
+
22
+ design:
23
+ runs-on: ubuntu-latest
24
+ continue-on-error: true # non-blocking until promoted (05-ci-validator §9)
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ with:
28
+ fetch-depth: 0
29
+ - uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 22
32
+ - name: impeccable detect on changed UI files
33
+ run: |
34
+ CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}"...HEAD -- '*.vue' '*.tsx' '*.jsx' '*.svelte' '*.blade.php' '*.css' | tr '\n' ' ')
35
+ if [ -n "$CHANGED" ]; then npx --yes impeccable detect $CHANGED; else echo "No UI files changed."; fi
@@ -0,0 +1,30 @@
1
+ # Pipeline rule jobs for GitLab CI — include from .gitlab-ci.yml:
2
+ # include:
3
+ # - local: .ai-agents/templates/ci/gitlab-ci.rules.yml
4
+ # /bootstrap adjusts stages, images, and which jobs apply (see agents/05-ci-validator.md).
5
+
6
+ .pipeline-rules-base:
7
+ stage: test
8
+ image: node:22-alpine
9
+ before_script:
10
+ - apk add --no-cache git
11
+ - git fetch --no-tags origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
12
+ rules:
13
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
14
+
15
+ rules:typed-boundaries:
16
+ extends: .pipeline-rules-base
17
+ script:
18
+ - node .ai-agents/scripts/check-typed-boundaries.mjs --base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
19
+
20
+ rules:test-with-change:
21
+ extends: .pipeline-rules-base
22
+ script:
23
+ - node .ai-agents/scripts/check-test-with-change.mjs --base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
24
+
25
+ design:impeccable-detect:
26
+ extends: .pipeline-rules-base
27
+ allow_failure: true # non-blocking until promoted (05-ci-validator §9)
28
+ script:
29
+ - CHANGED=$(git diff --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"...HEAD -- '*.vue' '*.tsx' '*.jsx' '*.svelte' '*.blade.php' '*.css' | tr '\n' ' ')
30
+ - if [ -n "$CHANGED" ]; then npx --yes impeccable detect $CHANGED; else echo "No UI files changed."; fi