@really-knows-ai/foundry 3.0.1 → 3.0.2

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.
@@ -38,73 +38,47 @@ function findLawEnd(lines, startIdx) {
38
38
  return lines.length;
39
39
  }
40
40
 
41
- // Extract full markdown for a single law from file content
42
41
  function extractLawMarkdown(content, lawId) {
43
42
  const lines = content.split('\n');
44
43
  const startIdx = findLawStart(lines, lawId);
45
-
46
44
  if (startIdx < 0) return null;
47
-
48
45
  const endIdx = findLawEnd(lines, startIdx);
49
46
  const lawLines = lines.slice(startIdx, endIdx);
50
-
51
- while (lawLines.length > 0 && lawLines[lawLines.length - 1] === '') {
52
- lawLines.pop();
53
- }
54
-
47
+ while (lawLines.length > 0 && lawLines[lawLines.length - 1] === '') lawLines.pop();
55
48
  return lawLines.join('\n') + '\n';
56
49
  }
57
50
 
58
51
  async function searchGlobalLaws(io, foundryDir, lawId) {
59
52
  const globalLawsDir = join(foundryDir, 'laws');
60
- if (!(await io.exists(globalLawsDir))) {
61
- return null;
62
- }
63
-
53
+ if (!(await io.exists(globalLawsDir))) return null;
64
54
  const files = await io.readDir(globalLawsDir);
65
55
  for (const file of files) {
66
56
  if (!file.endsWith('.md')) continue;
67
57
  const path = join(globalLawsDir, file);
68
58
  const content = await io.readFile(path);
69
- if (contentContainsLaw(content, lawId)) {
70
- return { path, fullMarkdown: content, source: 'global' };
71
- }
59
+ if (contentContainsLaw(content, lawId)) return { path, fullMarkdown: content, source: 'global' };
72
60
  }
73
-
74
61
  return null;
75
62
  }
76
63
 
77
64
  async function searchTypeSpecificLaws(io, foundryDir, lawId) {
78
65
  const artefactsDir = join(foundryDir, 'artefacts');
79
- if (!(await io.exists(artefactsDir))) {
80
- return null;
81
- }
82
-
66
+ if (!(await io.exists(artefactsDir))) return null;
83
67
  const types = await io.readDir(artefactsDir);
84
68
  for (const typeId of types) {
85
69
  const typeLawsPath = join(artefactsDir, typeId, 'laws.md');
86
70
  if (!(await io.exists(typeLawsPath))) continue;
87
-
88
71
  const content = await io.readFile(typeLawsPath);
89
- if (contentContainsLaw(content, lawId)) {
90
- return { path: typeLawsPath, fullMarkdown: content, source: `type:${typeId}` };
91
- }
72
+ if (contentContainsLaw(content, lawId)) return { path: typeLawsPath, fullMarkdown: content, source: `type:${typeId}` };
92
73
  }
93
-
94
74
  return null;
95
75
  }
96
76
 
97
77
  async function findLawByID(io, foundryDir, lawId) {
98
- let result = await searchGlobalLaws(io, foundryDir, lawId);
99
- if (result) {
100
- return { found: true, ...result };
101
- }
102
-
103
- result = await searchTypeSpecificLaws(io, foundryDir, lawId);
104
- if (result) {
105
- return { found: true, ...result };
106
- }
107
-
78
+ const global = await searchGlobalLaws(io, foundryDir, lawId);
79
+ if (global) return { found: true, ...global };
80
+ const typeSpec = await searchTypeSpecificLaws(io, foundryDir, lawId);
81
+ if (typeSpec) return { found: true, ...typeSpec };
108
82
  return { found: false };
109
83
  }
110
84
 
