@really-knows-ai/foundry 2.3.1 → 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 +200 -198
- 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 +42 -6
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +49 -21
- package/{skills → dist/skills}/add-cycle/SKILL.md +60 -14
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +39 -7
- 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 -90
- package/docs/concepts.md +0 -59
- package/docs/getting-started.md +0 -78
- package/docs/work-spec.md +0 -193
- 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 -105
- 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,249 @@
|
|
|
1
|
+
// scripts/lib/feedback-store.js
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
import { ulid } from './ulid.js';
|
|
4
|
+
import { validateTransition, hashText, canForgeWontFix } from './feedback-transitions.js';
|
|
5
|
+
|
|
6
|
+
const YAML_OPTS = { lineWidth: -1 };
|
|
7
|
+
|
|
8
|
+
const VALID_SOURCE_BASES = new Set(['forge', 'quench', 'appraise', 'human-appraise']);
|
|
9
|
+
|
|
10
|
+
function validateSourceBase(base) {
|
|
11
|
+
if (!VALID_SOURCE_BASES.has(base)) {
|
|
12
|
+
throw new Error(`unknown source base: ${base} (expected one of: forge, quench, appraise, human-appraise)`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateSourceFormat(source) {
|
|
17
|
+
if (typeof source !== 'string' || !source.includes(':')) {
|
|
18
|
+
throw new Error(`source must be in 'base:alias' form; got ${JSON.stringify(source)}`);
|
|
19
|
+
}
|
|
20
|
+
const [base, ...aliasParts] = source.split(':');
|
|
21
|
+
const alias = aliasParts.join(':');
|
|
22
|
+
if (!base || !alias) {
|
|
23
|
+
throw new Error(`source must be in 'base:alias' form; got ${JSON.stringify(source)}`);
|
|
24
|
+
}
|
|
25
|
+
validateSourceBase(base);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseYamlDoc(raw) {
|
|
29
|
+
const doc = yaml.load(raw);
|
|
30
|
+
if (doc === null) return null;
|
|
31
|
+
if (!Array.isArray(doc.items)) {
|
|
32
|
+
throw new Error(`WORK.feedback.yaml malformed: top-level must be an object with an 'items' array`);
|
|
33
|
+
}
|
|
34
|
+
return doc.items;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadItems(path, io) {
|
|
38
|
+
if (!io.exists(path)) return [];
|
|
39
|
+
const raw = io.readFile(path);
|
|
40
|
+
if (!raw || !raw.trim()) return [];
|
|
41
|
+
return parseYamlDoc(raw) || [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveItems(path, items, io) {
|
|
45
|
+
const body = yaml.dump({ items }, YAML_OPTS);
|
|
46
|
+
const tmp = `${path}.tmp`;
|
|
47
|
+
io.writeFile(tmp, body);
|
|
48
|
+
io.rename(tmp, path);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nowIso() {
|
|
52
|
+
return new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function currentState(item) {
|
|
56
|
+
return item.history[0].state;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cloneItem(it) {
|
|
60
|
+
return { ...it, history: it.history.map(h => ({ ...h })) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function openFeedbackStore(path, io) {
|
|
64
|
+
let items = loadItems(path, io);
|
|
65
|
+
|
|
66
|
+
function persist(nextItems) {
|
|
67
|
+
saveItems(path, nextItems, io);
|
|
68
|
+
items = nextItems;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
list() { return items.map(cloneItem); },
|
|
73
|
+
get(id) {
|
|
74
|
+
const it = items.find(x => x.id === id);
|
|
75
|
+
return it ? cloneItem(it) : null;
|
|
76
|
+
},
|
|
77
|
+
add(params) {
|
|
78
|
+
return storeAdd(params, items, {
|
|
79
|
+
hashFn: hashText, stateOf: currentState,
|
|
80
|
+
validateSrc: validateSourceFormat, persist,
|
|
81
|
+
makeUlid: ulid, timestamp: nowIso,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
transition(params) {
|
|
85
|
+
return storeTransition(params, items, {
|
|
86
|
+
validateTransitionFn: validateTransition,
|
|
87
|
+
canForgeWontFixFn: canForgeWontFix,
|
|
88
|
+
timestamp: nowIso, persist,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
writeDeadlockedSnapshotForTest(params) {
|
|
92
|
+
return storeWriteDeadlockedSnapshot(params, items, {
|
|
93
|
+
timestamp: nowIso, persist,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
writeDeadlockedSnapshots(ids, reason, stage, cycle) {
|
|
97
|
+
return storeWriteDeadlockedSnapshots({ ids, reason, stage, cycle }, items, {
|
|
98
|
+
timestamp: nowIso, persist,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isDuplicate(item, file, tag, textHash, stateOf) {
|
|
105
|
+
return item.file === file &&
|
|
106
|
+
item.tag === tag &&
|
|
107
|
+
hashText(item.text) === textHash &&
|
|
108
|
+
stateOf(item) !== 'resolved';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertAddParams(...params) {
|
|
112
|
+
for (const p of params) {
|
|
113
|
+
if (!p) throw new Error('add requires file, tag, text, source, cycle');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function storeAdd(params, items, deps) {
|
|
118
|
+
const { file, tag, text, source, cycle } = params;
|
|
119
|
+
const { hashFn, stateOf, validateSrc, persist, makeUlid, timestamp } = deps;
|
|
120
|
+
assertAddParams(file, tag, text, source, cycle);
|
|
121
|
+
validateSrc(source);
|
|
122
|
+
|
|
123
|
+
const textHash = hashFn(text);
|
|
124
|
+
const existing = items.find(it => isDuplicate(it, file, tag, textHash, stateOf));
|
|
125
|
+
if (existing) return { id: existing.id, deduped: true };
|
|
126
|
+
|
|
127
|
+
const id = makeUlid();
|
|
128
|
+
const item = {
|
|
129
|
+
id, file, tag, text, source,
|
|
130
|
+
history: [{ state: 'open', stage: source, cycle, timestamp: timestamp() }],
|
|
131
|
+
};
|
|
132
|
+
persist([...items, item]);
|
|
133
|
+
return { id, deduped: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function forgeWontFixAllowed(item, stageBase, target, canForgeWontFixFn) {
|
|
137
|
+
if (stageBase !== 'forge') return true;
|
|
138
|
+
if (target !== 'wont-fix') return true;
|
|
139
|
+
return canForgeWontFixFn(item, stageBase);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function forgeWontFixError(item) {
|
|
143
|
+
return `forge may only mark wont-fix on feedback whose source is ` +
|
|
144
|
+
`appraise; this item's source is ${item.source}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function reasonRequiredForTransition(target, current, reason) {
|
|
148
|
+
const REASON_REQUIRED_TARGETS = new Set(['rejected', 'wont-fix']);
|
|
149
|
+
return (REASON_REQUIRED_TARGETS.has(target) || current === 'deadlocked')
|
|
150
|
+
&& (!reason || !reason.trim());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sourceMatchesStage(stageBase, stage, itemSource) {
|
|
154
|
+
return stageBase === 'human-appraise' || stage === itemSource;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validateTransitionInput(item, ctx, fns) {
|
|
158
|
+
if (!forgeWontFixAllowed(item, ctx.stageBase, ctx.target, fns.canForgeWontFixFn)) {
|
|
159
|
+
return forgeWontFixError(item);
|
|
160
|
+
}
|
|
161
|
+
const current = item.history[0].state;
|
|
162
|
+
const check = fns.validateTransitionFn({
|
|
163
|
+
currentState: current, target: ctx.target, stageBase: ctx.stageBase,
|
|
164
|
+
sourceMatches: sourceMatchesStage(ctx.stageBase, ctx.stage, item.source),
|
|
165
|
+
});
|
|
166
|
+
if (!check.ok) return check.reason;
|
|
167
|
+
if (reasonRequiredForTransition(ctx.target, current, ctx.reason)) {
|
|
168
|
+
return `reason is required for transition → ${ctx.target}`;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasNonEmptyReason(reason) {
|
|
174
|
+
return Boolean(reason && reason.trim());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function applyTransition(items, id, snapshot) {
|
|
178
|
+
return items.map(it =>
|
|
179
|
+
it.id === id ? { ...it, history: [snapshot, ...it.history] } : it
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function storeTransition(params, items, deps) {
|
|
184
|
+
const { id, target, stage, cycle, reason } = params;
|
|
185
|
+
const { validateTransitionFn, canForgeWontFixFn, timestamp, persist } = deps;
|
|
186
|
+
|
|
187
|
+
const item = items.find(x => x.id === id);
|
|
188
|
+
if (!item) return { ok: false, error: `feedback item not found: ${id}` };
|
|
189
|
+
|
|
190
|
+
const stageBase = stage.split(':')[0];
|
|
191
|
+
const error = validateTransitionInput(item,
|
|
192
|
+
{ stageBase, stage, target, reason },
|
|
193
|
+
{ canForgeWontFixFn, validateTransitionFn },
|
|
194
|
+
);
|
|
195
|
+
if (error) return { ok: false, error };
|
|
196
|
+
|
|
197
|
+
const snapshot = { state: target, stage, cycle, timestamp: timestamp() };
|
|
198
|
+
if (hasNonEmptyReason(reason)) snapshot.reason = reason;
|
|
199
|
+
|
|
200
|
+
persist(applyTransition(items, id, snapshot));
|
|
201
|
+
return { ok: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function storeWriteDeadlockedSnapshot(params, items, deps) {
|
|
205
|
+
const { id, cycle, reason } = params;
|
|
206
|
+
const { timestamp, persist } = deps;
|
|
207
|
+
|
|
208
|
+
const item = items.find(x => x.id === id);
|
|
209
|
+
if (!item) return { ok: false, error: `feedback item not found: ${id}` };
|
|
210
|
+
if (!reason || !reason.trim()) {
|
|
211
|
+
return { ok: false, error: 'reason is required for deadlocked snapshot' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const snapshot = { state: 'deadlocked', stage: 'sort', cycle, timestamp: timestamp(), reason };
|
|
215
|
+
persist(items.map(it =>
|
|
216
|
+
it.id === id ? { ...it, history: [snapshot, ...it.history] } : it
|
|
217
|
+
));
|
|
218
|
+
return { ok: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function assertDeadlockedSnapshotArgs(ids, reason) {
|
|
222
|
+
if (!Array.isArray(ids)) return { ok: false, error: 'ids must be an array' };
|
|
223
|
+
if (ids.length === 0) return { ok: true };
|
|
224
|
+
if (!reason) return { ok: false, error: 'reason is required for deadlocked snapshot' };
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function storeWriteDeadlockedSnapshots(params, items, deps) {
|
|
229
|
+
const { ids, reason, stage, cycle } = params;
|
|
230
|
+
const { timestamp, persist } = deps;
|
|
231
|
+
|
|
232
|
+
const precheck = assertDeadlockedSnapshotArgs(ids, reason);
|
|
233
|
+
if (precheck) return precheck;
|
|
234
|
+
|
|
235
|
+
const ts = timestamp();
|
|
236
|
+
const idSet = new Set(ids);
|
|
237
|
+
const nextItems = items.map(it => {
|
|
238
|
+
if (!idSet.has(it.id)) return it;
|
|
239
|
+
return { ...it, history: [{ state: 'deadlocked', stage, cycle, timestamp: ts, reason }, ...it.history] };
|
|
240
|
+
});
|
|
241
|
+
for (const id of ids) {
|
|
242
|
+
if (!items.some(it => it.id === id)) {
|
|
243
|
+
return { ok: false, error: `feedback item(s) not found: ${id}` };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
persist(nextItems);
|
|
248
|
+
return { ok: true };
|
|
249
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// scripts/lib/feedback-transitions.js
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// State machine per spec §5.
|
|
5
|
+
//
|
|
6
|
+
// States: open, actioned, wont-fix, rejected, deadlocked, resolved (terminal).
|
|
7
|
+
//
|
|
8
|
+
// Transition rules (spec §5.1):
|
|
9
|
+
// 1. Forge operates on {open, rejected} → {actioned, wont-fix}.
|
|
10
|
+
// 2. Source-stage (quench/appraise/human-appraise) operates on {actioned, wont-fix}
|
|
11
|
+
// → {resolved, rejected}, when caller's stageId === item.source.
|
|
12
|
+
// 3. Sort writes 'deadlocked' via its own state machine logic (spec §6.1).
|
|
13
|
+
// Forge, quench, appraise, and human-appraise use this validator.
|
|
14
|
+
// 4. Human-appraise override: on a deadlocked item, transitions to
|
|
15
|
+
// {resolved, rejected} are legal regardless of source match. The
|
|
16
|
+
// override authority mirrors the source-stage targets — wont-fix is
|
|
17
|
+
// a forge declaration ("considered, choosing to defer") and stays
|
|
18
|
+
// outside reviewer verdicts, excluded here per spec.
|
|
19
|
+
// 5. 'resolved' is terminal.
|
|
20
|
+
//
|
|
21
|
+
// validateTransition takes an options object so new dimensions (sourceMatches,
|
|
22
|
+
// potential future flags) don't break the call shape.
|
|
23
|
+
|
|
24
|
+
const FORGE_TARGETS = new Set(['actioned', 'wont-fix']);
|
|
25
|
+
const SOURCE_TARGETS = new Set(['resolved', 'rejected']);
|
|
26
|
+
const HUMAN_OVERRIDE_TARGETS = new Set(['resolved', 'rejected']);
|
|
27
|
+
const KNOWN_STATES = new Set(['open', 'actioned', 'wont-fix', 'rejected', 'deadlocked', 'resolved']);
|
|
28
|
+
const SOURCE_STAGES = new Set(['quench', 'appraise', 'human-appraise']);
|
|
29
|
+
|
|
30
|
+
function validateDeadlocked(target, stageBase) {
|
|
31
|
+
if (stageBase !== 'human-appraise') {
|
|
32
|
+
return { ok: false, reason: `only human-appraise may resolve a deadlocked item; got ${stageBase}` };
|
|
33
|
+
}
|
|
34
|
+
if (!HUMAN_OVERRIDE_TARGETS.has(target)) {
|
|
35
|
+
return { ok: false, reason: `invalid deadlock-override transition → ${target}` };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateForge(currentState, target) {
|
|
41
|
+
if (currentState !== 'open' && currentState !== 'rejected') {
|
|
42
|
+
return { ok: false, reason: `forge cannot transition from ${currentState}` };
|
|
43
|
+
}
|
|
44
|
+
if (!FORGE_TARGETS.has(target)) {
|
|
45
|
+
return { ok: false, reason: `forge cannot produce ${target}` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateSourceStage(currentState, target, stageBase, sourceMatches) {
|
|
51
|
+
if (currentState !== 'actioned' && currentState !== 'wont-fix') {
|
|
52
|
+
return { ok: false, reason: `${stageBase} cannot transition from ${currentState}` };
|
|
53
|
+
}
|
|
54
|
+
if (!SOURCE_TARGETS.has(target)) {
|
|
55
|
+
return { ok: false, reason: `${stageBase} cannot produce ${target}` };
|
|
56
|
+
}
|
|
57
|
+
if (!sourceMatches) {
|
|
58
|
+
return { ok: false, reason: `only the source stage may resolve/reject this item` };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkPreconditions(currentState, sourceMatches) {
|
|
64
|
+
if (typeof sourceMatches !== 'boolean') {
|
|
65
|
+
throw new TypeError(
|
|
66
|
+
`validateTransition: sourceMatches must be a boolean; got ${typeof sourceMatches}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!KNOWN_STATES.has(currentState)) return { ok: false, reason: `unknown state: ${currentState}` };
|
|
70
|
+
if (currentState === 'resolved') return { ok: false, reason: 'resolved is terminal' };
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function validateTransition({ currentState, target, stageBase, sourceMatches }) {
|
|
75
|
+
const pre = checkPreconditions(currentState, sourceMatches);
|
|
76
|
+
if (pre) return pre;
|
|
77
|
+
if (currentState === 'deadlocked') return validateDeadlocked(target, stageBase);
|
|
78
|
+
if (stageBase === 'forge') return validateForge(currentState, target);
|
|
79
|
+
if (SOURCE_STAGES.has(stageBase)) return validateSourceStage(currentState, target, stageBase, sourceMatches);
|
|
80
|
+
return { ok: false, reason: `unsupported stage base: ${stageBase}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hashText(text) {
|
|
84
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per spec §5.1 rule 7 (REVISION-CONTRACT §A2): forge may produce the
|
|
89
|
+
* `wont-fix` target when the source stage base is `appraise`.
|
|
90
|
+
* For `quench`- or `human-appraise`-sourced items, forge's legal
|
|
91
|
+
* target from {open, rejected} is `actioned`.
|
|
92
|
+
*
|
|
93
|
+
* Forge-specific helper. Forge callers check this before attempting wont-fix
|
|
94
|
+
* transitions. Other stages use validateTransition directly.
|
|
95
|
+
*
|
|
96
|
+
* @param {{source: string}} item — feedback item; `source` is `base:alias`.
|
|
97
|
+
* @param {string} callerStageBase — the caller's stage base (e.g. 'forge').
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
export function canForgeWontFix(item, callerStageBase) {
|
|
101
|
+
if (callerStageBase !== 'forge') return false;
|
|
102
|
+
if (!item || typeof item.source !== 'string' || !item.source) return false;
|
|
103
|
+
const sourceBase = item.source.split(':')[0];
|
|
104
|
+
return sourceBase === 'appraise';
|
|
105
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// scripts/lib/finalize.js
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import { sortPaths } from './attestation/hash.js';
|
|
4
|
+
import { isToolManaged } from './git-policy.js';
|
|
5
|
+
|
|
6
|
+
// Accepts short (>=7) and full (<=64) hex SHAs. Rejects symbolic refs (HEAD),
|
|
7
|
+
// argument-injection (--upload-pack=...), and shell metacharacters. The
|
|
8
|
+
// practical attack surface is .foundry/active-stage.json and last-stage.json,
|
|
9
|
+
// which are persisted on disk and may be edited or corrupted; we validate
|
|
10
|
+
// here for defence-in-depth before passing the value to git.
|
|
11
|
+
const SHA_RE = /^[0-9a-f]{7,64}$/i;
|
|
12
|
+
|
|
13
|
+
function assertValidSha(baseSha) {
|
|
14
|
+
if (typeof baseSha !== 'string' || !SHA_RE.test(baseSha)) {
|
|
15
|
+
throw new Error(`invalid baseSha: ${JSON.stringify(baseSha)}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function git(exec, args) {
|
|
20
|
+
return exec(['git', ...args]).toString().split('\n').filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function changedFiles(exec, baseSha) {
|
|
24
|
+
const tracked = git(exec, ['diff', '--name-only', '--no-renames', baseSha, 'HEAD']);
|
|
25
|
+
const diffUnstaged = git(exec, ['diff', '--name-only', '--no-renames']);
|
|
26
|
+
const diffStaged = git(exec, ['diff', '--cached', '--name-only', '--no-renames']);
|
|
27
|
+
const untracked = git(exec, ['ls-files', '--others', '--exclude-standard']);
|
|
28
|
+
return [...new Set([...tracked, ...diffUnstaged, ...diffStaged, ...untracked])];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getAllowedPatterns(stageBase, cycleDef, artefactTypes) {
|
|
32
|
+
if (stageBase === 'forge') {
|
|
33
|
+
return artefactTypes[cycleDef.outputArtefactType]?.filePatterns ?? [];
|
|
34
|
+
}
|
|
35
|
+
if (stageBase === 'assay') {
|
|
36
|
+
return ['foundry-memory/**'];
|
|
37
|
+
}
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function classifyFiles(files, allowedPatterns) {
|
|
42
|
+
const unexpected = [];
|
|
43
|
+
const matched = [];
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const hit = allowedPatterns.find(p => minimatch(f, p));
|
|
46
|
+
if (hit) matched.push(f);
|
|
47
|
+
else unexpected.push(f);
|
|
48
|
+
}
|
|
49
|
+
return { matched, unexpected };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, registerArtefact, io }) {
|
|
53
|
+
if (!io?.exec) {
|
|
54
|
+
throw new Error('finalizeStage: io.exec is required');
|
|
55
|
+
}
|
|
56
|
+
assertValidSha(baseSha);
|
|
57
|
+
const files = changedFiles(io.exec, baseSha).filter(f => !isToolManaged(f));
|
|
58
|
+
const allowedPatterns = getAllowedPatterns(stageBase, cycleDef, artefactTypes);
|
|
59
|
+
const { matched, unexpected } = classifyFiles(files, allowedPatterns);
|
|
60
|
+
if (unexpected.length) return { ok: false, error: 'unexpected_files', files: unexpected };
|
|
61
|
+
const sortedFiles = sortPaths(matched);
|
|
62
|
+
// For non-forge stages, matched files are tool-managed side effects
|
|
63
|
+
// (e.g. assay's memory writes) that should not become artefacts.
|
|
64
|
+
if (stageBase !== 'forge') return { ok: true, artefacts: [], changedFiles: sortedFiles };
|
|
65
|
+
const artefacts = sortedFiles.map(file => {
|
|
66
|
+
registerArtefact({ file, type: cycleDef.outputArtefactType, status: 'draft' });
|
|
67
|
+
return { file, type: cycleDef.outputArtefactType, status: 'draft' };
|
|
68
|
+
});
|
|
69
|
+
return { ok: true, artefacts, changedFiles: sortedFiles };
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function requireGitRepo(io) {
|
|
2
|
+
if (io.exists('.git')) return { ok: true };
|
|
3
|
+
return { ok: false, error: 'not a git repository (no .git directory at worktree root)' };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function requireFoundryRoot(io) {
|
|
7
|
+
// Probe the bare directory name — matches requireGitRepo's '.git'.
|
|
8
|
+
if (io.exists('foundry')) return { ok: true };
|
|
9
|
+
return {
|
|
10
|
+
ok: false,
|
|
11
|
+
error: 'foundry/ directory not found at worktree root. Run the init-foundry skill to scaffold it.',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// scripts/lib/git-bridge.js
|
|
2
|
+
//
|
|
3
|
+
// Policy-enforcing git commit helper used by the orchestrator.
|
|
4
|
+
//
|
|
5
|
+
// This helper stages exactly the paths allowed for the current phase and
|
|
6
|
+
// creates the commit. It:
|
|
7
|
+
//
|
|
8
|
+
// 1. Reads the worktree status (NUL-terminated) so paths with spaces /
|
|
9
|
+
// renames are handled safely.
|
|
10
|
+
// 2. Partitions dirty files against the phase's allowed patterns and
|
|
11
|
+
// Foundry's tool-managed list (WORK.md / WORK.history.yaml /
|
|
12
|
+
// WORK.feedback.yaml / .foundry/**).
|
|
13
|
+
// 3. If anything unexpected is dirty, throws a structured error with the
|
|
14
|
+
// offending file list — the orchestrator turns this into a `violation`
|
|
15
|
+
// action with `affected_files`. Nothing is staged or committed.
|
|
16
|
+
// 4. Stages allowed paths explicitly via argv and creates the commit.
|
|
17
|
+
//
|
|
18
|
+
// `execFile` is injected so tests can drive the helper.
|
|
19
|
+
//
|
|
20
|
+
// Returns the short commit SHA on success.
|
|
21
|
+
|
|
22
|
+
import { partitionDirty, parsePorcelainZ } from './git-policy.js';
|
|
23
|
+
|
|
24
|
+
class UnexpectedFilesError extends Error {
|
|
25
|
+
constructor(files) {
|
|
26
|
+
super(`unexpected_files: ${files.length} file(s)`);
|
|
27
|
+
this.code = 'unexpected_files';
|
|
28
|
+
this.files = files;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { UnexpectedFilesError };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} opts
|
|
36
|
+
* @param {string} opts.message Commit message.
|
|
37
|
+
* @param {string[]} [opts.allowedPatterns] Globs allowed to be dirty for this phase.
|
|
38
|
+
* @param {(args: string[]) => string} opts.execFile
|
|
39
|
+
* Synchronous git runner: receives argv (no `git`), returns stdout as utf-8.
|
|
40
|
+
* @returns {string|null} Short SHA of the new commit, or null if the worktree
|
|
41
|
+
* is clean (nothing to commit).
|
|
42
|
+
* @throws {UnexpectedFilesError} if the worktree contains any file outside
|
|
43
|
+
* the tool-managed list and the allow list. Nothing is staged in this case.
|
|
44
|
+
*/
|
|
45
|
+
export function commitWithPolicy({
|
|
46
|
+
message,
|
|
47
|
+
allowedPatterns = [],
|
|
48
|
+
execFile,
|
|
49
|
+
}) {
|
|
50
|
+
const porcelain = execFile(['status', '-z', '--porcelain', '--untracked-files=all']);
|
|
51
|
+
const dirty = parsePorcelainZ(porcelain);
|
|
52
|
+
const { allowed, unexpected } = partitionDirty(dirty, allowedPatterns);
|
|
53
|
+
if (unexpected.length) throw new UnexpectedFilesError(unexpected);
|
|
54
|
+
|
|
55
|
+
// Reset the index so a previous `git add` of an unexpected file (e.g. left
|
|
56
|
+
// over from a failed run) cannot leak into our commit. Then add the
|
|
57
|
+
// allowed paths explicitly via argv.
|
|
58
|
+
execFile(['reset', '--quiet']);
|
|
59
|
+
if (allowed.length === 0) {
|
|
60
|
+
// Nothing to commit; the worktree is clean of any change we'd be
|
|
61
|
+
// expected to capture. Return null so the caller knows no SHA was made.
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Wrap add + commit in try/catch to maintain atomicity guarantee.
|
|
66
|
+
// If commit fails (pre-commit hook reject, gpg error, empty commit after
|
|
67
|
+
// gitignore filtering), we must reset the index to restore "Nothing is
|
|
68
|
+
// staged or committed" invariant.
|
|
69
|
+
try {
|
|
70
|
+
execFile(['add', '--', ...allowed]);
|
|
71
|
+
execFile(['commit', '-m', message]);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
execFile(['reset', '--quiet']);
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
return execFile(['rev-parse', '--short', 'HEAD']).trim();
|
|
77
|
+
}
|