@really-knows-ai/foundry 2.3.2 → 3.0.1
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 +533 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +433 -0
- package/dist/docs/concepts.md +395 -0
- package/dist/docs/getting-started.md +344 -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 +328 -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 +62 -40
- 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 +191 -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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a law block contains deprecated Passing:/Failing: scaffolding.
|
|
3
|
+
* @param {string[]} lines
|
|
4
|
+
* @returns {string[]} Array of error messages (empty if valid)
|
|
5
|
+
*/
|
|
6
|
+
function checkForDeprecatedScaffolding(lines) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
for (let i = 0; i < lines.length; i++) {
|
|
9
|
+
const line = lines[i].trim();
|
|
10
|
+
if (line.startsWith('Passing:') || line.startsWith('Failing:')) {
|
|
11
|
+
errors.push(`Line ${i + 1}: Deprecated scaffolding (${line.split(':')[0]}:) not allowed. Prose must be plain statements without structured fields. See spec: "No structured fields within the prose — no Passing: or Failing: scaffolding."`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check for duplicate law IDs within blocks.
|
|
19
|
+
* @param {{id: string}[]} blocks
|
|
20
|
+
* @returns {string[]} Array of error messages (empty if valid)
|
|
21
|
+
*/
|
|
22
|
+
function checkForDuplicateIds(blocks) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const block of blocks) {
|
|
26
|
+
if (seen.has(block.id)) {
|
|
27
|
+
errors.push(`duplicate law id: ${block.id}`);
|
|
28
|
+
}
|
|
29
|
+
seen.add(block.id);
|
|
30
|
+
}
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate a law definition body.
|
|
36
|
+
*
|
|
37
|
+
* Laws derive their ID from the filename rather than frontmatter, so name
|
|
38
|
+
* and io are unused here but accepted for API parity with other validators.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {string} opts.name Slugged identifier (unused; laws use filename as ID).
|
|
42
|
+
* @param {string} opts.body Full markdown body.
|
|
43
|
+
* @param {object} [opts.io] IO adapter (unused; laws validate body only).
|
|
44
|
+
* @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
|
|
45
|
+
*/
|
|
46
|
+
export async function validate({ body }) {
|
|
47
|
+
const lines = body.split('\n');
|
|
48
|
+
let errors = checkForDeprecatedScaffolding(lines);
|
|
49
|
+
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
return { ok: false, errors };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const blocks = parseLawBlocks(body);
|
|
55
|
+
|
|
56
|
+
if (blocks.length === 0) {
|
|
57
|
+
return { ok: false, errors: ['body must contain at least one law block (## <law-id>)'] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
errors = checkForDuplicateIds(blocks);
|
|
61
|
+
return errors.length ? { ok: false, errors } : { ok: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse law blocks from the body.
|
|
66
|
+
* Each block is a ## heading followed by its content until the next heading.
|
|
67
|
+
* @param {string} body
|
|
68
|
+
* @returns {{id: string, text: string}[]}
|
|
69
|
+
*/
|
|
70
|
+
function parseLawBlocks(body) {
|
|
71
|
+
const blocks = [];
|
|
72
|
+
const lines = body.split('\n');
|
|
73
|
+
let currentId = null;
|
|
74
|
+
let currentLines = [];
|
|
75
|
+
|
|
76
|
+
const flushBlock = () => {
|
|
77
|
+
if (currentId) {
|
|
78
|
+
blocks.push({ id: currentId, text: currentLines.join('\n') });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const heading = line.match(/^## (.+)/);
|
|
84
|
+
if (!heading && currentId) {
|
|
85
|
+
currentLines.push(line);
|
|
86
|
+
}
|
|
87
|
+
if (heading) {
|
|
88
|
+
flushBlock();
|
|
89
|
+
currentId = heading[1].trim();
|
|
90
|
+
currentLines = [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
flushBlock();
|
|
94
|
+
|
|
95
|
+
return blocks;
|
|
96
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured reads of foundry/ directory contents.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { parseFrontmatter } from './workfile.js';
|
|
7
|
+
|
|
8
|
+
function parseDoc(text) {
|
|
9
|
+
const frontmatter = parseFrontmatter(text);
|
|
10
|
+
const body = text.replace(/^---\n.+?\n---\n?/s, '').trim();
|
|
11
|
+
return { frontmatter, body };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function getCycleDefinition(foundryDir, cycleId, io) {
|
|
15
|
+
const path = join(foundryDir, 'cycles', `${cycleId}.md`);
|
|
16
|
+
if (!(await io.exists(path))) {
|
|
17
|
+
throw new Error(`Cycle not found: ${cycleId}`);
|
|
18
|
+
}
|
|
19
|
+
const text = await io.readFile(path);
|
|
20
|
+
return parseDoc(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getArtefactType(foundryDir, typeId, io) {
|
|
24
|
+
const path = join(foundryDir, 'artefacts', typeId, 'definition.md');
|
|
25
|
+
if (!(await io.exists(path))) {
|
|
26
|
+
throw new Error(`Artefact type not found: ${typeId}`);
|
|
27
|
+
}
|
|
28
|
+
const text = await io.readFile(path);
|
|
29
|
+
return parseDoc(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse law entries from a markdown file. Each `## heading` starts a new law.
|
|
34
|
+
* Each law may have an optional `validators:` block with entries containing id, command, and optional failure-means.
|
|
35
|
+
* Validators are extracted and returned separately from prose.
|
|
36
|
+
* Returns: [{id, text (prose only), validators (if present)}]
|
|
37
|
+
*/
|
|
38
|
+
function parseLaws(text, source) {
|
|
39
|
+
const laws = [];
|
|
40
|
+
const lines = text.split('\n');
|
|
41
|
+
let currentId = null;
|
|
42
|
+
const currentLines = [];
|
|
43
|
+
|
|
44
|
+
function flush() {
|
|
45
|
+
if (currentId) {
|
|
46
|
+
const lawText = currentLines.join('\n').trim();
|
|
47
|
+
const { prose, validators } = extractValidators(lawText, currentId);
|
|
48
|
+
const lawObj = { id: currentId, text: prose };
|
|
49
|
+
if (validators && validators.length > 0) {
|
|
50
|
+
lawObj.validators = validators;
|
|
51
|
+
}
|
|
52
|
+
laws.push(lawObj);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const heading = line.match(/^## (.+)/);
|
|
58
|
+
if (heading) {
|
|
59
|
+
flush();
|
|
60
|
+
currentId = heading[1];
|
|
61
|
+
currentLines.length = 0;
|
|
62
|
+
} else if (currentId) {
|
|
63
|
+
currentLines.push(line);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
flush();
|
|
67
|
+
return laws;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract validators block from law text and return prose-only text plus parsed validators.
|
|
72
|
+
* @param {string} lawText - Full law text including optional validators block
|
|
73
|
+
* @param {string} lawId - Law ID for error messages
|
|
74
|
+
* @returns {{prose: string, validators: Array}} - Prose text and validators array
|
|
75
|
+
* @throws {Error} if validators block is malformed
|
|
76
|
+
*/
|
|
77
|
+
function extractValidators(lawText, lawId) {
|
|
78
|
+
// Find validators: block (must start at beginning of line, followed by indented lines)
|
|
79
|
+
// The pattern captures lines starting with spaces/tabs (indented content)
|
|
80
|
+
// Using possessive quantifier to avoid backtracking: (?:...)+ instead of (...)*.
|
|
81
|
+
const validatorBlockMatch = lawText.match(/^validators:\n((?:[ \t]+\S.*(?:\n|$))*)/m);
|
|
82
|
+
|
|
83
|
+
if (!validatorBlockMatch || !validatorBlockMatch[1].trim()) {
|
|
84
|
+
return { prose: lawText, validators: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Extract prose (everything before validators:)
|
|
88
|
+
const prose = lawText.substring(0, validatorBlockMatch.index).trim();
|
|
89
|
+
|
|
90
|
+
// Extract and parse validators block
|
|
91
|
+
const validatorBlockText = validatorBlockMatch[1];
|
|
92
|
+
const validators = parseValidatorBlock(validatorBlockText, lawId);
|
|
93
|
+
|
|
94
|
+
return { prose, validators };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parse a validator entry from lines.
|
|
99
|
+
* @param {string} lawId - Law ID for error messages
|
|
100
|
+
* @param {Set} seenIds - Set of seen validator IDs to detect duplicates
|
|
101
|
+
* @param {object} validator - Validator object being built
|
|
102
|
+
* @throws {Error} if validator is invalid
|
|
103
|
+
*/
|
|
104
|
+
function saveValidator(validator, seenIds, lawId) {
|
|
105
|
+
validateValidator(validator, seenIds, lawId);
|
|
106
|
+
return validator;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse field from a line: key: value
|
|
111
|
+
* @param {string} line - Line to parse
|
|
112
|
+
* @param {string} key - Field key to match
|
|
113
|
+
* @returns {string|null} - Field value or null
|
|
114
|
+
*/
|
|
115
|
+
function parseField(line, key) {
|
|
116
|
+
const match = line.match(new RegExp(`^\\s*${key}:\\s*(.+)`));
|
|
117
|
+
return match ? match[1].trim() : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handle a new validator entry line.
|
|
122
|
+
* @param {object} currentValidator - Current validator object or null
|
|
123
|
+
* @param {Array} validators - List to add saved validators to
|
|
124
|
+
* @param {string} line - Line to process
|
|
125
|
+
* @param {Set} seenIds - Set of seen IDs
|
|
126
|
+
* @param {string} lawId - Law ID for error messages
|
|
127
|
+
* @returns {object} - New validator object or null
|
|
128
|
+
*/
|
|
129
|
+
function handleValidatorEntry(currentValidator, validators, line, seenIds, lawId) {
|
|
130
|
+
const entryMatch = line.match(/^\s*-\s*id:\s*(.+)/);
|
|
131
|
+
if (entryMatch) {
|
|
132
|
+
// Save previous validator if exists
|
|
133
|
+
if (currentValidator) {
|
|
134
|
+
validators.push(saveValidator(currentValidator, seenIds, lawId));
|
|
135
|
+
}
|
|
136
|
+
return { id: entryMatch[1].trim() };
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Process a field line in a validator entry.
|
|
143
|
+
* @param {object} validator - Current validator object
|
|
144
|
+
* @param {string} line - Line to process
|
|
145
|
+
*/
|
|
146
|
+
function processValidatorField(validator, line) {
|
|
147
|
+
const command = parseField(line, 'command');
|
|
148
|
+
if (command !== null) {
|
|
149
|
+
validator.command = command;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const failureMeans = parseField(line, 'failure-means');
|
|
154
|
+
if (failureMeans !== null) {
|
|
155
|
+
validator['failure-means'] = failureMeans;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Process a single line in the validators block.
|
|
161
|
+
* @param {object} currentValidator - Current validator or null
|
|
162
|
+
* @param {string} line - Line to process
|
|
163
|
+
* @param {string} lawId - Law ID for error messages
|
|
164
|
+
* @throws {Error} if validator entry not started
|
|
165
|
+
*/
|
|
166
|
+
function processValidatorLine(currentValidator, line, lawId) {
|
|
167
|
+
if (!currentValidator) {
|
|
168
|
+
throw new Error(`law "${lawId}": validator entry missing required 'id'`);
|
|
169
|
+
}
|
|
170
|
+
processValidatorField(currentValidator, line);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse the validators block content (YAML-like format)
|
|
175
|
+
* @param {string} blockText - Text content of validators block
|
|
176
|
+
* @param {string} lawId - Law ID for error messages
|
|
177
|
+
* @returns {Array} - Array of parsed validators
|
|
178
|
+
* @throws {Error} if validators are malformed
|
|
179
|
+
*/
|
|
180
|
+
function parseValidatorBlock(blockText, lawId) {
|
|
181
|
+
const validators = [];
|
|
182
|
+
const lines = blockText.split('\n');
|
|
183
|
+
let currentValidator = null;
|
|
184
|
+
const seenIds = new Set();
|
|
185
|
+
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
// Skip empty lines
|
|
188
|
+
if (!line.trim()) continue;
|
|
189
|
+
|
|
190
|
+
// Check for new validator entry (starts with -)
|
|
191
|
+
const newValidator = handleValidatorEntry(currentValidator, validators, line, seenIds, lawId);
|
|
192
|
+
if (newValidator) {
|
|
193
|
+
currentValidator = newValidator;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
processValidatorLine(currentValidator, line, lawId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Save last validator
|
|
201
|
+
if (currentValidator) {
|
|
202
|
+
validators.push(saveValidator(currentValidator, seenIds, lawId));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return validators;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate a single validator entry.
|
|
210
|
+
* @param {object} validator - Validator object with id, command, and optional failure-means
|
|
211
|
+
* @param {Set} seenIds - Set of seen validator IDs to detect duplicates
|
|
212
|
+
* @param {string} lawId - Law ID for error messages
|
|
213
|
+
* @throws {Error} if validator is invalid
|
|
214
|
+
*/
|
|
215
|
+
function validateValidator(validator, seenIds, lawId) {
|
|
216
|
+
if (!validator.id) {
|
|
217
|
+
throw new Error(`law "${lawId}": validator entry missing required 'id'`);
|
|
218
|
+
}
|
|
219
|
+
if (!validator.command) {
|
|
220
|
+
throw new Error(`law "${lawId}": validator entry missing required 'command'`);
|
|
221
|
+
}
|
|
222
|
+
if (seenIds.has(validator.id)) {
|
|
223
|
+
throw new Error(`law "${lawId}": duplicate validator id '${validator.id}' in law`);
|
|
224
|
+
}
|
|
225
|
+
seenIds.add(validator.id);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function collectLawsFromDir(dir, io, sourcePrefix) {
|
|
229
|
+
if (!(await io.exists(dir))) return [];
|
|
230
|
+
const files = await io.readDir(dir);
|
|
231
|
+
const mdFiles = files.filter(f => f.endsWith('.md')).sort();
|
|
232
|
+
const results = [];
|
|
233
|
+
for (const file of mdFiles) {
|
|
234
|
+
const text = await io.readFile(join(dir, file));
|
|
235
|
+
results.push(...parseLaws(text, `${sourcePrefix}/${file}`));
|
|
236
|
+
}
|
|
237
|
+
return results;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Collect all laws (global and type-specific) for a given artefact type.
|
|
242
|
+
* Returns laws with their source and validator information.
|
|
243
|
+
*/
|
|
244
|
+
async function collectAllLaws(foundryDir, io, { typeId } = {}) {
|
|
245
|
+
const laws = await collectLawsFromDir(join(foundryDir, 'laws'), io, 'laws');
|
|
246
|
+
|
|
247
|
+
if (typeId) {
|
|
248
|
+
const typeLawsPath = join(foundryDir, 'artefacts', typeId, 'laws.md');
|
|
249
|
+
if (await io.exists(typeLawsPath)) {
|
|
250
|
+
const text = await io.readFile(typeLawsPath);
|
|
251
|
+
laws.push(...parseLaws(text, `artefacts/${typeId}/laws.md`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return laws;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function getLaws(foundryDir, io, { typeId } = {}) {
|
|
259
|
+
const laws = await collectAllLaws(foundryDir, io, { typeId });
|
|
260
|
+
|
|
261
|
+
// Return prose-only without source or validators
|
|
262
|
+
return laws.map(law => ({ id: law.id, text: law.text }));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function getLawsForQuench(foundryDir, io, { typeId } = {}) {
|
|
266
|
+
const laws = await collectAllLaws(foundryDir, io, { typeId });
|
|
267
|
+
|
|
268
|
+
// Return only laws that have validators
|
|
269
|
+
return laws.filter(law => law.validators && law.validators.length > 0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function getAppraisers(foundryDir, io) {
|
|
273
|
+
const dir = join(foundryDir, 'appraisers');
|
|
274
|
+
if (!(await io.exists(dir))) return [];
|
|
275
|
+
const files = await io.readDir(dir);
|
|
276
|
+
const mdFiles = files.filter(f => f.endsWith('.md')).sort();
|
|
277
|
+
const result = [];
|
|
278
|
+
for (const file of mdFiles) {
|
|
279
|
+
const text = await io.readFile(join(dir, file));
|
|
280
|
+
const { frontmatter, body } = parseDoc(text);
|
|
281
|
+
const entry = { id: frontmatter.id, personality: body };
|
|
282
|
+
if (frontmatter.model) entry.model = frontmatter.model;
|
|
283
|
+
result.push(entry);
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function getFlow(foundryDir, flowId, io) {
|
|
289
|
+
const path = join(foundryDir, 'flows', `${flowId}.md`);
|
|
290
|
+
if (!(await io.exists(path))) {
|
|
291
|
+
throw new Error(`Flow not found: ${flowId}`);
|
|
292
|
+
}
|
|
293
|
+
const text = await io.readFile(path);
|
|
294
|
+
return parseDoc(text);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildAppraiserPool(allAppraisers, allowed) {
|
|
298
|
+
return allowed ? allAppraisers.filter(a => allowed.includes(a.id)) : allAppraisers;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function roundRobinSelect(pool, count) {
|
|
302
|
+
const result = [];
|
|
303
|
+
for (let i = 0; i < count; i++) {
|
|
304
|
+
result.push(pool[i % pool.length]);
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function resolveAppraiserConfig(frontmatter, countOverride) {
|
|
310
|
+
const appraiserConfig = frontmatter.appraisers || {};
|
|
311
|
+
return {
|
|
312
|
+
count: countOverride || appraiserConfig.count || 3,
|
|
313
|
+
allowed: appraiserConfig.allowed || null,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function selectAppraisers(foundryDir, typeId, { io, countOverride } = {}) {
|
|
318
|
+
if (!io) throw new Error('selectAppraisers: io is required');
|
|
319
|
+
|
|
320
|
+
const { frontmatter } = await getArtefactType(foundryDir, typeId, io);
|
|
321
|
+
const { count, allowed } = resolveAppraiserConfig(frontmatter, countOverride);
|
|
322
|
+
|
|
323
|
+
const allAppraisers = await getAppraisers(foundryDir, io);
|
|
324
|
+
const pool = buildAppraiserPool(allAppraisers, allowed);
|
|
325
|
+
if (pool.length === 0) return [];
|
|
326
|
+
|
|
327
|
+
return roundRobinSelect(pool, count);
|
|
328
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failed-flow lifecycle helpers.
|
|
3
|
+
*
|
|
4
|
+
* When a tool encounters an unrecoverable error (e.g. stage_end cannot
|
|
5
|
+
* flush memory to NDJSON and the on-disk source of truth is now behind
|
|
6
|
+
* the live DB), it marks WORK.md with `status: failed` and a `reason`.
|
|
7
|
+
*
|
|
8
|
+
* Every mutating tool guards on this state via `requireNotFailed`. This
|
|
9
|
+
* includes work-branch FS writers (artefacts, feedback, workfile, stage,
|
|
10
|
+
* orchestrate), memory writers — row-level (memory_put, memory_relate,
|
|
11
|
+
* memory_unrelate) and admin (create_*, rename_*, drop_*, reset, init,
|
|
12
|
+
* vacuum, change_embedding_model) — and `validate_run`, since validation
|
|
13
|
+
* commands are project-defined subprocesses with arbitrary side effects
|
|
14
|
+
* (linters with --fix, formatters). The rule is simple: tools that mutate
|
|
15
|
+
* disk or live DB state, or run unsandboxed subprocesses that could mutate
|
|
16
|
+
* it, stay blocked while the abandoned work-branch filesystem remains the
|
|
17
|
+
* source of truth.
|
|
18
|
+
*
|
|
19
|
+
* Read-only diagnostics (workfile_get, memory_list/get/neighbours/query/search,
|
|
20
|
+
* memory_dump, memory_validate) remain available to support diagnosis before
|
|
21
|
+
* the cycle is abandoned.
|
|
22
|
+
*
|
|
23
|
+
* Recovery paths: `foundry_workfile_delete` abandons the cycle; editing
|
|
24
|
+
* WORK.md to remove the failed status after fixing the underlying issue
|
|
25
|
+
* restores normal operation. `foundry_stage_retry` provides a deterministic
|
|
26
|
+
* rollback mechanism: it discards uncommitted memory changes, clears the
|
|
27
|
+
* failed status, and resets the stage state, provided the git working tree
|
|
28
|
+
* is clean.
|
|
29
|
+
*/
|
|
30
|
+
import { parseFrontmatter, setFrontmatterField, writeFrontmatter } from './workfile.js';
|
|
31
|
+
|
|
32
|
+
const MAX_REASON_LEN = 500;
|
|
33
|
+
|
|
34
|
+
function truncateReason(reason) {
|
|
35
|
+
const s = String(reason ?? '');
|
|
36
|
+
if (s.length <= MAX_REASON_LEN) return s;
|
|
37
|
+
return s.slice(0, MAX_REASON_LEN) + '...';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {{exists: (p: string) => boolean, readFile: (p: string) => string}} io
|
|
42
|
+
* @returns {{reason: string} | null}
|
|
43
|
+
*/
|
|
44
|
+
export function readFailedStatus(io) {
|
|
45
|
+
if (!io.exists('WORK.md')) return null;
|
|
46
|
+
const text = io.readFile('WORK.md');
|
|
47
|
+
const fm = parseFrontmatter(text);
|
|
48
|
+
if (fm.status !== 'failed') return null;
|
|
49
|
+
return { reason: fm.reason === undefined ? '' : String(fm.reason) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Idempotent: sets `status: failed` and `reason` if not already failed.
|
|
54
|
+
* Preserves the first failure reason when called multiple times - the
|
|
55
|
+
* initial diagnostic reason is more valuable than cascading failures.
|
|
56
|
+
* @param {object} io - requires exists, readFile, writeFile
|
|
57
|
+
* @param {string} reason
|
|
58
|
+
*/
|
|
59
|
+
export function markWorkfileFailed(io, reason) {
|
|
60
|
+
if (!io.exists('WORK.md')) {
|
|
61
|
+
throw new Error('markWorkfileFailed: WORK.md not found');
|
|
62
|
+
}
|
|
63
|
+
const text = io.readFile('WORK.md');
|
|
64
|
+
|
|
65
|
+
// Check if already failed - preserve the first failure reason
|
|
66
|
+
const failed = readFailedStatus(io);
|
|
67
|
+
if (failed) {
|
|
68
|
+
// Already failed - skip overwrite to preserve diagnostic first reason
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const withStatus = setFrontmatterField(text, 'status', 'failed');
|
|
73
|
+
const withReason = setFrontmatterField(withStatus, 'reason', truncateReason(reason));
|
|
74
|
+
io.writeFile('WORK.md', withReason);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tool guard: returns `{ok:true}` when the flow is healthy, otherwise
|
|
79
|
+
* `{ok:false, error}` with a message that tells the LLM exactly how to
|
|
80
|
+
* escape (abandon the flow).
|
|
81
|
+
* @param {object} io
|
|
82
|
+
* @returns {{ok: true} | {ok: false, error: string}}
|
|
83
|
+
*/
|
|
84
|
+
export function requireNotFailed(io) {
|
|
85
|
+
let failed;
|
|
86
|
+
try {
|
|
87
|
+
failed = readFailedStatus(io);
|
|
88
|
+
} catch {
|
|
89
|
+
// WORK.md is corrupted (malformed YAML) or unreadable (IO error).
|
|
90
|
+
// This is a trouble signal - refuse to proceed.
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error:
|
|
94
|
+
`WORK.md is corrupted or unreadable. ` +
|
|
95
|
+
`No mutating tools are permitted. Use foundry_workfile_delete({confirm: true}) ` +
|
|
96
|
+
`to abandon the cycle, then back out to main and delete the work branch.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!failed) return { ok: true };
|
|
101
|
+
const reason = failed.reason || '(no reason recorded)';
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error:
|
|
105
|
+
`flow is in failed state (reason: ${reason}). ` +
|
|
106
|
+
`No mutating tools are permitted. Use foundry_workfile_delete({confirm: true}) ` +
|
|
107
|
+
`to abandon the cycle, then back out to main and delete the work branch.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clears the failed status from WORK.md, restoring normal operation.
|
|
113
|
+
* Idempotent: safe to call even if not failed.
|
|
114
|
+
* @param {object} io - requires exists, readFile, writeFile
|
|
115
|
+
*/
|
|
116
|
+
export function clearWorkfileFailed(io) {
|
|
117
|
+
if (!io.exists('WORK.md')) {
|
|
118
|
+
throw new Error('clearWorkfileFailed: WORK.md not found');
|
|
119
|
+
}
|
|
120
|
+
const text = io.readFile('WORK.md');
|
|
121
|
+
const fm = parseFrontmatter(text);
|
|
122
|
+
|
|
123
|
+
// Remove status and reason fields
|
|
124
|
+
delete fm.status;
|
|
125
|
+
delete fm.reason;
|
|
126
|
+
|
|
127
|
+
// Rebuild the file with cleaned frontmatter
|
|
128
|
+
const fmBlock = writeFrontmatter(fm);
|
|
129
|
+
const body = text.replace(/^---\n.+?\n---\n?/s, '');
|
|
130
|
+
io.writeFile('WORK.md', body ? `${fmBlock}\n${body}` : fmBlock);
|
|
131
|
+
}
|