@really-knows-ai/foundry 3.7.0 → 3.7.3
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 +3 -3
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +24 -36
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +31 -1
- package/dist/CHANGELOG.md +22 -0
- package/dist/scripts/lib/git-policy.js +18 -0
- package/dist/skills/add-law/SKILL.md +54 -5
- package/package.json +1 -1
|
@@ -318,7 +318,7 @@ function makeReadLawTool(tool) {
|
|
|
318
318
|
function makeAddLawTool(tool) {
|
|
319
319
|
return tool({
|
|
320
320
|
description: 'Add a new law (config-tier; requires a config/* branch). ' +
|
|
321
|
-
'
|
|
321
|
+
'Args: id, name, description, passing, failing, target ({kind, file|typeId}), validators ([{id, command, failureMeans?}]). Every validator needs a companion test (TDD) before the law is created.',
|
|
322
322
|
args: {
|
|
323
323
|
id: tool.schema.string().describe('Law identifier. Becomes the ## <id> heading.'),
|
|
324
324
|
name: tool.schema.string().describe('Human-readable name stored as prose after heading.'),
|
|
@@ -332,9 +332,9 @@ function makeAddLawTool(tool) {
|
|
|
332
332
|
}).describe('Where to write the law'),
|
|
333
333
|
validators: tool.schema.array(tool.schema.object({
|
|
334
334
|
id: tool.schema.string().describe('Validator identifier'),
|
|
335
|
-
command: tool.schema.string().describe('CLI command with optional {pattern} / {files} placeholders. Prefer
|
|
335
|
+
command: tool.schema.string().describe('CLI command with optional {pattern} / {files} placeholders. Prefer .mjs scripts (e.g. "node foundry/artefacts/<type>/check.mjs {files}") with a companion .test.js file (TDD). Stdout must be NDJSON: one JSON object per line with required fields "file" (relative path) and "text" (message). Optional: "location" (line:col), "severity" (error|warning). Exit code is ignored.'),
|
|
336
336
|
failureMeans: tool.schema.string().optional().describe('Description of what failure means'),
|
|
337
|
-
})).optional().describe('Optional deterministic validators'),
|
|
337
|
+
})).optional().describe('Optional deterministic validators. Each requires a companion test file.'),
|
|
338
338
|
},
|
|
339
339
|
execute: guarded('foundry_config_add_law', CREATE_GUARDS, executeAddLaw, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
340
340
|
});
|
|
@@ -4,8 +4,7 @@ import { existsSync, unlinkSync, writeFileSync, readFileSync } from 'fs';
|
|
|
4
4
|
import { slugify } from '../../../scripts/lib/slug.js';
|
|
5
5
|
import { CONFIG_RE, DRY_RUN_RE } from '../../../scripts/lib/branch-guard.js';
|
|
6
6
|
import { finishWorkBranchWithArchive } from '../../../scripts/lib/git-finish/work-finish.js';
|
|
7
|
-
import {
|
|
8
|
-
import { asyncIoFactory } from './helpers.js';
|
|
7
|
+
import { checkConfigBranchFiles } from '../../../scripts/lib/git-policy.js';
|
|
9
8
|
|
|
10
9
|
const WORK_FILES = ['WORK.md', 'WORK.history.yaml', 'WORK.feedback.yaml'];
|
|
11
10
|
|
|
@@ -233,15 +232,35 @@ export function finishBranchCommon({ branchName, branchType, base, cwd, args })
|
|
|
233
232
|
const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
234
233
|
const planned = computeFinishPlan({ branchName, branchType, base, args, cwd });
|
|
235
234
|
if (args.confirm !== true) return makeConfirmRefusal(planned);
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
238
|
-
if (branchType === 'work') deleteWorkFilesAndCommit(planned.filesToDelete, cwd, branchName);
|
|
235
|
+
const guardErr = runPreMergeGuards({ branchName, branchType, base, cwd, opts, planned });
|
|
236
|
+
if (guardErr) return guardErr;
|
|
239
237
|
const mergeErr = squashMergeIntoBase(base, branchName, branchType, opts);
|
|
240
238
|
if (mergeErr) return mergeErr;
|
|
241
239
|
const { hash } = commitAndDeleteBranch(args.message, branchName, opts);
|
|
242
240
|
return JSON.stringify({ ok: true, hash, branch: base });
|
|
243
241
|
}
|
|
244
242
|
|
|
243
|
+
function runPreMergeGuards({ branchName, branchType, base, cwd, opts, planned }) {
|
|
244
|
+
const dirty = dirtyTrackedFiles(cwd);
|
|
245
|
+
if (dirty.length) return makeDirtyRefusal(dirty);
|
|
246
|
+
if (branchType === 'work') {
|
|
247
|
+
deleteWorkFilesAndCommit(planned.filesToDelete, cwd, branchName);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
if (branchType !== 'config') return null;
|
|
251
|
+
const diff = execFileSync('git', ['diff', '--name-only', `${base}..${branchName}`],
|
|
252
|
+
{ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
253
|
+
const result = checkConfigBranchFiles(diff);
|
|
254
|
+
if (result) {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
ok: false,
|
|
257
|
+
error: 'Config branches may only change files inside foundry/. Outside files detected:',
|
|
258
|
+
outside: result.files,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
245
264
|
// -- finishWorkBranch helpers --
|
|
246
265
|
|
|
247
266
|
function makeExecGit(cwd, opts) {
|
|
@@ -321,34 +340,3 @@ export function finishConfigBranch({ configBranch, base, cwd, args }) {
|
|
|
321
340
|
});
|
|
322
341
|
}
|
|
323
342
|
|
|
324
|
-
// -- finishDryRunBranch --
|
|
325
|
-
|
|
326
|
-
export async function finishDryRunBranch({ branch, args, cwd }) {
|
|
327
|
-
const io = asyncIoFactory({ worktree: cwd });
|
|
328
|
-
const exec = (argv) => execFileSync('git', argv,
|
|
329
|
-
{ cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
330
|
-
|
|
331
|
-
if (args.confirm !== true) {
|
|
332
|
-
return JSON.stringify({
|
|
333
|
-
ok: false,
|
|
334
|
-
error: 'foundry_git_finish requires {confirm: true} to perform destructive operations. Re-invoke with confirm:true to apply the plan.',
|
|
335
|
-
planned: {
|
|
336
|
-
branch,
|
|
337
|
-
action: 'snapshot + discard (dry-run finish)',
|
|
338
|
-
snapshotPath: '.snapshots/<runId> (computed at apply time)',
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
try {
|
|
344
|
-
const out = await finishDryRun({
|
|
345
|
-
message: args.message, branch, io, execFile: exec,
|
|
346
|
-
});
|
|
347
|
-
return JSON.stringify(out);
|
|
348
|
-
} catch (err) {
|
|
349
|
-
return JSON.stringify({
|
|
350
|
-
ok: false,
|
|
351
|
-
error: `foundry_git_finish: dry-run finish failed: ${err.message ?? String(err)}`,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
classifyBranch,
|
|
7
7
|
finishWorkBranch,
|
|
8
8
|
finishConfigBranch,
|
|
9
|
-
finishDryRunBranch,
|
|
10
9
|
KIND_DRY_RUN,
|
|
11
10
|
KINDS,
|
|
12
11
|
} from './git-helpers.js';
|
|
@@ -14,6 +13,7 @@ import { makeIO, makeExec, asyncIoFactory } from './helpers.js';
|
|
|
14
13
|
import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
15
14
|
import { currentBranch } from '../../../scripts/lib/branch-guard.js';
|
|
16
15
|
import { truncateTrace } from '../../../scripts/lib/tracing.js';
|
|
16
|
+
import { finishDryRun } from '../../../scripts/lib/snapshot/finish.js';
|
|
17
17
|
|
|
18
18
|
function refuse(error) { return JSON.stringify({ error }); }
|
|
19
19
|
|
|
@@ -107,6 +107,36 @@ function refuseUnknownFinishBranch(branch) {
|
|
|
107
107
|
`(expected work/<x>, config/<x>, or dry-run/<x>/<y>).`);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
async function finishDryRunBranch({ branch, args, cwd }) {
|
|
111
|
+
const io = asyncIoFactory({ worktree: cwd });
|
|
112
|
+
const exec = (argv) => execFileSync('git', argv,
|
|
113
|
+
{ cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
114
|
+
|
|
115
|
+
if (args.confirm !== true) {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
ok: false,
|
|
118
|
+
error: 'foundry_git_finish requires {confirm: true} to perform destructive operations. Re-invoke with confirm:true to apply the plan.',
|
|
119
|
+
planned: {
|
|
120
|
+
branch,
|
|
121
|
+
action: 'snapshot + discard (dry-run finish)',
|
|
122
|
+
snapshotPath: '.snapshots/<runId> (computed at apply time)',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const out = await finishDryRun({
|
|
129
|
+
message: args.message, branch, io, execFile: exec,
|
|
130
|
+
});
|
|
131
|
+
return JSON.stringify(out);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
ok: false,
|
|
135
|
+
error: `foundry_git_finish: dry-run finish failed: ${err.message ?? String(err)}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
function routeDryRunFinish(branch, args, cwd) {
|
|
111
141
|
if (args.baseBranch !== undefined)
|
|
112
142
|
return refuseBaseBranchForDryRun();
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.3] - 2026-05-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Config branch file enforcement: `foundry_git_finish` on a `config/*` branch now validates that every changed file lives inside `foundry/` or is tool-managed. Files outside `foundry/` are rejected with a clear list of offending paths. This prevents test fixtures, artefact output, or other non-config files from accidentally landing on config branches.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- The `add-law` skill clarifies that all flow artefacts — validator scripts, tests, and test fixtures — must live inside `foundry/`. Test fixtures colocate under `foundry/artefacts/<type>/test/fixtures/`. The worked example and "what you do NOT do" list updated accordingly.
|
|
12
|
+
|
|
13
|
+
## [3.7.2] - 2026-05-27
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Every validator now requires a companion test file written with TDD. The `add-law` skill walks through TDD (test first, confirm failure, implement, verify pass), produces a `.test.js` file alongside each validator, and refuses to create validators without passing tests. The `foundry_config_add_law` tool description surfaces the requirement.
|
|
18
|
+
|
|
19
|
+
## [3.7.1] - 2026-05-27
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `mock.module` calls in `quench-module.test.js` use the canonical `namedExports` key instead of the deprecated `exports` alias. Node 22 does not recognise the old key, causing tests to fail with "does not provide an export named `computeArtefactVersion`".
|
|
24
|
+
|
|
3
25
|
## [3.7.0] - 2026-05-27
|
|
4
26
|
|
|
5
27
|
### Added
|
|
@@ -99,3 +99,21 @@ export function allowedPatternsForStage({ stageBase, forgeFilePatterns = [] } =
|
|
|
99
99
|
if (stageBase === 'assay') return ['foundry-memory/**'];
|
|
100
100
|
return [];
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check that every file changed on a config branch lives inside foundry/ or
|
|
105
|
+
* is tool-managed. Returns null when clean, or { files: [...] } when outside
|
|
106
|
+
* files are detected.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} diffOut - Raw `git diff --name-only base..branch` output
|
|
109
|
+
* @returns {null|{files: string[]}}
|
|
110
|
+
*/
|
|
111
|
+
export function checkConfigBranchFiles(diffOut) {
|
|
112
|
+
if (!diffOut) return null;
|
|
113
|
+
const toolManaged = new Set(TOOL_MANAGED);
|
|
114
|
+
const outside = diffOut.split('\n')
|
|
115
|
+
.map(f => f.trim())
|
|
116
|
+
.filter(f => f.length > 0 && !toolManaged.has(f) && !f.startsWith('foundry/'))
|
|
117
|
+
.filter(f => !TOOL_MANAGED_PREFIX.some(p => f.startsWith(p)));
|
|
118
|
+
return outside.length ? { files: outside } : null;
|
|
119
|
+
}
|
|
@@ -68,6 +68,10 @@ Walk the user through which elements of the law can be validated deterministical
|
|
|
68
68
|
|
|
69
69
|
For each script-checkable element, write a standalone `.mjs` script next to the artefacts it validates (e.g. `foundry/artefacts/<type>/check-line-count.mjs`) and reference it in the command (e.g. `node foundry/artefacts/<type>/check-line-count.mjs {files}`). Place validators alongside the artefacts so they colocate with what they validate. Use existing project dependencies and Node.js built‑ins. Hand‑rolled heuristics (custom syllable counters, regex parsers, manual character walks) are a last resort — they produce false positives, waste tokens on debugging, and break on edge cases. Install a library instead. Only write validation logic from scratch when no npm package exists for the task and the heuristic is trivially correct.
|
|
70
70
|
|
|
71
|
+
All flow artefacts — validator scripts, tests, test fixtures — live inside `foundry/`. Never place artefacts outside `foundry/`. Test fixtures colocate with the validator's test file under `foundry/artefacts/<type>/test/fixtures/`. When test fixtures match an artefact type's `file-patterns:`, they trigger false-positive quench feedback during flow runs. Keeping them inside `foundry/` prevents this.
|
|
72
|
+
|
|
73
|
+
Every validator carries a companion test file alongside it (e.g. `check-line-count.test.js`). The test uses Node's built‑in test runner — `node --test check-line-count.test.js`. Follow TDD: write the test, confirm it fails against a current artefact, implement the validator, verify the test passes. The test feeds sample inputs to the validator script and asserts the correct JSONL output on stdout — it validates the JSONL contract, not just that the script runs.
|
|
74
|
+
|
|
71
75
|
**Validators**: Ask about `validators` (optional) — offer to create one or skip.
|
|
72
76
|
|
|
73
77
|
**Conflict check**: Read all existing laws that would apply to the same artefact types. Check for contradiction, duplication, or overlap. If any conflict is found, present it to the user:
|
|
@@ -85,7 +89,7 @@ For each script-checkable element, write a standalone `.mjs` script next to the
|
|
|
85
89
|
|
|
86
90
|
### 2. Plan
|
|
87
91
|
|
|
88
|
-
Present a structured summary: law id, name, description, passing/failing criteria, target (global or type-specific with typeId),
|
|
92
|
+
Present a structured summary: law id, name, description, passing/failing criteria, target (global or type-specific with typeId), validators (which elements are checked deterministically), and the companion test file for each validator. Ask: "Does this capture what you want, or should we adjust the wording?" Iterate until the user is satisfied.
|
|
89
93
|
|
|
90
94
|
### 3. Confirm
|
|
91
95
|
|
|
@@ -93,9 +97,15 @@ Ask: "Proceed with this plan?" — wait for user answer before building. If the
|
|
|
93
97
|
|
|
94
98
|
### 4. Build
|
|
95
99
|
|
|
96
|
-
1. **
|
|
100
|
+
1. **Write validators with TDD**: For each validator declared in the plan:
|
|
101
|
+
|
|
102
|
+
a. **Write the test first** — create a companion test file alongside the validator (e.g. `foundry/artefacts/<type>/check-line-count.test.js`). The test imports or spawns the validator script with sample inputs and asserts the correct JSONL output on stdout. Run `node --test` to confirm it fails.
|
|
97
103
|
|
|
98
|
-
|
|
104
|
+
b. **Implement the validator** — write the `.mjs` script. Run the test again to confirm it passes. Do not commit the validator without its passing test.
|
|
105
|
+
|
|
106
|
+
2. **Validate**: Call `foundry_config_validate_law({ name: "<id>", body: "<assembled markdown>" })`. Assemble the body from the fields using the `## <id>` heading format the tool produces internally. If the result is `{ ok: false, errors: [...] }`, address each error and re-run until `{ ok: true }`. Common issues: missing required frontmatter keys, references to artefact types that do not exist yet.
|
|
107
|
+
|
|
108
|
+
3. **Create**: Translate the scope into the `target` argument:
|
|
99
109
|
- Global → `target: { kind: "global", file: "<file-name>.md" }`
|
|
100
110
|
- Type-specific → `target: { kind: "type-specific", typeId: "<artefact-type>" }`
|
|
101
111
|
|
|
@@ -117,7 +127,7 @@ Ask: "Proceed with this plan?" — wait for user answer before building. If the
|
|
|
117
127
|
|
|
118
128
|
The tool appends to an existing `laws.md` automatically when the new law id is not already present. It only errors when a law with the same id is already in the file — in that case use `foundry_config_edit_law({ id: "<law-id>", description: "<updated>", passing: "<updated>", failing: "<updated>" })` to modify the existing law in place.
|
|
119
129
|
|
|
120
|
-
|
|
130
|
+
4. **Verify uniqueness**: After the file is created, confirm the law id is unique across all law files. If a collision exists, read the colliding law, present the conflict to the user, propose a rename or merge, ask one focused question about the user's preference, then write and commit the resolution.
|
|
121
131
|
|
|
122
132
|
### 5. Editing existing laws (prose or validators)
|
|
123
133
|
|
|
@@ -145,7 +155,7 @@ Then proceed with the update.
|
|
|
145
155
|
|
|
146
156
|
> 🔍 **Drift check:** Verify that the changed validator still aligns with the law's prose. If the validator has narrowed or broadened, the prose may need a corresponding update.
|
|
147
157
|
|
|
148
|
-
|
|
158
|
+
After the validator implementation changes, update the companion test file. Run the tests to confirm they pass against the updated validator before committing.
|
|
149
159
|
|
|
150
160
|
#### 5e. Apply the update
|
|
151
161
|
|
|
@@ -258,8 +268,47 @@ validators:
|
|
|
258
268
|
failure-means: The artefact file does not contain exactly three non-empty lines.
|
|
259
269
|
~~~
|
|
260
270
|
|
|
271
|
+
#### Companion test
|
|
272
|
+
|
|
273
|
+
`foundry/artefacts/haiku/check-line-count.test.js`:
|
|
274
|
+
|
|
275
|
+
~~~js
|
|
276
|
+
import { describe, it } from 'node:test';
|
|
277
|
+
import { execSync } from 'node:child_process';
|
|
278
|
+
import assert from 'node:assert/strict';
|
|
279
|
+
|
|
280
|
+
describe('check-line-count', () => {
|
|
281
|
+
it('passes for exactly three non-empty lines', () => {
|
|
282
|
+
const result = execSync(
|
|
283
|
+
`node foundry/artefacts/haiku/check-line-count.mjs foundry/artefacts/haiku/test/fixtures/haiku-valid.md`,
|
|
284
|
+
{ encoding: 'utf8' },
|
|
285
|
+
);
|
|
286
|
+
assert.strictEqual(result.trim(), '');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('reports an error for fewer than three lines', () => {
|
|
290
|
+
const result = execSync(
|
|
291
|
+
`node foundry/artefacts/haiku/check-line-count.mjs foundry/artefacts/haiku/test/fixtures/haiku-short.md`,
|
|
292
|
+
{ encoding: 'utf8' },
|
|
293
|
+
);
|
|
294
|
+
assert.match(result, /Expected 3 non-empty lines/);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('reports an error for more than three lines', () => {
|
|
298
|
+
const result = execSync(
|
|
299
|
+
`node foundry/artefacts/haiku/check-line-count.mjs foundry/artefacts/haiku/test/fixtures/haiku-long.md`,
|
|
300
|
+
{ encoding: 'utf8' },
|
|
301
|
+
);
|
|
302
|
+
assert.match(result, /Expected 3 non-empty lines/);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
~~~
|
|
306
|
+
|
|
261
307
|
## What you do NOT do
|
|
262
308
|
|
|
263
309
|
- You do not skip the conflict check
|
|
264
310
|
- You do not silently overwrite existing laws
|
|
265
311
|
- You do not create artefact types unless the user's stated goal clearly requires it; ask one focused question when multiple designs are plausible
|
|
312
|
+
- You do not write validators without companion tests
|
|
313
|
+
- You do not place flow artefacts or test fixtures outside `foundry/`
|
|
314
|
+
- You do not accept test failures — fix the validator and retry until every test passes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@really-knows-ai/foundry",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.3",
|
|
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",
|