@@ -204,82 +178,92 @@ function computeTargetPath(target) {
204
178
 
205
179
  // --- add law executor --------------------------------------------------------
206
180
 
181
+ function extractLawId(body) {
182
+ const match = body.match(/^## ([^\s]+)/m);
183
+ return match ? match[1] : null;
184
+ }
185
+
186
+ async function checkExistingLaw(io, path, lawId) {
187
+ if (!(await io.exists(path))) return { existedBefore: false, priorContent: null };
188
+ const priorContent = await io.readFile(path);
189
+ if (contentContainsLaw(priorContent, lawId)) {
190
+ return { error: `law id "${lawId}" already exists in ${path}; use foundry_config_edit_law to update it` };
191
+ }
192
+ return { existedBefore: true, priorContent };
193
+ }
194
+
207
195
  async function validateAddLawPrerequisites(io, args) {
208
196
  const targetError = validateAddLawTarget(args.target);
209
- if (targetError) {
210
- return { error: targetError };
211
- }
197
+ if (targetError) return { error: targetError };
212
198
 
213
199
  const path = computeTargetPath(args.target);
214
200
  const validation = await validateLaw({ body: args.body, io });
215
- if (!validation.ok) {
216
- return validation;
217
- }
201
+ if (!validation.ok) return validation;
218
202
 
219
- if (await io.exists(path)) {
220
- return {
221
- ok: false,
222
- errors: [`${path} already exists; use foundry_config_edit_law to update an existing law in place`],
223
- };
224
- }
203
+ const lawId = extractLawId(args.body);
204
+ if (!lawId) return { error: 'could not determine law id from body (expected "## <law-id>" heading)' };
205
+
206
+ const existing = await checkExistingLaw(io, path, lawId);
207
+ if (existing.error) return { error: existing.error };
208
+ return { ok: true, path, lawId, ...existing };
209
+ }
210
+
211
+ function formatAddLawError(err) {
212
+ return err instanceof UnexpectedFilesError
213
+ ? JSON.stringify({ error: err.message, affected_files: err.files })
214
+ : errorJson(err);
215
+ }
225
216
 
226
- return { ok: true, path };
217
+ function buildNextContent(existedBefore, priorContent, body) {
218
+ return existedBefore ? priorContent.trimEnd() + '\n\n' + body.trimStart() : body;
219
+ }
220
+
221
+ async function rollbackAddLaw(io, path, existedBefore, priorContent) {
222
+ if (existedBefore) await io.writeFile(path, priorContent);
223
+ else await io.rm(path);
227
224
  }
228
225
 
229
226
  async function executeAddLaw(args, context) {
230
227
  const io = makeAsyncIO(context.worktree);
231
228
  const execFile = makeExecFile(context.worktree);
229
+ let path, existedBefore, priorContent;
232
230
 
233
231
  try {
234
232
  const prereq = await validateAddLawPrerequisites(io, args);
235
- if (prereq.error) {
236
- return JSON.stringify({ ok: false, errors: [prereq.error] });
237
- }
238
- if (!prereq.ok) {
239
- return JSON.stringify(prereq);
240
- }
233
+ if (prereq.error) return JSON.stringify({ ok: false, errors: [prereq.error] });
234
+ if (!prereq.ok) return JSON.stringify(prereq);
241
235
 
242
- const path = prereq.path;
236
+ ({ path, existedBefore, priorContent } = prereq);
237
+ const nextContent = buildNextContent(existedBefore, priorContent, args.body);
243
238
 
244
239
  await io.mkdirp(dirname(path));
245
- await io.writeFile(path, args.body);
240
+ await io.writeFile(path, nextContent);
246
241
 
247
242
  const sha = commitWithPolicy({
248
243
  message: `config: add law ${args.name}\n\nvia foundry_config_add_law`,
249
244
  allowedPatterns: ['foundry/**'],
250
245
  execFile,
251
246
  });
252
-
253
247
  return JSON.stringify({ ok: true, path, sha });
254
248
  } catch (err) {
255
- if (err instanceof UnexpectedFilesError) {
256
- return JSON.stringify({ error: err.message, affected_files: err.files });
257
- }
258
- return errorJson(err);
249
+ if (path) await rollbackAddLaw(io, path, existedBefore, priorContent);
250
+ return formatAddLawError(err);
259
251
  }
260
252
  }
261
253
 
262
254
  // --- helper for preserving sibling laws -------------------------------------------------------
263
255
 
264
- // Replace a law in file content while preserving other laws
265
256
  function replaceLawInContent(content, lawId, newLawMarkdown) {
266
257
  const lines = content.split('\n');
267
258
  const startIdx = findLawStart(lines, lawId);
268
259
  if (startIdx < 0) return content.trimEnd() + '\n\n' + newLawMarkdown;
269
-
270
260
  const endIdx = findLawEnd(lines, startIdx);
271
261
  const before = lines.slice(0, startIdx);
272
262
  const after = lines.slice(endIdx);
273
-
274
- // Trim trailing empty lines from before
275
263
  const beforeEnd = before.findLastIndex(l => l !== '') + 1;
276
264
  before.length = beforeEnd;
277
-
278
- // Trim leading empty lines from after
279
265
  const afterStart = after.findIndex(l => l !== '');
280
266
  if (afterStart > 0) after.splice(0, afterStart);
281
-
282
- // newLawMarkdown includes trailing newline; split and rejoin without final empty string
283
267
  const newLines = newLawMarkdown.trimEnd().split('\n');
284
268
  return before.concat(newLines, after).join('\n') + '\n';
285
269
  }
@@ -52,7 +52,7 @@ async function executeValidator(expanded, worktree, patterns) {
52
52
  * JSON or missing required fields, `pattern-mismatch` for files that
53
53
  * didn't match the artefact type's `file-patterns`.
54
54
  */
55
- async function runValidators(laws, patterns, patternSubstitution, worktree) {
55
+ async function runValidators(laws, patterns, substitutions, worktree) {
56
56
  const results = {
57
57
  validatorsRun: 0,
58
58
  items: [],
@@ -61,7 +61,7 @@ async function runValidators(laws, patterns, patternSubstitution, worktree) {
61
61
 
62
62
  for (const law of laws) {
63
63
  if (!law.validators || law.validators.length === 0) continue;
64
- await runLawValidators(law, patterns, patternSubstitution, worktree, results);
64
+ await runLawValidators(law, patterns, substitutions, worktree, results);
65
65
  }
66
66
 
67
67
  return results;
@@ -70,15 +70,15 @@ async function runValidators(laws, patterns, patternSubstitution, worktree) {
70
70
  /**
71
71
  * Run validators for a single law.
72
72
  */
73
- async function runLawValidators(law, patterns, patternSubstitution, worktree, results) {
73
+ async function runLawValidators(law, patterns, substitutions, worktree, results) {
74
74
  for (const validator of law.validators) {
75
- // Skip validators if pattern substitution is empty (no matching files)
76
- // Self-resolving validators (npm test, tsc) omit {pattern}, so they still run
77
- if (patternSubstitution === '' && validator.command.includes('{pattern}')) {
75
+ // Skip iff command uses {files} and there are no matching files.
76
+ // {pattern}-only and verbatim commands always run.
77
+ if (substitutions.files === '' && /(?:^|\s)\{files\}(?=\s|$)/.test(validator.command)) {
78
78
  continue;
79
79
  }
80
80
  results.validatorsRun++;
81
- const expanded = expandValidatorCommand(validator.command, patternSubstitution);
81
+ const expanded = expandValidatorCommand(validator.command, substitutions);
82
82
  const parseResult = await executeValidator(expanded, worktree, patterns);
83
83
  collectValidatorResult(parseResult, law.id, validator.id, results);
84
84
  }
@@ -160,8 +160,11 @@ async function performValidation(args, context) {
160
160
  */
161
161
  async function runValidatorsAndReport(laws, patterns, worktree) {
162
162
  const expandedFiles = await expandPatterns(patterns, worktree);
163
- const patternSubstitution = expandedFiles.map(shellQuote).join(' ');
164
- const results = await runValidators(laws, patterns, patternSubstitution, worktree);
163
+ const substitutions = {
164
+ pattern: patterns.map(shellQuote).join(' '),
165
+ files: expandedFiles.map(shellQuote).join(' '),
166
+ };
167
+ const results = await runValidators(laws, patterns, substitutions, worktree);
165
168
 
166
169
  return JSON.stringify({
167
170
  ok: results.errors.length === 0,
@@ -235,29 +238,34 @@ async function expandPatterns(patterns, worktree) {
235
238
  }
236
239
 
237
240
  /**
238
- * Expand validator command by replacing {pattern} placeholder.
241
+ * Expand validator command by replacing {pattern} and {files} placeholders.
242
+ *
243
+ * - {pattern} → space-separated, shell-quoted globs from the artefact
244
+ * type's `file-patterns:` array (e.g. "'haikus/*.md' 'drafts/*.md'").
245
+ * - {files} → space-separated, shell-quoted matching file paths in the
246
+ * worktree (e.g. "'haikus/one.md' 'haikus/two.md'").
239
247
  *
240
- * Only replaces {pattern} when it appears as a standalone token bounded by
241
- * whitespace or string start/end. This allows self-resolving validators
242
- * (e.g., npm test, tsc --noEmit) to omit the placeholder without risk of
243
- * accidental substitution if they contain the literal text "{pattern}" as part
244
- * of another string.
248
+ * Both placeholders are recognised only as standalone tokens, bounded
249
+ * by whitespace or start/end of string. Surrounding single or double
250
+ * quotes around the placeholder are stripped first so authors can
251
+ * write `rg "{pattern}"` for readability.
245
252
  *
246
- * @param {string} command - The validator command
247
- * @param {string} patternSubstitution - Shell-quoted file paths, space-separated
248
- * @returns {string} The expanded command
253
+ * @param {string} command
254
+ * @param {{ pattern: string, files: string }} substitutions
255
+ * @returns {string}
249
256
  */
250
- export function expandValidatorCommand(command, patternSubstitution) {
251
- // First strip surrounding quotes around {pattern} to handle cases like
252
- // rg "{pattern}" where authors add quotes for readability
253
- const cmd = command
257
+ export function expandValidatorCommand(command, { pattern, files }) {
258
+ let cmd = command
254
259
  .replace(/"\{pattern\}"/g, '{pattern}')
255
- .replace(/'\{pattern\}'/g, '{pattern}');
260
+ .replace(/'\{pattern\}'/g, '{pattern}')
261
+ .replace(/"\{files\}"/g, '{files}')
262
+ .replace(/'\{files\}'/g, '{files}');
256
263
 
257
- // Only substitute {pattern} when it appears as a standalone token
258
- // (bounded by whitespace or start/end of string)
259
- return cmd.replace(/(?:^|\s)\{pattern\}(?=\s|$)/g, (match) => {
260
- const leadingSpace = match.startsWith('{') ? '' : ' ';
261
- return leadingSpace + patternSubstitution;
262
- });
264
+ cmd = cmd.replace(/(?:^|\s)\{pattern\}(?=\s|$)/g, (match) =>
265
+ match.startsWith('{') ? pattern : ' ' + pattern);
266
+
267
+ cmd = cmd.replace(/(?:^|\s)\{files\}(?=\s|$)/g, (match) =>
268
+ match.startsWith('{') ? files : ' ' + files);
269
+
270
+ return cmd;
263
271
  }
package/dist/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.0.2] - 2026-05-11
4
+
5
+ A documentation and tool-correctness patch driven by a failing
6
+ haiku-flow setup session. Closes the loop on the laws-with-validators
7
+ migration: the validator contract is now fully documented in
8
+ `add-law`; the `add_law` tool appends additional laws to an existing
9
+ file and rolls back its file write when the commit fails;
10
+ `init-foundry` seeds a `node_modules/` ignore so npm-installed
11
+ validator dependencies never collide with the config-tier write
12
+ guard.
13
+
14
+ ### Validator contract is canonical in `add-law`
15
+
16
+ - `add-law` SKILL.md gains a **§7a. Validator contract** that covers
17
+ the JSONL output shape (`file`, `text` required; `location`,
18
+ `severity` optional), command placeholders, working directory, skip
19
+ rule, and a worked Node example. Authors no longer need to read
20
+ plugin source to write a validator.
21
+ - `add-artefact-type` step 5 drops its half-duplicate contract and
22
+ cross-references `add-law` §7a. Step 1 and step 4 now make clear
23
+ that the frontmatter `name:` field equals the artefact type's id
24
+ (lowercase, hyphenated); human-readable labels go in the
25
+ `## Definition` prose. Step 9 reflects the new append-aware
26
+ `add_law`.
27
+
28
+ ### Validator command placeholders split
29
+
30
+ - `{pattern}` now renders the artefact type's `file-patterns:` as
31
+ space-separated, shell-quoted globs (e.g.
32
+ `'haikus/*.md' 'drafts/*.md'`). Use it when a validator does its
33
+ own globbing or accepts globs directly (e.g. `rg --glob`).
34
+ - `{files}` renders the matching files in the worktree as
35
+ space-separated, shell-quoted paths. Use it when the validator
36
+ takes an explicit list of file paths.
37
+ - A validator is skipped iff its command contains `{files}` and there
38
+ are no matching files. `{pattern}`-only and verbatim commands
39
+ always run.
40
+ - **Migration:** any existing validator authored against the prior
41
+ semantics (where `{pattern}` substituted expanded paths) must be
42
+ updated to use `{files}` instead. Foundry was tagged 3.0.1 only
43
+ 12 hours before this release; no migration helper is provided.
44
+
45
+ ### `foundry_config_add_law` correctness
46
+
47
+ - The tool now appends a new law to an existing `laws.md` instead of
48
+ erroring on file-exists. It only errors when a law with the same
49
+ id is already present in the file — in that case the caller
50
+ switches to `foundry_config_edit_law`.
51
+ - File writes are atomic with the commit. If the commit fails (most
52
+ commonly `unexpected_files`), the tool restores `laws.md` to its
53
+ prior content (or deletes it if it didn't exist before the call).
54
+ This eliminates the orphaned-file state that previously broke the
55
+ next call with "already exists".
56
+
57
+ ### `init-foundry` seeds `node_modules/`
58
+
59
+ - `.gitignore` now starts with `.snapshots/`, `node_modules/`, and
60
+ `.DS_Store`. The new entry stops `npm install` from immediately
61
+ blocking every config-tier tool with `unexpected_files`.
62
+
63
+ ### Migration
64
+
65
+ - Update any validator commands that used `{pattern}` for file
66
+ expansion to use `{files}` instead.
67
+ - No action needed for projects already on 3.0.1 that have not yet
68
+ authored validators using `{pattern}`.
69
+ - Existing projects can add `node_modules/` to `.gitignore` by hand;
70
+ the `init-foundry` change only affects newly-initialised projects.
71
+
3
72
  ## [3.0.1] - 2026-05-11
4
73
 
5
74
  A documentation and cleanup patch. No runtime behaviour change. `quench`
@@ -42,10 +42,12 @@ Before running this skill, verify all three of the following:
42
42
  ### 1. Gather basics
43
43
 
44
44
  From the user's prompt, establish:
45
- - `id` — lowercase, hyphenated identifier
46
- - `name` human-readable name
47
- - `file-patterns` glob patterns for files this type produces (forge's write scope is exactly these patterns)
48
- - A prose description of what this artefact type is
45
+ - `id` — lowercase, hyphenated identifier (e.g. `haiku`). The
46
+ frontmatter `name:` field must equal this id; any human-readable
47
+ label goes in the `## Definition` prose, not in frontmatter.
48
+ - `file-patterns` glob patterns for files this type produces
49
+ (forge's write scope is exactly these patterns).
50
+ - A prose description of what this artefact type is.
49
51
 
50
52
  If any of these are missing, ask.
51
53
 
@@ -87,7 +89,7 @@ Present the definition to the user:
87
89
 
88
90
  ```markdown
89
91
  ---
90
- name: <name>
92
+ name: <id>
91
93
  file-patterns:
92
94
  - "<pattern>"
93
95
  ---
@@ -97,6 +99,10 @@ file-patterns:
97
99
  <description>
98
100
  ```
99
101
 
102
+ The `name:` value must exactly match the artefact type's `id`
103
+ (lowercase, hyphenated). If you want a human-readable label, put it
104
+ in the `## Definition` prose.
105
+
100
106
  Ask: does this capture the artefact type correctly?
101
107
 
102
108
  ### 5. Laws (optional)
@@ -110,22 +116,18 @@ If yes, walk through each law using the same format as `add-law`:
110
116
  - Check for conflicts with global laws and any existing type-specific laws
111
117
  - Confirm with the user
112
118
 
113
- Each law may declare an optional `validators:` block. Include validators only when a deterministic check is needed. The format matches `add-law`:
114
-
115
- ```markdown
116
- ## <law-id>
117
-
118
- <What this law checks — one or two sentences.>
119
-
120
- validators:
121
- - id: validator-id
122
- command: ./script.sh
123
- failure-means: (optional description)
124
- ```
125
-
126
- The `validators` block is optional. When present, `quench` runs each validator for this law. Validator scripts live within the artefact type directory (e.g., `foundry/artefacts/<type>/check-foo.mjs`).
119
+ Each law may declare an optional `validators:` block; the YAML shape,
120
+ JSONL output contract, `{pattern}` / `{files}` placeholders, skip
121
+ rule, working directory, and a worked example are documented once in
122
+ the `add-law` skill under **§7a. Validator contract**. Authors of
123
+ type-specific laws must follow that contract — do not invent a
124
+ different one here.
127
125
 
128
- **Use existing libraries:** Before writing custom validation logic, search npm for well-tested libraries that solve the problem (e.g., `syllable` for syllable counting, `natural` for NLP tasks). Hand-rolled heuristics are fragile — prefer battle-tested packages. Install them as project dependencies.
126
+ **Use existing libraries:** Before writing custom validation logic,
127
+ search npm for well-tested libraries that solve the problem (e.g.
128
+ `syllable` for syllable counting, `natural` for NLP tasks).
129
+ Hand-rolled heuristics are fragile — prefer battle-tested packages.
130
+ Install them as project dependencies.
129
131
 
130
132
  Check the project's `package.json` for `"type": "module"`:
131
133
  - If ESM (`"type": "module"`): use `import` syntax, or name scripts with `.mjs` extension
@@ -177,7 +179,12 @@ Show the user the resulting commit hash from the response.
177
179
 
178
180
  ### 9. Add laws file (if defined)
179
181
 
180
- The create tool writes only `definition.md`. If you drafted any type-specific laws in step 5, append them to `foundry/artefacts/<id>/laws.md` by hand on this same `config/*` branch (use the `Edit` tool to create the file) and commit that as a separate microcommit.
182
+ If you drafted any type-specific laws in step 5, add them via
183
+ `foundry_config_add_law` (one call per law) with
184
+ `target: { kind: "type-specific", typeId: "<id>" }`. The first call
185
+ creates `foundry/artefacts/<id>/laws.md`; subsequent calls append to
186
+ that same file. Each call produces its own microcommit. See the
187
+ `add-law` skill for the full protocol.
181
188
 
182
189
  ### 10. Confirm
183
190
 
@@ -71,7 +71,9 @@ The `law-id` (heading) should be:
71
71
  - Short but descriptive
72
72
  - Unique across all laws (global and type-specific)
73
73
 
74
- The `validators:` block is optional. Include it only if you want to add validation commands for this law.
74
+ The `validators:` block is optional. Include it only when a
75
+ deterministic check can decide pass/fail. See **Validator contract**
76
+ below for the exact shape a validator command must satisfy.
75
77
 
76
78
  ### 3. Check for conflicts
77
79
 
@@ -142,7 +144,11 @@ The tool:
142
144
  - writes the law file at the path determined by `target`;
143
145
  - produces one git commit on the current `config/*` branch.
144
146
 
145
- If the tool returns `{ ok: false, errors }` because the target file already exists, use `foundry_config_edit_law({ id: "<law-id>", body: "<updated-body>" })` to modify the law.
147
+ The tool appends to an existing `laws.md` automatically when the
148
+ new `## <law-id>` heading is not already present. It only errors when
149
+ a law with the same id is already in the file — in that case use
150
+ `foundry_config_edit_law({ id: "<law-id>", body: "<updated-body>" })`
151
+ to modify the existing law in place.
146
152
 
147
153
  Show the user the resulting commit hash from the response.
148
154
 
@@ -150,6 +156,111 @@ Show the user the resulting commit hash from the response.
150
156
 
151
157
  After the file is created, confirm the law id is unique across all law files. If a collision exists, ask the user to rename and edit by hand on this branch.
152
158
 
159
+ ### 7a. Validator contract
160
+
161
+ A law's `validators:` entries declare CLI commands that `quench` runs
162
+ during a cycle. The plugin parses each command's stdout as **JSONL**
163
+ (one JSON object per line). Authors must follow this contract exactly;
164
+ nothing in plugin source needs to be read.
165
+
166
+ #### Output format (stdout, parsed as JSONL)
167
+
168
+ One JSON object per line. Empty lines are ignored. Required fields:
169
+
170
+ - `file` *(string)* — path of the offending file, relative to the
171
+ worktree root. Must match at least one of the artefact type's
172
+ `file-patterns:`; otherwise the line becomes a validator-level
173
+ error, not feedback.
174
+ - `text` *(string)* — the feedback message.
175
+
176
+ Optional fields:
177
+
178
+ - `location` *(string, e.g. `"3:1"`)* — line:column reference,
179
+ prepended to `text` as `file:location — text`.
180
+ - `severity` *(string, e.g. `"error"` or `"warning"`)* — prepended to
181
+ `text` as `[severity] file:location — text` (or `[severity] file —
182
+ text` when no `location`).
183
+
184
+ Anything else on the line is preserved verbatim on the parsed item.
185
+ The validator's exit code is **ignored** — the parser reads stdout
186
+ either way, and falls back to stderr when stdout is empty (so tools
187
+ like `rg` that exit non-zero on hits still work).
188
+
189
+ #### Command placeholders
190
+
191
+ Inside `command:`, two placeholders may appear, alone, together, or
192
+ not at all. They are recognised only as standalone tokens (bounded by
193
+ whitespace or string start/end). Authors may wrap a placeholder in
194
+ single or double quotes for readability — surrounding quotes are
195
+ stripped before substitution.
196
+
197
+ - `{pattern}` → the artefact type's `file-patterns:` rendered as
198
+ space-separated, shell-quoted globs (e.g.
199
+ `'haikus/*.md' 'drafts/*.md'`). Use this when the validator does
200
+ its own globbing or accepts globs directly (e.g. `rg --glob`).
201
+ - `{files}` → the matching files in the worktree, rendered as
202
+ space-separated, shell-quoted paths (e.g.
203
+ `'haikus/one.md' 'haikus/two.md'`). Use this when the validator
204
+ takes an explicit list of file paths.
205
+
206
+ A command with neither placeholder runs verbatim — useful for
207
+ self-resolving validators such as `npm test`, `tsc --noEmit`, or
208
+ `pnpm run lint`.
209
+
210
+ #### Skip rule
211
+
212
+ A validator is skipped iff its command contains `{files}` and there
213
+ are no matching files in the worktree. Commands using `{pattern}` only,
214
+ or no placeholders at all, always run.
215
+
216
+ #### Working directory
217
+
218
+ Validators run with `cwd` set to the worktree root, so root-level
219
+ `node_modules/`, `package.json`, and project tooling all resolve
220
+ normally. Do not assume the validator runs from inside the artefact
221
+ type's directory.
222
+
223
+ #### Worked example
224
+
225
+ A validator that checks each `.md` file in `haikus/` has exactly three
226
+ non-empty lines, attached to a haiku artefact type
227
+ (`file-patterns: ["haikus/*.md"]`):
228
+
229
+ `foundry/artefacts/haiku/check-line-count.mjs`:
230
+
231
+ ~~~js
232
+ #!/usr/bin/env node
233
+ import { readFile } from 'node:fs/promises';
234
+
235
+ for (const file of process.argv.slice(2)) {
236
+ const content = await readFile(file, 'utf8');
237
+ const lines = content
238
+ .split('\n')
239
+ .map((l) => l.trim())
240
+ .filter((l) => l.length > 0);
241
+ if (lines.length !== 3) {
242
+ process.stdout.write(JSON.stringify({
243
+ file,
244
+ text: `Expected 3 non-empty lines, got ${lines.length}.`,
245
+ severity: 'error',
246
+ }) + '\n');
247
+ }
248
+ }
249
+ ~~~
250
+
251
+ Declared in the law:
252
+
253
+ ~~~markdown
254
+ ## three-lines
255
+
256
+ A haiku must consist of exactly three non-empty lines.
257
+
258
+ validators:
259
+ - id: line-count
260
+ command: node foundry/artefacts/haiku/check-line-count.mjs {files}
261
+ failure-means: The artefact file does not contain exactly three non-empty lines.
262
+ ~~~
263
+
153
264
  ### 8. Editing existing laws (prose or validators)
154
265
 
155
266
  When the user wants to modify an existing law — whether updating the prose description or adding/changing validators — use this flow:
@@ -31,9 +31,24 @@ Set up the `foundry/` directory structure in the current project.
31
31
 
32
32
  3. **Update `.gitignore`**
33
33
 
34
- Append `.snapshots/` to the project's `.gitignore` (creating the file if absent). This directory is where dry-run snapshots are written and must never be committed.
34
+ Append the following lines to the project's `.gitignore` (creating
35
+ the file if absent), skipping any that are already present:
35
36
 
36
- The plugin will idempotently append `.foundry/` itself on first boot, so you do not need to add that line.
37
+ ```
38
+ .snapshots/
39
+ node_modules/
40
+ .DS_Store
41
+ ```
42
+
43
+ - `.snapshots/` keeps dry-run snapshots out of git.
44
+ - `node_modules/` keeps any npm dependencies (e.g. validator
45
+ packages) out of git. Without it, foundry's `config/*` tools
46
+ reject calls with `unexpected_files` as soon as the user runs
47
+ `npm install`.
48
+ - `.DS_Store` keeps macOS metadata out of git.
49
+
50
+ The plugin will idempotently append `.foundry/` itself on first
51
+ boot, so you do not need to add that line.
37
52
 
38
53
  4. **Generate foundry agent files**
39
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@really-knows-ai/foundry",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "A skill-driven framework for governed artefact generation with AI coding tools. Define your own artefact types, laws, and flows — Foundry handles the forge → quench → appraise pipeline with deterministic routing, quality gates, and iterative refinement.",
5
5
  "type": "module",
6
6
  "main": "dist/.opencode/plugins/foundry.js",