@really-knows-ai/foundry 2.3.2 → 3.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.
- package/README.md +180 -369
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +393 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +46 -24
- package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -100
- package/docs/concepts.md +0 -122
- package/docs/getting-started.md +0 -187
- package/docs/work-spec.md +0 -207
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -111
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// Tools for reading, adding, and editing laws
|
|
2
|
+
// foundry_config_read_law: read-only, returns full markdown (no branch guard)
|
|
3
|
+
// foundry_config_add_law: write + commit on config/* branch
|
|
4
|
+
// foundry_config_edit_law: update body and commit on config/* branch
|
|
5
|
+
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { validate as validateLaw } from '../../../scripts/lib/config-validators/law.js';
|
|
8
|
+
import { requireGitRepo, requireFoundryRoot } from '../../../scripts/lib/foundational-guards.js';
|
|
9
|
+
import { requireOnConfigBranch } from '../../../scripts/lib/branch-guard.js';
|
|
10
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
11
|
+
import { UnexpectedFilesError, commitWithPolicy } from '../../../scripts/lib/git-bridge.js';
|
|
12
|
+
import { makeIO, makeExec, makeAsyncIO, errorJson, branchIoFactory, asyncIoFactory } from './helpers.js';
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
|
+
|
|
15
|
+
// --- utility functions -------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function contentContainsLaw(content, lawId) {
|
|
18
|
+
const pattern = new RegExp(`^## ${lawId}(?:\\s|$)`, 'm');
|
|
19
|
+
return pattern.test(content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findLawStart(lines, lawId) {
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const heading = lines[i].match(/^## (.+)/);
|
|
25
|
+
if (heading && heading[1].trim() === lawId) {
|
|
26
|
+
return i;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findLawEnd(lines, startIdx) {
|
|
33
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
34
|
+
if (lines[i].match(/^## (.+)/)) {
|
|
35
|
+
return i;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return lines.length;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract full markdown for a single law from file content
|
|
42
|
+
function extractLawMarkdown(content, lawId) {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
const startIdx = findLawStart(lines, lawId);
|
|
45
|
+
|
|
46
|
+
if (startIdx < 0) return null;
|
|
47
|
+
|
|
48
|
+
const endIdx = findLawEnd(lines, startIdx);
|
|
49
|
+
const lawLines = lines.slice(startIdx, endIdx);
|
|
50
|
+
|
|
51
|
+
while (lawLines.length > 0 && lawLines[lawLines.length - 1] === '') {
|
|
52
|
+
lawLines.pop();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return lawLines.join('\n') + '\n';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function searchGlobalLaws(io, foundryDir, lawId) {
|
|
59
|
+
const globalLawsDir = join(foundryDir, 'laws');
|
|
60
|
+
if (!(await io.exists(globalLawsDir))) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const files = await io.readDir(globalLawsDir);
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (!file.endsWith('.md')) continue;
|
|
67
|
+
const path = join(globalLawsDir, file);
|
|
68
|
+
const content = await io.readFile(path);
|
|
69
|
+
if (contentContainsLaw(content, lawId)) {
|
|
70
|
+
return { path, fullMarkdown: content, source: 'global' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function searchTypeSpecificLaws(io, foundryDir, lawId) {
|
|
78
|
+
const artefactsDir = join(foundryDir, 'artefacts');
|
|
79
|
+
if (!(await io.exists(artefactsDir))) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const types = await io.readDir(artefactsDir);
|
|
84
|
+
for (const typeId of types) {
|
|
85
|
+
const typeLawsPath = join(artefactsDir, typeId, 'laws.md');
|
|
86
|
+
if (!(await io.exists(typeLawsPath))) continue;
|
|
87
|
+
|
|
88
|
+
const content = await io.readFile(typeLawsPath);
|
|
89
|
+
if (contentContainsLaw(content, lawId)) {
|
|
90
|
+
return { path: typeLawsPath, fullMarkdown: content, source: `type:${typeId}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
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
|
+
|
|
108
|
+
return { found: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- guard helpers ---------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
function gitRepoGuard(_args, context) {
|
|
114
|
+
return requireGitRepo(makeIO(context.worktree));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function foundryRootGuard(_args, context) {
|
|
118
|
+
return requireFoundryRoot(makeIO(context.worktree));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function configBranchGuard(_args, context) {
|
|
122
|
+
return requireOnConfigBranch({ exec: makeExec(context.worktree) });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
126
|
+
|
|
127
|
+
const GIT_COMMAND = 'git';
|
|
128
|
+
|
|
129
|
+
function makeExecFile(cwd) {
|
|
130
|
+
return (argv) => execFileSync(GIT_COMMAND, argv, { cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const READ_GUARDS = [gitRepoGuard, foundryRootGuard];
|
|
134
|
+
const CREATE_GUARDS = [gitRepoGuard, foundryRootGuard, configBranchGuard, gateNotFailed];
|
|
135
|
+
const EDIT_GUARDS = [gitRepoGuard, foundryRootGuard, configBranchGuard, gateNotFailed];
|
|
136
|
+
|
|
137
|
+
// --- read law executor -------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
async function executeReadLaw(args, context) {
|
|
140
|
+
try {
|
|
141
|
+
const io = makeAsyncIO(context.worktree);
|
|
142
|
+
const result = await findLawByID(io, 'foundry', args.id);
|
|
143
|
+
if (!result.found) {
|
|
144
|
+
return JSON.stringify({
|
|
145
|
+
ok: false,
|
|
146
|
+
errors: [`Law "${args.id}" not found`],
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const markdown = extractLawMarkdown(result.fullMarkdown, args.id);
|
|
150
|
+
if (!markdown) {
|
|
151
|
+
return JSON.stringify({
|
|
152
|
+
ok: false,
|
|
153
|
+
errors: [`Could not extract law "${args.id}" from file`],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return JSON.stringify({
|
|
157
|
+
ok: true,
|
|
158
|
+
id: args.id,
|
|
159
|
+
markdown,
|
|
160
|
+
source: result.source,
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return errorJson(err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- add law validation helpers -------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function validateAddLawTarget(target) {
|
|
170
|
+
const err = validateAddLawTargetStruct(target);
|
|
171
|
+
if (err) return err;
|
|
172
|
+
return target.kind === 'global' ? validateGlobalLawTarget(target) : validateTypeSpecLawTarget(target);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateAddLawTargetStruct(target) {
|
|
176
|
+
if (!target || typeof target !== 'object') {
|
|
177
|
+
return 'target argument is required (object with kind + locator)';
|
|
178
|
+
}
|
|
179
|
+
const kinds = ['global', 'type-specific'];
|
|
180
|
+
if (!kinds.includes(target.kind)) return `unknown target.kind: ${target.kind}`;
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateGlobalLawTarget(target) {
|
|
185
|
+
if (typeof target.file !== 'string' || !target.file.trim()) {
|
|
186
|
+
return 'target.file is required for kind: "global"';
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function validateTypeSpecLawTarget(target) {
|
|
192
|
+
if (typeof target.typeId !== 'string' || !target.typeId.trim()) {
|
|
193
|
+
return 'target.typeId is required for kind: "type-specific"';
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function computeTargetPath(target) {
|
|
199
|
+
if (target?.kind === 'global') {
|
|
200
|
+
return join('foundry', 'laws', target.file);
|
|
201
|
+
}
|
|
202
|
+
return join('foundry', 'artefacts', target.typeId, 'laws.md');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- add law executor --------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
async function validateAddLawPrerequisites(io, args) {
|
|
208
|
+
const targetError = validateAddLawTarget(args.target);
|
|
209
|
+
if (targetError) {
|
|
210
|
+
return { error: targetError };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const path = computeTargetPath(args.target);
|
|
214
|
+
const validation = await validateLaw({ body: args.body, io });
|
|
215
|
+
if (!validation.ok) {
|
|
216
|
+
return validation;
|
|
217
|
+
}
|
|
218
|
+
|
|
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
|
+
}
|
|
225
|
+
|
|
226
|
+
return { ok: true, path };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function executeAddLaw(args, context) {
|
|
230
|
+
const io = makeAsyncIO(context.worktree);
|
|
231
|
+
const execFile = makeExecFile(context.worktree);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
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
|
+
}
|
|
241
|
+
|
|
242
|
+
const path = prereq.path;
|
|
243
|
+
|
|
244
|
+
await io.mkdirp(dirname(path));
|
|
245
|
+
await io.writeFile(path, args.body);
|
|
246
|
+
|
|
247
|
+
const sha = commitWithPolicy({
|
|
248
|
+
message: `config: add law ${args.name}\n\nvia foundry_config_add_law`,
|
|
249
|
+
allowedPatterns: ['foundry/**'],
|
|
250
|
+
execFile,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return JSON.stringify({ ok: true, path, sha });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err instanceof UnexpectedFilesError) {
|
|
256
|
+
return JSON.stringify({ error: err.message, affected_files: err.files });
|
|
257
|
+
}
|
|
258
|
+
return errorJson(err);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- helper for preserving sibling laws -------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
// Replace a law in file content while preserving other laws
|
|
265
|
+
function replaceLawInContent(content, lawId, newLawMarkdown) {
|
|
266
|
+
const lines = content.split('\n');
|
|
267
|
+
const startIdx = findLawStart(lines, lawId);
|
|
268
|
+
if (startIdx < 0) return content.trimEnd() + '\n\n' + newLawMarkdown;
|
|
269
|
+
|
|
270
|
+
const endIdx = findLawEnd(lines, startIdx);
|
|
271
|
+
const before = lines.slice(0, startIdx);
|
|
272
|
+
const after = lines.slice(endIdx);
|
|
273
|
+
|
|
274
|
+
// Trim trailing empty lines from before
|
|
275
|
+
const beforeEnd = before.findLastIndex(l => l !== '') + 1;
|
|
276
|
+
before.length = beforeEnd;
|
|
277
|
+
|
|
278
|
+
// Trim leading empty lines from after
|
|
279
|
+
const afterStart = after.findIndex(l => l !== '');
|
|
280
|
+
if (afterStart > 0) after.splice(0, afterStart);
|
|
281
|
+
|
|
282
|
+
// newLawMarkdown includes trailing newline; split and rejoin without final empty string
|
|
283
|
+
const newLines = newLawMarkdown.trimEnd().split('\n');
|
|
284
|
+
return before.concat(newLines, after).join('\n') + '\n';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- edit law executor -------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
async function executeEditLaw(args, context) {
|
|
290
|
+
const io = makeAsyncIO(context.worktree);
|
|
291
|
+
const execFile = makeExecFile(context.worktree);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const result = await findLawByID(io, 'foundry', args.id);
|
|
295
|
+
if (!result.found) {
|
|
296
|
+
return JSON.stringify({
|
|
297
|
+
ok: false,
|
|
298
|
+
errors: [`Law "${args.id}" not found`],
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const validation = await validateLaw({ body: args.body, io });
|
|
303
|
+
if (!validation.ok) {
|
|
304
|
+
return JSON.stringify(validation);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const fileContent = replaceLawInContent(result.fullMarkdown, args.id, args.body);
|
|
308
|
+
await io.writeFile(result.path, fileContent);
|
|
309
|
+
execFile(['add', result.path]);
|
|
310
|
+
execFile(['commit', '-m', `config: edit law ${args.id}\n\nvia foundry_config_edit_law`]);
|
|
311
|
+
|
|
312
|
+
return JSON.stringify({
|
|
313
|
+
ok: true,
|
|
314
|
+
id: args.id,
|
|
315
|
+
path: result.path.replace(/^foundry\//, 'foundry/'),
|
|
316
|
+
source: result.source,
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (err instanceof UnexpectedFilesError) {
|
|
320
|
+
return JSON.stringify({ error: err.message, affected_files: err.files });
|
|
321
|
+
}
|
|
322
|
+
return errorJson(err);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- tool factories -------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
function makeReadLawTool(tool) {
|
|
329
|
+
return tool({
|
|
330
|
+
description: 'Read a law by ID, returning the full markdown including validators block.',
|
|
331
|
+
args: {
|
|
332
|
+
id: tool.schema.string().describe('Law ID to read'),
|
|
333
|
+
},
|
|
334
|
+
execute: guarded('foundry_config_read_law', READ_GUARDS, executeReadLaw, {
|
|
335
|
+
branchIo: branchIoFactory,
|
|
336
|
+
io: asyncIoFactory,
|
|
337
|
+
}),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function makeAddLawTool(tool) {
|
|
342
|
+
return tool({
|
|
343
|
+
description: 'Add a new law (config-tier; requires a config/* branch). Target must be {kind:"global", file:"<name>.md"} or {kind:"type-specific", typeId:"<id>"}.',
|
|
344
|
+
args: {
|
|
345
|
+
name: tool.schema.string(),
|
|
346
|
+
body: tool.schema.string(),
|
|
347
|
+
target: tool.schema.object({
|
|
348
|
+
kind: tool.schema.string(),
|
|
349
|
+
file: tool.schema.string().optional(),
|
|
350
|
+
typeId: tool.schema.string().optional(),
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
execute: guarded('foundry_config_add_law', CREATE_GUARDS, executeAddLaw, {
|
|
354
|
+
branchIo: branchIoFactory,
|
|
355
|
+
io: asyncIoFactory,
|
|
356
|
+
}),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function makeEditLawTool(tool) {
|
|
361
|
+
return tool({
|
|
362
|
+
description: 'Edit an existing law by ID. Validates the new body, updates the file, and commits on the current config/* branch.',
|
|
363
|
+
args: {
|
|
364
|
+
id: tool.schema.string().describe('Law ID to edit'),
|
|
365
|
+
body: tool.schema.string().describe('Full new markdown body for the law'),
|
|
366
|
+
},
|
|
367
|
+
execute: guarded('foundry_config_edit_law', EDIT_GUARDS, executeEditLaw, {
|
|
368
|
+
branchIo: branchIoFactory,
|
|
369
|
+
io: asyncIoFactory,
|
|
370
|
+
}),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function createConfigLawTools({ tool }) {
|
|
375
|
+
return {
|
|
376
|
+
foundry_config_read_law: makeReadLawTool(tool),
|
|
377
|
+
foundry_config_add_law: makeAddLawTool(tool),
|
|
378
|
+
foundry_config_edit_law: makeEditLawTool(tool),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getCycleDefinition, getArtefactType, getLaws, getAppraisers, getFlow } from '../../../scripts/lib/config.js';
|
|
2
|
+
import { makeIO } from './helpers.js';
|
|
3
|
+
|
|
4
|
+
function makeConfigTool(tool, description, argSchema, invoke) {
|
|
5
|
+
return tool({
|
|
6
|
+
description,
|
|
7
|
+
args: argSchema,
|
|
8
|
+
async execute(args, context) {
|
|
9
|
+
const io = makeIO(context.worktree);
|
|
10
|
+
return JSON.stringify(await invoke(args, io));
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createConfigTools({ tool }) {
|
|
16
|
+
return {
|
|
17
|
+
foundry_config_cycle: makeConfigTool(
|
|
18
|
+
tool, 'Get a cycle definition from foundry config',
|
|
19
|
+
{ cycleId: tool.schema.string().describe('Cycle ID') },
|
|
20
|
+
(args, io) => getCycleDefinition('foundry', args.cycleId, io),
|
|
21
|
+
),
|
|
22
|
+
foundry_config_artefact_type: makeConfigTool(
|
|
23
|
+
tool, 'Get an artefact type definition',
|
|
24
|
+
{ typeId: tool.schema.string().describe('Artefact type ID') },
|
|
25
|
+
(args, io) => getArtefactType('foundry', args.typeId, io),
|
|
26
|
+
),
|
|
27
|
+
foundry_config_laws: makeConfigTool(
|
|
28
|
+
tool, 'Get laws, optionally filtered by artefact type',
|
|
29
|
+
{ typeId: tool.schema.string().optional().describe('Artefact type ID') },
|
|
30
|
+
(args, io) => getLaws('foundry', io, { typeId: args.typeId }),
|
|
31
|
+
),
|
|
32
|
+
foundry_config_appraisers: makeConfigTool(
|
|
33
|
+
tool, 'List all appraisers',
|
|
34
|
+
{},
|
|
35
|
+
(_args, io) => getAppraisers('foundry', io),
|
|
36
|
+
),
|
|
37
|
+
foundry_config_flow: makeConfigTool(
|
|
38
|
+
tool, 'Get a flow definition',
|
|
39
|
+
{ flowId: tool.schema.string().describe('Flow ID') },
|
|
40
|
+
(args, io) => getFlow('foundry', args.flowId, io),
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { openFeedbackStore } from '../../../scripts/lib/feedback-store.js';
|
|
2
|
+
import { parseFrontmatter } from '../../../scripts/lib/workfile.js';
|
|
3
|
+
import { requireActiveStage, stageBaseOf } from '../../../scripts/lib/stage-guard.js';
|
|
4
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
5
|
+
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
6
|
+
|
|
7
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
8
|
+
|
|
9
|
+
function readCycle(io) {
|
|
10
|
+
if (!io.exists('WORK.md')) return null;
|
|
11
|
+
const fm = parseFrontmatter(io.readFile('WORK.md'));
|
|
12
|
+
return fm.cycle || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Shared guard preamble for stage-bound feedback tools.
|
|
16
|
+
// Returns {ok:true, activeStage, stageBase, cycle} or {ok:false, error}.
|
|
17
|
+
// Caller is responsible for any tool-specific stage-base / tag checks.
|
|
18
|
+
function preflight(io, toolName) {
|
|
19
|
+
const guard = requireActiveStage(io);
|
|
20
|
+
if (!guard.ok) return { ok: false, error: `${toolName} requires active stage; ${guard.error}` };
|
|
21
|
+
const activeStage = guard.active.stage;
|
|
22
|
+
const stageBase = stageBaseOf(activeStage);
|
|
23
|
+
const cycle = readCycle(io);
|
|
24
|
+
if (!cycle) return { ok: false, error: `${toolName}: WORK.md cycle not found` };
|
|
25
|
+
return { ok: true, activeStage, stageBase, cycle };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Stages that are forbidden from adding feedback entirely.
|
|
29
|
+
const FEEDBACK_ADD_FORBIDDEN = {
|
|
30
|
+
forge: 'foundry_feedback_add: forge stages do not add feedback',
|
|
31
|
+
assay: 'foundry_feedback_add: assay stages do not add feedback (extractor failure marks the workfile failed)',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Per-stage tag validators for stages that allow feedback with restrictions.
|
|
35
|
+
const FEEDBACK_ADD_TAG_VALIDATORS = {
|
|
36
|
+
quench: {
|
|
37
|
+
test: tag => tag.startsWith('law:'),
|
|
38
|
+
message: tag => `foundry_feedback_add: quench may only add tags starting with "law:"; got "${tag}"`,
|
|
39
|
+
},
|
|
40
|
+
appraise: {
|
|
41
|
+
test: tag => tag.startsWith('law:'),
|
|
42
|
+
message: tag => `foundry_feedback_add: appraise tag must start with "law:"; got "${tag}"`,
|
|
43
|
+
},
|
|
44
|
+
'human-appraise': {
|
|
45
|
+
test: tag => tag === 'human',
|
|
46
|
+
message: tag => `foundry_feedback_add: human-appraise may only add tag "human"; got "${tag}"`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Validate that the given tag is allowed for the current stage base.
|
|
51
|
+
// Returns {ok: true} or {ok: false, error}.
|
|
52
|
+
function validateFeedbackAddTag(stageBase, tag) {
|
|
53
|
+
const forbiddenMsg = FEEDBACK_ADD_FORBIDDEN[stageBase];
|
|
54
|
+
if (forbiddenMsg) return { ok: false, error: forbiddenMsg };
|
|
55
|
+
const rule = FEEDBACK_ADD_TAG_VALIDATORS[stageBase];
|
|
56
|
+
if (!rule) return { ok: true };
|
|
57
|
+
if (rule.test(tag)) return { ok: true };
|
|
58
|
+
return { ok: false, error: rule.message(tag) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function executeFeedbackAdd(args, context) {
|
|
62
|
+
const io = makeIO(context.worktree);
|
|
63
|
+
const pre = preflight(io, 'foundry_feedback_add');
|
|
64
|
+
if (!pre.ok) return JSON.stringify({ error: pre.error });
|
|
65
|
+
const { activeStage, stageBase, cycle } = pre;
|
|
66
|
+
|
|
67
|
+
const tagCheck = validateFeedbackAddTag(stageBase, args.tag);
|
|
68
|
+
if (!tagCheck.ok) return JSON.stringify({ error: tagCheck.error });
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
72
|
+
const { id, deduped } = store.add({
|
|
73
|
+
file: args.file,
|
|
74
|
+
tag: args.tag,
|
|
75
|
+
text: args.text,
|
|
76
|
+
source: activeStage,
|
|
77
|
+
cycle,
|
|
78
|
+
});
|
|
79
|
+
return JSON.stringify({ ok: true, id, deduped });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return JSON.stringify({ error: `foundry_feedback_add: ${err.message}` });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function executeFeedbackAction(args, context) {
|
|
86
|
+
const io = makeIO(context.worktree);
|
|
87
|
+
const pre = preflight(io, 'foundry_feedback_action');
|
|
88
|
+
if (!pre.ok) return JSON.stringify({ error: pre.error });
|
|
89
|
+
const { activeStage, stageBase, cycle } = pre;
|
|
90
|
+
if (stageBase !== 'forge') {
|
|
91
|
+
return JSON.stringify({ error: `foundry_feedback_action requires active forge stage; current: ${activeStage}` });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
96
|
+
const r = store.transition({
|
|
97
|
+
id: args.id,
|
|
98
|
+
target: 'actioned',
|
|
99
|
+
stage: activeStage,
|
|
100
|
+
cycle,
|
|
101
|
+
});
|
|
102
|
+
if (!r.ok) return JSON.stringify({ error: r.error });
|
|
103
|
+
return JSON.stringify({ ok: true });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return JSON.stringify({ error: `foundry_feedback_action: ${err.message}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function executeFeedbackWontfix(args, context) {
|
|
110
|
+
const io = makeIO(context.worktree);
|
|
111
|
+
const pre = preflight(io, 'foundry_feedback_wontfix');
|
|
112
|
+
if (!pre.ok) return JSON.stringify({ error: pre.error });
|
|
113
|
+
const { activeStage, stageBase, cycle } = pre;
|
|
114
|
+
if (stageBase !== 'forge') {
|
|
115
|
+
return JSON.stringify({ error: `foundry_feedback_wontfix requires active forge stage; current: ${activeStage}` });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
120
|
+
const r = store.transition({
|
|
121
|
+
id: args.id,
|
|
122
|
+
target: 'wont-fix',
|
|
123
|
+
stage: activeStage,
|
|
124
|
+
cycle,
|
|
125
|
+
reason: args.reason,
|
|
126
|
+
});
|
|
127
|
+
if (!r.ok) return JSON.stringify({ error: r.error });
|
|
128
|
+
return JSON.stringify({ ok: true });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return JSON.stringify({ error: `foundry_feedback_wontfix: ${err.message}` });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveTargetFromResolution(resolution) {
|
|
135
|
+
return resolution === 'approved' ? 'resolved' : 'rejected';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function executeFeedbackResolve(args, context) {
|
|
139
|
+
const io = makeIO(context.worktree);
|
|
140
|
+
const pre = preflight(io, 'foundry_feedback_resolve');
|
|
141
|
+
if (!pre.ok) return JSON.stringify({ error: pre.error });
|
|
142
|
+
const { activeStage, stageBase, cycle } = pre;
|
|
143
|
+
const allowedStages = ['quench', 'appraise', 'human-appraise'];
|
|
144
|
+
if (!allowedStages.includes(stageBase)) {
|
|
145
|
+
return JSON.stringify({ error: `foundry_feedback_resolve requires active quench|appraise|human-appraise stage; current: ${activeStage}` });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const target = resolveTargetFromResolution(args.resolution);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
152
|
+
const r = store.transition({
|
|
153
|
+
id: args.id,
|
|
154
|
+
target,
|
|
155
|
+
stage: activeStage,
|
|
156
|
+
cycle,
|
|
157
|
+
reason: args.reason,
|
|
158
|
+
});
|
|
159
|
+
if (!r.ok) return JSON.stringify({ error: r.error });
|
|
160
|
+
return JSON.stringify({ ok: true });
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return JSON.stringify({ error: `foundry_feedback_resolve: ${err.message}` });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function executeFeedbackList(args, context) {
|
|
167
|
+
const io = makeIO(context.worktree);
|
|
168
|
+
if (!io.exists('WORK.md')) {
|
|
169
|
+
return JSON.stringify({ error: 'foundry_feedback_list: WORK.md cycle not found' });
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const store = openFeedbackStore('WORK.feedback.yaml', io);
|
|
173
|
+
const items = store.list()
|
|
174
|
+
.filter(it => !args.file || it.file === args.file)
|
|
175
|
+
.map(it => {
|
|
176
|
+
const head = it.history[0];
|
|
177
|
+
const base = {
|
|
178
|
+
id: it.id,
|
|
179
|
+
file: it.file,
|
|
180
|
+
tag: it.tag,
|
|
181
|
+
text: it.text,
|
|
182
|
+
source: it.source,
|
|
183
|
+
state: head.state,
|
|
184
|
+
depth: it.history.length,
|
|
185
|
+
};
|
|
186
|
+
if (head.reason) base.reason = head.reason;
|
|
187
|
+
return base;
|
|
188
|
+
});
|
|
189
|
+
return JSON.stringify(items);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return JSON.stringify({ error: `foundry_feedback_list: ${err.message}` });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createFeedbackTools({ tool }) {
|
|
196
|
+
return {
|
|
197
|
+
foundry_feedback_add: tool({ description: 'Add a feedback item to WORK.feedback.yaml',
|
|
198
|
+
args: {
|
|
199
|
+
file: tool.schema.string().describe('Artefact file path'),
|
|
200
|
+
text: tool.schema.string().describe('Feedback text'),
|
|
201
|
+
tag: tool.schema.string().describe('Tag for the feedback item'),
|
|
202
|
+
},
|
|
203
|
+
execute: guarded('foundry_feedback_add', [flowBranchGuard, gateNotFailed], executeFeedbackAdd, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
204
|
+
}),
|
|
205
|
+
foundry_feedback_action: tool({ description: 'Mark a feedback item as actioned (forge stages only)',
|
|
206
|
+
args: {
|
|
207
|
+
id: tool.schema.string().describe('Feedback item id (ULID)'),
|
|
208
|
+
},
|
|
209
|
+
execute: guarded('foundry_feedback_action', [flowBranchGuard, gateNotFailed], executeFeedbackAction, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
210
|
+
}),
|
|
211
|
+
foundry_feedback_wontfix: tool({ description: 'Mark a feedback item as wont-fix with reason (forge stages only)',
|
|
212
|
+
args: {
|
|
213
|
+
id: tool.schema.string().describe('Feedback item id (ULID)'),
|
|
214
|
+
reason: tool.schema.string().describe('Reason for wont-fix'),
|
|
215
|
+
},
|
|
216
|
+
execute: guarded('foundry_feedback_wontfix', [flowBranchGuard, gateNotFailed], executeFeedbackWontfix, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
217
|
+
}),
|
|
218
|
+
foundry_feedback_resolve: tool({
|
|
219
|
+
description: 'Resolve a feedback item (approved or rejected). In human-appraise stages, this tool can override deadlocked items by providing a reason.',
|
|
220
|
+
args: {
|
|
221
|
+
id: tool.schema.string().describe('Feedback item id (ULID)'),
|
|
222
|
+
resolution: tool.schema.enum(['approved', 'rejected']).describe('Resolution type'),
|
|
223
|
+
reason: tool.schema.string().optional().describe('Reason (required if rejected, or for deadlock override)'),
|
|
224
|
+
},
|
|
225
|
+
execute: guarded('foundry_feedback_resolve', [flowBranchGuard, gateNotFailed], executeFeedbackResolve, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
226
|
+
}),
|
|
227
|
+
foundry_feedback_list: tool({ description: 'List feedback items, optionally filtered by file',
|
|
228
|
+
args: {
|
|
229
|
+
file: tool.schema.string().optional().describe('Filter by artefact file path'),
|
|
230
|
+
},
|
|
231
|
+
execute: executeFeedbackList,
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
234
|
+
}
|