@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.
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +53 -69
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +37 -29
- package/dist/CHANGELOG.md +69 -0
- package/dist/skills/add-artefact-type/SKILL.md +28 -21
- package/dist/skills/add-law/SKILL.md +113 -2
- package/dist/skills/init-foundry/SKILL.md +17 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
256
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
73
|
+
async function runLawValidators(law, patterns, substitutions, worktree, results) {
|
|
74
74
|
for (const validator of law.validators) {
|
|
75
|
-
// Skip
|
|
76
|
-
//
|
|
77
|
-
if (
|
|
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,
|
|
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
|
|
164
|
-
|
|
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}
|
|
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
|
-
*
|
|
241
|
-
* whitespace or
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|
|
247
|
-
* @param {
|
|
248
|
-
* @returns {string}
|
|
253
|
+
* @param {string} command
|
|
254
|
+
* @param {{ pattern: string, files: string }} substitutions
|
|
255
|
+
* @returns {string}
|
|
249
256
|
*/
|
|
250
|
-
export function expandValidatorCommand(command,
|
|
251
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
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: <
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|