@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
package/scripts/lib/history.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import yaml from 'js-yaml';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Load history entries for a cycle, sorted by timestamp ascending.
|
|
5
|
-
*/
|
|
6
|
-
export function loadHistory(historyPath, cycle, io) {
|
|
7
|
-
if (!io.exists(historyPath)) return [];
|
|
8
|
-
const data = yaml.load(io.readFile(historyPath)) || [];
|
|
9
|
-
const filtered = data.filter(e => e.cycle === cycle);
|
|
10
|
-
filtered.sort((a, b) => {
|
|
11
|
-
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
12
|
-
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
13
|
-
return ta - tb;
|
|
14
|
-
});
|
|
15
|
-
return filtered;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Append a history entry with auto-generated ISO timestamp.
|
|
20
|
-
*/
|
|
21
|
-
export function appendEntry(historyPath, { cycle, stage, iteration, comment, route }, io) {
|
|
22
|
-
if (iteration == null) throw new Error('iteration is required');
|
|
23
|
-
if (!comment) throw new Error('comment is required');
|
|
24
|
-
|
|
25
|
-
let existing = [];
|
|
26
|
-
if (io.exists(historyPath)) {
|
|
27
|
-
existing = yaml.load(io.readFile(historyPath)) || [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const entry = {
|
|
31
|
-
cycle,
|
|
32
|
-
stage,
|
|
33
|
-
iteration,
|
|
34
|
-
comment,
|
|
35
|
-
timestamp: new Date().toISOString(),
|
|
36
|
-
};
|
|
37
|
-
if (route !== undefined) entry.route = route;
|
|
38
|
-
existing.push(entry);
|
|
39
|
-
|
|
40
|
-
io.writeFile(historyPath, yaml.dump(existing));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Count forge entries for a cycle.
|
|
45
|
-
*/
|
|
46
|
-
export function getIteration(historyPath, cycle, io) {
|
|
47
|
-
const history = loadHistory(historyPath, cycle, io);
|
|
48
|
-
return history.filter(e => (e.stage || '').split(':')[0] === 'forge').length;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Return the `route` field from the most recent `sort` history entry for a
|
|
53
|
-
* given cycle, or null if none exists.
|
|
54
|
-
*/
|
|
55
|
-
export function readLastSortRoute(historyPath, cycle, io) {
|
|
56
|
-
const entries = loadHistory(historyPath, cycle, io).filter(e => e.stage === 'sort');
|
|
57
|
-
if (!entries.length) return null;
|
|
58
|
-
return entries[entries.length - 1].route ?? null;
|
|
59
|
-
}
|
package/scripts/lib/secret.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, mkdirSync, openSync, writeSync, closeSync } from 'node:fs';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
export function readOrCreateSecret(directory) {
|
|
6
|
-
const dir = join(directory, '.foundry');
|
|
7
|
-
const file = join(dir, '.secret');
|
|
8
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
9
|
-
const bytes = randomBytes(32);
|
|
10
|
-
let fd;
|
|
11
|
-
try {
|
|
12
|
-
fd = openSync(file, 'wx', 0o600);
|
|
13
|
-
} catch (err) {
|
|
14
|
-
if (err.code === 'EEXIST') return readFileSync(file);
|
|
15
|
-
throw err;
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
writeSync(fd, bytes);
|
|
19
|
-
} finally {
|
|
20
|
-
closeSync(fd);
|
|
21
|
-
}
|
|
22
|
-
return bytes;
|
|
23
|
-
}
|
package/scripts/lib/tags.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared tag validation utilities used by sort.js and validate-tags.js.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
6
|
-
import { join } from 'path';
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Constants
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
export const VALID_TAG_RE = /^(#validation|#hitl|#law:[\w-]+)$/;
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Law collection
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export function collectLawIds(foundryDir) {
|
|
19
|
-
const ids = new Set();
|
|
20
|
-
|
|
21
|
-
const lawsDir = join(foundryDir, 'laws');
|
|
22
|
-
if (existsSync(lawsDir)) {
|
|
23
|
-
for (const file of readdirSync(lawsDir)) {
|
|
24
|
-
if (!file.endsWith('.md')) continue;
|
|
25
|
-
const text = readFileSync(join(lawsDir, file), 'utf-8');
|
|
26
|
-
for (const id of extractLawHeadings(text)) ids.add(id);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const artefactsDir = join(foundryDir, 'artefacts');
|
|
31
|
-
if (existsSync(artefactsDir)) {
|
|
32
|
-
for (const typeDir of readdirSync(artefactsDir)) {
|
|
33
|
-
const lawsPath = join(artefactsDir, typeDir, 'laws.md');
|
|
34
|
-
if (!existsSync(lawsPath)) continue;
|
|
35
|
-
const text = readFileSync(lawsPath, 'utf-8');
|
|
36
|
-
for (const id of extractLawHeadings(text)) ids.add(id);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return ids;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function extractLawHeadings(text) {
|
|
44
|
-
const ids = [];
|
|
45
|
-
for (const line of text.split('\n')) {
|
|
46
|
-
const match = line.match(/^## (.+)$/);
|
|
47
|
-
if (match) ids.push(match[1].trim());
|
|
48
|
-
}
|
|
49
|
-
return ids;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Tag extraction
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract all hash-tags from a feedback line.
|
|
58
|
-
* Returns an array of strings like ['#validation', '#law:brevity'].
|
|
59
|
-
*/
|
|
60
|
-
export function extractAllTags(line) {
|
|
61
|
-
return (line.match(/#[\w][\w:-]*/g) || []);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Validation
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Validate all feedback tags in the Feedback section of WORK.md text.
|
|
70
|
-
*
|
|
71
|
-
* Returns an array of error strings. Empty array = all valid.
|
|
72
|
-
*/
|
|
73
|
-
export function validateTags(workText, foundryDir) {
|
|
74
|
-
const lawIds = collectLawIds(foundryDir);
|
|
75
|
-
const errors = [];
|
|
76
|
-
let inFeedback = false;
|
|
77
|
-
let lineNum = 0;
|
|
78
|
-
|
|
79
|
-
for (const line of workText.split('\n')) {
|
|
80
|
-
lineNum++;
|
|
81
|
-
const stripped = line.trim();
|
|
82
|
-
|
|
83
|
-
if (stripped === '# Feedback') { inFeedback = true; continue; }
|
|
84
|
-
if (inFeedback && stripped.startsWith('# ') && stripped !== '# Feedback') {
|
|
85
|
-
inFeedback = false; continue;
|
|
86
|
-
}
|
|
87
|
-
if (!inFeedback || !(/^- \[/.test(stripped))) continue;
|
|
88
|
-
|
|
89
|
-
const tags = extractAllTags(stripped);
|
|
90
|
-
if (tags.length === 0) {
|
|
91
|
-
errors.push({ line: lineNum, message: 'Feedback item has no tag', raw: stripped });
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
for (const tag of tags) {
|
|
96
|
-
if (!VALID_TAG_RE.test(tag)) {
|
|
97
|
-
errors.push({ line: lineNum, message: `Unknown tag: ${tag}`, raw: stripped });
|
|
98
|
-
} else if (tag.startsWith('#law:')) {
|
|
99
|
-
const lawId = tag.slice(5);
|
|
100
|
-
if (!lawIds.has(lawId)) {
|
|
101
|
-
errors.push({ line: lineNum, message: `Law not found: ${lawId}`, raw: stripped });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return errors;
|
|
108
|
-
}
|
package/scripts/lib/token.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
export function signToken(payload, secret) {
|
|
4
|
-
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
5
|
-
const mac = createHmac('sha256', secret).update(body).digest('base64url');
|
|
6
|
-
return `${body}.${mac}`;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function verifyToken(token, secret) {
|
|
10
|
-
if (typeof token !== 'string' || !token.includes('.')) return { ok: false, reason: 'malformed' };
|
|
11
|
-
const [body, mac] = token.split('.');
|
|
12
|
-
if (!body || !mac) return { ok: false, reason: 'malformed' };
|
|
13
|
-
const expected = createHmac('sha256', secret).update(body).digest();
|
|
14
|
-
let given;
|
|
15
|
-
try { given = Buffer.from(mac, 'base64url'); } catch { return { ok: false, reason: 'malformed' }; }
|
|
16
|
-
if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
|
|
17
|
-
return { ok: false, reason: 'bad_signature' };
|
|
18
|
-
}
|
|
19
|
-
let payload;
|
|
20
|
-
try { payload = JSON.parse(Buffer.from(body, 'base64url').toString()); }
|
|
21
|
-
catch { return { ok: false, reason: 'malformed' }; }
|
|
22
|
-
if (typeof payload.exp !== 'number' || payload.exp < Date.now()) {
|
|
23
|
-
return { ok: false, reason: 'expired' };
|
|
24
|
-
}
|
|
25
|
-
return { ok: true, payload };
|
|
26
|
-
}
|
package/scripts/orchestrate.js
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
// Foundry v2.3.0 orchestrate: deterministic cycle orchestration.
|
|
2
|
-
// Composes internal functions (sort, finalize, history, commit, configure)
|
|
3
|
-
// into a single entry point the LLM drives via a 3-line loop.
|
|
4
|
-
|
|
5
|
-
import { runSort } from './sort.js';
|
|
6
|
-
import {
|
|
7
|
-
getCycleDefinition,
|
|
8
|
-
getArtefactType,
|
|
9
|
-
getValidation,
|
|
10
|
-
} from './lib/config.js';
|
|
11
|
-
import { parseFrontmatter, writeFrontmatter } from './lib/workfile.js';
|
|
12
|
-
import { parseArtefactsTable, addArtefactRow, setArtefactStatus } from './lib/artefacts.js';
|
|
13
|
-
import { readActiveStage, readLastStage, clearActiveStage } from './lib/state.js';
|
|
14
|
-
import { appendEntry, getIteration } from './lib/history.js';
|
|
15
|
-
import { listFeedback } from './lib/feedback.js';
|
|
16
|
-
|
|
17
|
-
export function renderDispatchPrompt({ stage, cycle, token, cwd, filePatterns }) {
|
|
18
|
-
const lines = [
|
|
19
|
-
`You are a Foundry stage agent. Invoke the ${stage.split(':')[0]} skill and follow its instructions exactly.`,
|
|
20
|
-
``,
|
|
21
|
-
`Stage: ${stage}`,
|
|
22
|
-
`Cycle: ${cycle}`,
|
|
23
|
-
`Token: ${token}`,
|
|
24
|
-
`Working directory: ${cwd}`,
|
|
25
|
-
];
|
|
26
|
-
if (filePatterns && filePatterns.length) {
|
|
27
|
-
lines.push(`File patterns (forge only): ${JSON.stringify(filePatterns)}`);
|
|
28
|
-
}
|
|
29
|
-
lines.push(
|
|
30
|
-
``,
|
|
31
|
-
`Your FIRST tool call MUST be foundry_stage_begin({stage, cycle, token}) using the values above.`,
|
|
32
|
-
`Your LAST tool call MUST be foundry_stage_end({summary}).`,
|
|
33
|
-
``,
|
|
34
|
-
`When done, report back a brief summary. Do NOT call foundry_history_append, foundry_git_commit, or foundry_artefacts_add — the orchestrator handles all of those.`
|
|
35
|
-
);
|
|
36
|
-
return lines.join('\n');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function synthesizeStages({ cycleId, hasValidation, humanAppraise }) {
|
|
40
|
-
const stages = [`forge:${cycleId}`];
|
|
41
|
-
if (hasValidation) stages.push(`quench:${cycleId}`);
|
|
42
|
-
stages.push(`appraise:${cycleId}`);
|
|
43
|
-
if (humanAppraise) stages.push(`human-appraise:${cycleId}`);
|
|
44
|
-
return stages;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function needsSetup(workMdContent) {
|
|
48
|
-
const match = workMdContent.match(/^---\n([\s\S]*?)\n---/);
|
|
49
|
-
if (!match) return true;
|
|
50
|
-
const fm = match[1];
|
|
51
|
-
return !/^stages:/m.test(fm);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// Task-6 stub helpers (wired in later). readForgeFilePatterns is real now
|
|
56
|
-
// because the first-call dispatch prompt needs it.
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
export function findCycleOutputArtefact(cycleId, io) {
|
|
60
|
-
if (!io.exists('WORK.md')) return null;
|
|
61
|
-
const content = io.readFile('WORK.md');
|
|
62
|
-
const rows = parseArtefactsTable(content);
|
|
63
|
-
const match = rows.find(r => r.cycle === cycleId);
|
|
64
|
-
return match ? { file: match.file, type: match.type, status: match.status } : null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function readCycleTargets(cycleId, io) {
|
|
68
|
-
try {
|
|
69
|
-
const cd = await getCycleDefinition('foundry', cycleId, io);
|
|
70
|
-
return cd.frontmatter?.targets ?? [];
|
|
71
|
-
} catch {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function readForgeFilePatterns(cycleId, io) {
|
|
77
|
-
try {
|
|
78
|
-
const cd = await getCycleDefinition('foundry', cycleId, io);
|
|
79
|
-
const output = cd.frontmatter?.output;
|
|
80
|
-
if (!output) return null;
|
|
81
|
-
const at = await getArtefactType('foundry', output, io);
|
|
82
|
-
return at.frontmatter?.['file-patterns'] ?? null;
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function readRecentFeedback(cycleId, io, limit = 5) {
|
|
89
|
-
// Best-effort: surface recent deadlocked items (rejected or wont-fix) for
|
|
90
|
-
// the human-appraise checkpoint. Returns last `limit` matching entries.
|
|
91
|
-
// On any parse error, return [] rather than crashing the cycle.
|
|
92
|
-
try {
|
|
93
|
-
if (!io.exists('WORK.md')) return [];
|
|
94
|
-
const content = io.readFile('WORK.md');
|
|
95
|
-
const rows = parseArtefactsTable(content);
|
|
96
|
-
const items = listFeedback(content, cycleId, rows);
|
|
97
|
-
const deadlocked = items.filter(
|
|
98
|
-
it => it.state === 'wont-fix' || it.state === 'rejected'
|
|
99
|
-
);
|
|
100
|
-
return deadlocked.slice(-limit);
|
|
101
|
-
} catch {
|
|
102
|
-
return [];
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function violation(details, affectedFiles = []) {
|
|
107
|
-
return {
|
|
108
|
-
action: 'violation',
|
|
109
|
-
details,
|
|
110
|
-
recoverable: false,
|
|
111
|
-
affected_files: affectedFiles,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function markArtefactBlocked(cycleId, io) {
|
|
116
|
-
if (!io.exists('WORK.md')) return { ok: true };
|
|
117
|
-
const content = io.readFile('WORK.md');
|
|
118
|
-
const rows = parseArtefactsTable(content);
|
|
119
|
-
const row = rows.find(r => r.cycle === cycleId);
|
|
120
|
-
if (!row) return { ok: true };
|
|
121
|
-
try {
|
|
122
|
-
io.writeFile('WORK.md', setArtefactStatus(content, row.file, 'blocked'));
|
|
123
|
-
return { ok: true };
|
|
124
|
-
} catch (e) {
|
|
125
|
-
// Surface to caller: setArtefactStatus is strict (e.g. row already
|
|
126
|
-
// blocked/done, invalid status). Don't crash; let caller annotate
|
|
127
|
-
// the violation.
|
|
128
|
-
return { ok: false, error: e?.message || String(e) };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
// Sort result -> action shape
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
|
|
136
|
-
async function handleSortResult(sortResult, { cycleId, cwd, io }) {
|
|
137
|
-
const { route, model, token, details } = sortResult;
|
|
138
|
-
const base = typeof route === 'string' ? route.split(':')[0] : '';
|
|
139
|
-
|
|
140
|
-
if (route === 'done') {
|
|
141
|
-
const art = findCycleOutputArtefact(cycleId, io);
|
|
142
|
-
return {
|
|
143
|
-
action: 'done',
|
|
144
|
-
cycle: cycleId,
|
|
145
|
-
artefact_file: art?.file ?? null,
|
|
146
|
-
next_cycles: await readCycleTargets(cycleId, io),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (route === 'blocked') {
|
|
151
|
-
const art = findCycleOutputArtefact(cycleId, io);
|
|
152
|
-
return {
|
|
153
|
-
action: 'blocked',
|
|
154
|
-
cycle: cycleId,
|
|
155
|
-
artefact_file: art?.file ?? null,
|
|
156
|
-
reason: details ?? 'iteration limit reached with unresolved feedback',
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (route === 'violation') {
|
|
161
|
-
return violation(details ?? 'sort returned violation');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (base === 'human-appraise') {
|
|
165
|
-
const art = findCycleOutputArtefact(cycleId, io);
|
|
166
|
-
return {
|
|
167
|
-
action: 'human_appraise',
|
|
168
|
-
stage: route,
|
|
169
|
-
token,
|
|
170
|
-
context: {
|
|
171
|
-
cycle: cycleId,
|
|
172
|
-
artefact_file: art?.file ?? null,
|
|
173
|
-
recent_feedback: readRecentFeedback(cycleId, io),
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// forge | quench | appraise
|
|
179
|
-
if (!model) {
|
|
180
|
-
const art = findCycleOutputArtefact(cycleId, io);
|
|
181
|
-
return violation(
|
|
182
|
-
`cycle ${cycleId} stage ${route} has no model declared in cycle definition (\`models:\` field) and no default available`,
|
|
183
|
-
[art?.file].filter(Boolean)
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const filePatterns = base === 'forge'
|
|
188
|
-
? await readForgeFilePatterns(cycleId, io)
|
|
189
|
-
: null;
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
action: 'dispatch',
|
|
193
|
-
stage: route,
|
|
194
|
-
subagent_type: model,
|
|
195
|
-
prompt: renderDispatchPrompt({
|
|
196
|
-
stage: route,
|
|
197
|
-
cycle: cycleId,
|
|
198
|
-
token,
|
|
199
|
-
cwd,
|
|
200
|
-
filePatterns,
|
|
201
|
-
}),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
// Main entry point
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
|
|
209
|
-
export async function runOrchestrate(args = {}, io) {
|
|
210
|
-
const {
|
|
211
|
-
cwd = process.cwd(),
|
|
212
|
-
cycleDef: cycleDefOverride = null,
|
|
213
|
-
git,
|
|
214
|
-
mint,
|
|
215
|
-
now = Date.now,
|
|
216
|
-
lastResult = null,
|
|
217
|
-
finalize = null,
|
|
218
|
-
} = args;
|
|
219
|
-
|
|
220
|
-
if (!io.exists('WORK.md')) {
|
|
221
|
-
return violation('no WORK.md; flow skill must create it first');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let workContent = io.readFile('WORK.md');
|
|
225
|
-
const fm = parseFrontmatter(workContent);
|
|
226
|
-
const cycleId = fm.cycle;
|
|
227
|
-
if (!cycleId) {
|
|
228
|
-
return violation('WORK.md frontmatter missing cycle field', ['WORK.md']);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (needsSetup(workContent)) {
|
|
232
|
-
if (lastResult) {
|
|
233
|
-
return violation(
|
|
234
|
-
'inconsistent state: lastResult provided but WORK.md still needs setup',
|
|
235
|
-
['WORK.md']
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const foundryDir = 'foundry';
|
|
240
|
-
let cycleDefDoc;
|
|
241
|
-
try {
|
|
242
|
-
cycleDefDoc = await getCycleDefinition(foundryDir, cycleId, io);
|
|
243
|
-
} catch {
|
|
244
|
-
return violation(`cycle definition not found for id: ${cycleId}`, ['WORK.md']);
|
|
245
|
-
}
|
|
246
|
-
const cfm = cycleDefDoc.frontmatter || {};
|
|
247
|
-
|
|
248
|
-
const outputType = cfm.output;
|
|
249
|
-
if (!outputType) {
|
|
250
|
-
return violation(`cycle ${cycleId} missing output field`, ['WORK.md']);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
await getArtefactType(foundryDir, outputType, io);
|
|
255
|
-
} catch {
|
|
256
|
-
return violation(`artefact type not found: ${outputType}`, ['WORK.md']);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const validation = await getValidation(foundryDir, outputType, io);
|
|
260
|
-
|
|
261
|
-
let stages;
|
|
262
|
-
if (Array.isArray(cfm.stages)) {
|
|
263
|
-
if (cfm.stages.length === 0) {
|
|
264
|
-
const art = findCycleOutputArtefact(cycleId, io);
|
|
265
|
-
return violation(
|
|
266
|
-
`cycle ${cycleId} has no stages declared in cycle definition`,
|
|
267
|
-
[art?.file, 'WORK.md'].filter(Boolean)
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
stages = cfm.stages.map(s =>
|
|
271
|
-
typeof s === 'string' && s.includes(':') ? s : `${s}:${cycleId}`
|
|
272
|
-
);
|
|
273
|
-
} else {
|
|
274
|
-
stages = synthesizeStages({
|
|
275
|
-
cycleId,
|
|
276
|
-
hasValidation: !!validation && validation.length > 0,
|
|
277
|
-
humanAppraise: cfm['human-appraise'] === true,
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const newFm = { ...fm };
|
|
282
|
-
newFm.stages = stages;
|
|
283
|
-
newFm['max-iterations'] = cfm['max-iterations'] ?? 3;
|
|
284
|
-
newFm['human-appraise'] = cfm['human-appraise'] === true;
|
|
285
|
-
newFm['deadlock-appraise'] = cfm['deadlock-appraise'] !== false;
|
|
286
|
-
newFm['deadlock-iterations'] = cfm['deadlock-iterations'] ?? 5;
|
|
287
|
-
if (cfm.models) newFm.models = cfm.models;
|
|
288
|
-
|
|
289
|
-
const body = workContent.replace(/^---\n[\s\S]+?\n---\n?/, '');
|
|
290
|
-
const fmBlock = writeFrontmatter(newFm);
|
|
291
|
-
const newWork = body ? `${fmBlock}\n${body}` : fmBlock;
|
|
292
|
-
io.writeFile('WORK.md', newWork);
|
|
293
|
-
|
|
294
|
-
if (git && typeof git.commit === 'function') {
|
|
295
|
-
git.commit(`[${cycleId}] setup: configure stages and limits`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
workContent = io.readFile('WORK.md');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const activeStage = readActiveStage(io);
|
|
302
|
-
const lastStage = readLastStage(io);
|
|
303
|
-
|
|
304
|
-
if (activeStage && !lastResult) {
|
|
305
|
-
return violation(
|
|
306
|
-
`prior stage ${activeStage.stage} orphaned — no lastResult provided but active stage exists. ` +
|
|
307
|
-
`Likely cause: previous orchestrate call returned dispatch but caller did not follow up.`,
|
|
308
|
-
[]
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (lastResult) {
|
|
313
|
-
// Subagent crash path: stage_end may NOT have been called, so activeStage
|
|
314
|
-
// can still exist and lastStage may be stale or absent. Prefer activeStage
|
|
315
|
-
// (current dispatch) over lastStage (could be from a prior cycle).
|
|
316
|
-
if (lastResult.ok === false) {
|
|
317
|
-
const failedStage = activeStage || lastStage;
|
|
318
|
-
if (!failedStage) {
|
|
319
|
-
return violation('lastResult.ok=false but no stage recorded — orphaned state');
|
|
320
|
-
}
|
|
321
|
-
const blockResult = markArtefactBlocked(cycleId, io);
|
|
322
|
-
if (activeStage) clearActiveStage(io);
|
|
323
|
-
const art = findCycleOutputArtefact(cycleId, io);
|
|
324
|
-
const blockNote = blockResult.ok ? '' : ` (also: failed to mark artefact blocked: ${blockResult.error})`;
|
|
325
|
-
return violation(
|
|
326
|
-
`subagent dispatch failed: ${lastResult.error || 'unknown error'}${blockNote}`,
|
|
327
|
-
[art?.file].filter(Boolean)
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Happy path: foundry_stage_end has run, which writes lastStage and clears
|
|
332
|
-
// activeStage. lastStage is the canonical source of stage identity & baseSha.
|
|
333
|
-
if (!lastStage) {
|
|
334
|
-
return violation('lastResult provided but no last stage recorded — orphaned state');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
let finalizeResult;
|
|
338
|
-
if (typeof finalize !== 'function') {
|
|
339
|
-
return violation(
|
|
340
|
-
'orchestrate caller must inject a `finalize` function when providing lastResult; ' +
|
|
341
|
-
'the plugin wires lib/finalize.finalizeStage; tests must pass a stub.',
|
|
342
|
-
[]
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
finalizeResult = await finalize({
|
|
346
|
-
cycleId,
|
|
347
|
-
stage: lastStage.stage,
|
|
348
|
-
baseSha: lastStage.baseSha,
|
|
349
|
-
io,
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
if (!finalizeResult.ok) {
|
|
353
|
-
const blockResult = markArtefactBlocked(cycleId, io);
|
|
354
|
-
if (activeStage) clearActiveStage(io);
|
|
355
|
-
const blockNote = blockResult.ok ? '' : ` (also: failed to mark artefact blocked: ${blockResult.error})`;
|
|
356
|
-
if (finalizeResult.error === 'unexpected_files') {
|
|
357
|
-
return violation(
|
|
358
|
-
`unexpected files written by subagent: ${(finalizeResult.files || []).join(', ')}${blockNote}`,
|
|
359
|
-
finalizeResult.files || []
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
return violation(`stage_finalize error: ${finalizeResult.error}${blockNote}`, []);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
for (const a of finalizeResult.artefacts ?? []) {
|
|
366
|
-
let wm = io.readFile('WORK.md');
|
|
367
|
-
const rows = parseArtefactsTable(wm);
|
|
368
|
-
if (!rows.some(r => r.file === a.file)) {
|
|
369
|
-
wm = addArtefactRow(wm, {
|
|
370
|
-
file: a.file,
|
|
371
|
-
type: a.type,
|
|
372
|
-
cycle: cycleId,
|
|
373
|
-
status: a.status ?? 'draft',
|
|
374
|
-
});
|
|
375
|
-
io.writeFile('WORK.md', wm);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const summary = lastStage.summary || '(no summary)';
|
|
380
|
-
const historyPath = 'WORK.history.yaml';
|
|
381
|
-
const iteration = getIteration(historyPath, cycleId, io);
|
|
382
|
-
|
|
383
|
-
appendEntry(historyPath, {
|
|
384
|
-
cycle: cycleId,
|
|
385
|
-
stage: 'sort',
|
|
386
|
-
iteration,
|
|
387
|
-
route: lastStage.stage,
|
|
388
|
-
comment: `route ${lastStage.stage}`,
|
|
389
|
-
}, io);
|
|
390
|
-
appendEntry(historyPath, {
|
|
391
|
-
cycle: cycleId,
|
|
392
|
-
stage: lastStage.stage,
|
|
393
|
-
iteration,
|
|
394
|
-
comment: summary,
|
|
395
|
-
}, io);
|
|
396
|
-
|
|
397
|
-
if (git && typeof git.commit === 'function') {
|
|
398
|
-
git.commit(`[${cycleId}] ${lastStage.stage}: ${summary}`);
|
|
399
|
-
}
|
|
400
|
-
// Defensive: stage_end clears activeStage already; this is a no-op in the
|
|
401
|
-
// normal lifecycle but cleans up if the subagent skipped stage_end.
|
|
402
|
-
if (activeStage) clearActiveStage(io);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const sortResult = runSort(
|
|
406
|
-
{
|
|
407
|
-
cycleDef: cycleDefOverride,
|
|
408
|
-
mint,
|
|
409
|
-
now: typeof now === 'function' ? now() : now,
|
|
410
|
-
},
|
|
411
|
-
io
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
return handleSortResult(sortResult, { cycleId, cwd, io });
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Test-only export; keep underscored to discourage runtime use.
|
|
418
|
-
export { handleSortResult as __handleSortResultForTest };
|