@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,28 @@
|
|
|
1
|
+
import { selectAppraisers } from '../../../scripts/lib/config.js';
|
|
2
|
+
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
3
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
4
|
+
|
|
5
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
6
|
+
|
|
7
|
+
export function createAppraiserTools({ tool }) {
|
|
8
|
+
return {
|
|
9
|
+
foundry_appraisers_select: tool({
|
|
10
|
+
description: 'Select appraisers for an artefact type',
|
|
11
|
+
args: {
|
|
12
|
+
typeId: tool.schema.string().describe('Artefact type ID'),
|
|
13
|
+
count: tool.schema.number().optional().describe('Number of appraisers to select'),
|
|
14
|
+
},
|
|
15
|
+
// Flow-tier mutation per SPEC §6: appraiser selection mutates the
|
|
16
|
+
// dispatch state of the in-flight cycle. Branch guard runs before
|
|
17
|
+
// failed-flow gate so wrong-branch refusals win over failed-state.
|
|
18
|
+
execute: guarded('foundry_appraisers_select', [flowBranchGuard, gateNotFailed], async (args, context) => {
|
|
19
|
+
const io = makeIO(context.worktree);
|
|
20
|
+
const result = await selectAppraisers('foundry', args.typeId, {
|
|
21
|
+
io,
|
|
22
|
+
countOverride: args.count ?? null,
|
|
23
|
+
});
|
|
24
|
+
return JSON.stringify(result);
|
|
25
|
+
}, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
4
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
5
|
+
import { parseArtefactsTable, setArtefactStatus } from '../../../scripts/lib/artefacts.js';
|
|
6
|
+
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard } from './helpers.js';
|
|
7
|
+
|
|
8
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
9
|
+
|
|
10
|
+
function makeListTool(tool) {
|
|
11
|
+
return tool({
|
|
12
|
+
description: 'List artefacts from the WORK.md table. Optionally filter by cycle — callers should always pass the current cycle to avoid picking up stale rows from prior sessions.',
|
|
13
|
+
args: {
|
|
14
|
+
cycle: tool.schema.string().optional().describe('Only return rows whose Cycle column matches this value'),
|
|
15
|
+
},
|
|
16
|
+
async execute(args, context) {
|
|
17
|
+
const workPath = path.join(context.worktree, 'WORK.md');
|
|
18
|
+
if (!existsSync(workPath)) {
|
|
19
|
+
return JSON.stringify({ error: 'WORK.md not found' });
|
|
20
|
+
}
|
|
21
|
+
const text = readFileSync(workPath, 'utf-8');
|
|
22
|
+
const rows = parseArtefactsTable(text);
|
|
23
|
+
const filtered = args.cycle ? rows.filter(r => r.cycle === args.cycle) : rows;
|
|
24
|
+
return JSON.stringify(filtered);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createArtefactTools({ tool }) {
|
|
30
|
+
return {
|
|
31
|
+
// NOTE: `foundry_artefacts_add` was removed in v2.2.0. Artefacts are now
|
|
32
|
+
// registered automatically by the orchestrator's internal finalize step as drafts,
|
|
33
|
+
// then promoted to done|blocked via `foundry_artefacts_set_status`.
|
|
34
|
+
foundry_artefacts_set_status: tool({
|
|
35
|
+
description: 'Update the status of an artefact in WORK.md (done|blocked only)',
|
|
36
|
+
args: {
|
|
37
|
+
file: tool.schema.string().describe('Artefact file path'),
|
|
38
|
+
status: tool.schema.string().describe('New status (done|blocked)'),
|
|
39
|
+
},
|
|
40
|
+
execute: guarded('foundry_artefacts_set_status', [flowBranchGuard, gateNotFailed], async (args, context) => {
|
|
41
|
+
const io = makeIO(context.worktree);
|
|
42
|
+
const guard = requireNoActiveStage(io);
|
|
43
|
+
if (!guard.ok) return JSON.stringify({ error: `foundry_artefacts_set_status ${guard.error}` });
|
|
44
|
+
const workPath = path.join(context.worktree, 'WORK.md');
|
|
45
|
+
const text = readFileSync(workPath, 'utf-8');
|
|
46
|
+
try {
|
|
47
|
+
const updated = setArtefactStatus(text, args.file, args.status);
|
|
48
|
+
writeFileSync(workPath, updated, 'utf-8');
|
|
49
|
+
return JSON.stringify({ ok: true });
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return JSON.stringify({ error: e.message });
|
|
52
|
+
}
|
|
53
|
+
}, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
foundry_artefacts_list: makeListTool(tool),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { requireActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
2
|
+
import { markWorkfileFailed } from '../../../scripts/lib/failed-flow.js';
|
|
3
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
4
|
+
import { runAssay } from '../../../scripts/lib/assay/run.js';
|
|
5
|
+
import { syncStore } from '../../../scripts/lib/memory/store.js';
|
|
6
|
+
import { putEntity, relate as memRelate } from '../../../scripts/lib/memory/writes.js';
|
|
7
|
+
import { withStore } from './memory-helpers.js';
|
|
8
|
+
import { makeIO, branchIoFactory, asyncIoFactory, flowBranchGuard, errorJson } from './helpers.js';
|
|
9
|
+
|
|
10
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
11
|
+
|
|
12
|
+
function truncateStderr(stderr) {
|
|
13
|
+
if (!stderr) return '';
|
|
14
|
+
const trimmed = stderr.trim();
|
|
15
|
+
if (trimmed.length <= 500) return trimmed;
|
|
16
|
+
const bytesRemaining = trimmed.length - 500;
|
|
17
|
+
const plural = bytesRemaining === 1 ? 'byte' : 'bytes';
|
|
18
|
+
return `${trimmed.slice(0, 500)}... (${bytesRemaining} more ${plural})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleExtractorFailure(io, res) {
|
|
22
|
+
const stderrSnippet = truncateStderr(res.stderr);
|
|
23
|
+
const msg = `assay aborted on extractor \`${res.failedExtractor}\`: ${res.reason}` +
|
|
24
|
+
(stderrSnippet ? ` (stderr: ${stderrSnippet})` : '');
|
|
25
|
+
try { markWorkfileFailed(io, msg); } catch { /* WORK.md gone? nothing we can do */ }
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
error: msg,
|
|
28
|
+
flow_failed: true,
|
|
29
|
+
aborted: true,
|
|
30
|
+
failedExtractor: res.failedExtractor,
|
|
31
|
+
reason: res.reason,
|
|
32
|
+
stderr: res.stderr,
|
|
33
|
+
perExtractor: res.perExtractor,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function syncAfterAssay(store, memIo) {
|
|
38
|
+
try {
|
|
39
|
+
await syncStore({ store, io: memIo });
|
|
40
|
+
return { ok: true };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return { ok: false, err };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function handleSyncFailure(io, err) {
|
|
47
|
+
const msg = `assay post-run memory sync failed: ${err?.message ?? err}`;
|
|
48
|
+
try { markWorkfileFailed(io, msg); } catch { /* WORK.md gone? nothing we can do */ }
|
|
49
|
+
return JSON.stringify({ error: msg, flow_failed: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleAssayRun(args, context) {
|
|
53
|
+
const io = makeIO(context.worktree);
|
|
54
|
+
const guard = requireActiveStage(io, { stageBase: 'assay', cycle: args.cycle });
|
|
55
|
+
if (!guard.ok) return JSON.stringify({ error: `foundry_assay_run requires active assay stage for cycle '${args.cycle}'; ${guard.error}` });
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { store, vocabulary, writeEmbedder, io: memIo } = await withStore(context);
|
|
59
|
+
const res = await runAssay({
|
|
60
|
+
foundryDir: 'foundry',
|
|
61
|
+
cwd: context.worktree,
|
|
62
|
+
io: memIo,
|
|
63
|
+
extractors: args.extractors,
|
|
64
|
+
store,
|
|
65
|
+
vocabulary,
|
|
66
|
+
putEntity,
|
|
67
|
+
relate: memRelate,
|
|
68
|
+
writeEmbedder,
|
|
69
|
+
syncStore, // G29: Pass syncStore so it's called after each extractor
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!res.ok) return handleExtractorFailure(io, res);
|
|
73
|
+
|
|
74
|
+
const syncResult = await syncAfterAssay(store, memIo);
|
|
75
|
+
if (!syncResult.ok) return handleSyncFailure(io, syncResult.err);
|
|
76
|
+
|
|
77
|
+
return JSON.stringify(res);
|
|
78
|
+
} catch (err) { return errorJson(err); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createAssayTools({ tool }) {
|
|
82
|
+
return {
|
|
83
|
+
foundry_assay_run: tool({
|
|
84
|
+
description: 'Run extractors to populate flow memory. Only callable during an active assay stage. Aborts on first failure; marks the workfile failed. Extractors must output one JSON object per line (JSONL/NDJSON format), not pretty-printed multi-line JSON.',
|
|
85
|
+
args: {
|
|
86
|
+
cycle: tool.schema.string().describe('Cycle name'),
|
|
87
|
+
extractors: tool.schema.array(tool.schema.string()).describe('Extractor names, executed in order'),
|
|
88
|
+
},
|
|
89
|
+
execute: guarded('foundry_assay_run', [flowBranchGuard, gateNotFailed], handleAssayRun, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { writeFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { extractAttestationBlock } from '../../../scripts/lib/attestation/parse.js';
|
|
5
|
+
import { verifyAttestationRef } from '../../../scripts/lib/attestation/verify.js';
|
|
6
|
+
import { buildAttestation } from '../../../scripts/lib/attestation/attest.js';
|
|
7
|
+
import { parseFrontmatter } from '../../../scripts/lib/workfile.js';
|
|
8
|
+
import { requireNoActiveStage } from '../../../scripts/lib/stage-guard.js';
|
|
9
|
+
import { currentBranch } from '../../../scripts/lib/branch-guard.js';
|
|
10
|
+
import { errorJson, makeIO, makeExec } from './helpers.js';
|
|
11
|
+
|
|
12
|
+
function refuse(error) { return JSON.stringify({ error }); }
|
|
13
|
+
|
|
14
|
+
function createShowTool(tool) {
|
|
15
|
+
return tool({
|
|
16
|
+
description: 'Show parsed attestation payload and human summary for a commit ref (default HEAD).',
|
|
17
|
+
args: {
|
|
18
|
+
ref: tool.schema.string().optional().describe('Git ref (default: HEAD)'),
|
|
19
|
+
},
|
|
20
|
+
async execute(args, context) {
|
|
21
|
+
try {
|
|
22
|
+
const ref = args.ref || 'HEAD';
|
|
23
|
+
const cwd = context.worktree;
|
|
24
|
+
const message = execFileSync('git', ['log', '-1', '--pretty=%B', ref],
|
|
25
|
+
{ cwd, encoding: 'utf8', stdio: 'pipe' }
|
|
26
|
+
);
|
|
27
|
+
const json = extractAttestationBlock(message);
|
|
28
|
+
let payload;
|
|
29
|
+
try {
|
|
30
|
+
payload = JSON.parse(json);
|
|
31
|
+
} catch {
|
|
32
|
+
return errorJson(new Error(`malformed attestation JSON: ${json}`));
|
|
33
|
+
}
|
|
34
|
+
const subjectLine = message.split('\n')[0];
|
|
35
|
+
return JSON.stringify({ ok: true, human_summary: subjectLine, payload });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return errorJson(err);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createVerifyTool(tool) {
|
|
44
|
+
return tool({
|
|
45
|
+
description: 'Verify GPG signature and attestation payload for a commit ref (default HEAD).',
|
|
46
|
+
args: {
|
|
47
|
+
ref: tool.schema.string().optional().describe('Git ref (default: HEAD)'),
|
|
48
|
+
},
|
|
49
|
+
async execute(args, context) {
|
|
50
|
+
try {
|
|
51
|
+
const ref = args.ref || 'HEAD';
|
|
52
|
+
const cwd = context.worktree;
|
|
53
|
+
const result = verifyAttestationRef({ cwd, ref });
|
|
54
|
+
return JSON.stringify({ ok: true, ...result });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return errorJson(err);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function guardStageAndBranch(cwd) {
|
|
63
|
+
const io = makeIO(cwd);
|
|
64
|
+
const stageGuard = requireNoActiveStage(io);
|
|
65
|
+
if (!stageGuard.ok) {
|
|
66
|
+
return { ok: false, error: `foundry_attest: ${stageGuard.error}` };
|
|
67
|
+
}
|
|
68
|
+
const branch = currentBranch({ exec: makeExec(cwd) });
|
|
69
|
+
if (!branch || !branch.startsWith('work/')) {
|
|
70
|
+
return { ok: false, error: `foundry_attest: must be run on a work/* branch, current branch is '${branch}'` };
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, branch };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function refuseConfirm(branch, baseBranch) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
ok: false,
|
|
78
|
+
error: 'foundry_attest requires {confirm: true}. Re-invoke with confirm:true to write ATTEST.md.',
|
|
79
|
+
planned: {
|
|
80
|
+
action: 'verify-cycle, compute-diff-sha, write-ATTEST.md, commit',
|
|
81
|
+
branch,
|
|
82
|
+
baseBranch: baseBranch || 'main',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeExecGit(cwd, opts) {
|
|
88
|
+
return (argv) => {
|
|
89
|
+
if (argv[0] === 'diff') {
|
|
90
|
+
return execFileSync('git', argv, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
91
|
+
}
|
|
92
|
+
return execFileSync('git', argv, opts).toString();
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readCycleFromWorkfile(cwd) {
|
|
97
|
+
const workText = readFileSync(path.join(cwd, 'WORK.md'), 'utf8');
|
|
98
|
+
const frontmatter = parseFrontmatter(workText);
|
|
99
|
+
return frontmatter.cycle ?? 'unknown';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function gitRevParseHead(opts) {
|
|
103
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], opts).trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function commitErrorString(err) {
|
|
107
|
+
const stderr = err.stderr ?? err.stdout ?? '';
|
|
108
|
+
return `foundry_attest: commit failed. ${String(stderr).trim()}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cleanupFailedCommit(attestPath, opts) {
|
|
112
|
+
try { execFileSync('git', ['reset', 'HEAD', 'ATTEST.md'], opts); } catch { /* best effort */ }
|
|
113
|
+
try { unlinkSync(attestPath); } catch { /* best effort */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function commitAttestation(cwd, cycle, content, opts) {
|
|
117
|
+
const attestPath = path.join(cwd, 'ATTEST.md');
|
|
118
|
+
writeFileSync(attestPath, content, 'utf8');
|
|
119
|
+
try {
|
|
120
|
+
execFileSync('git', ['add', 'ATTEST.md'], opts);
|
|
121
|
+
const commitMsg = `[${cycle}] attest: cycle complete`;
|
|
122
|
+
execFileSync('git', ['commit', '--no-gpg-sign', '-m', commitMsg], opts);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
cleanupFailedCommit(attestPath, opts);
|
|
125
|
+
return { ok: false, error: commitErrorString(err) };
|
|
126
|
+
}
|
|
127
|
+
const commitSha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], opts).trim();
|
|
128
|
+
return { ok: true, commitSha };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildAttestationInputs(opts) {
|
|
132
|
+
return {
|
|
133
|
+
cwd: opts.cwd,
|
|
134
|
+
baseBranch: opts.baseBranch,
|
|
135
|
+
goalText: opts.goalText,
|
|
136
|
+
archiveBranch: opts.archiveBranch,
|
|
137
|
+
archiveTipSha: opts.archiveTipSha,
|
|
138
|
+
io: { readFile: (p) => readFileSync(p, 'utf8'), fileExists: (p) => existsSync(p) },
|
|
139
|
+
execGit: opts.execGit,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handleBuildResult(result, cwd, cycle, opts) {
|
|
144
|
+
if (!result.ok) return JSON.stringify(result);
|
|
145
|
+
const commitResult = commitAttestation(cwd, cycle, result.content, opts);
|
|
146
|
+
if (!commitResult.ok) return JSON.stringify(commitResult);
|
|
147
|
+
return JSON.stringify({ ok: true, diffSha: result.diffSha, commitSha: commitResult.commitSha });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createAttestTool(tool) {
|
|
151
|
+
return tool({
|
|
152
|
+
description:
|
|
153
|
+
'Verify the current work cycle is complete (all required stages ran, no unresolved ' +
|
|
154
|
+
'feedback, no blocked artefacts) and commit an ATTEST.md attestation file to the work branch. ' +
|
|
155
|
+
'foundry_git_finish will not merge without this commit at HEAD.',
|
|
156
|
+
args: {
|
|
157
|
+
baseBranch: tool.schema.string().optional()
|
|
158
|
+
.describe('Branch to compute diff from (default: main)'),
|
|
159
|
+
message: tool.schema.string()
|
|
160
|
+
.describe('Goal text / human summary for the attestation'),
|
|
161
|
+
confirm: tool.schema.boolean().optional()
|
|
162
|
+
.describe('Must be true to write and commit ATTEST.md'),
|
|
163
|
+
},
|
|
164
|
+
async execute(args, context) {
|
|
165
|
+
const cwd = context.worktree;
|
|
166
|
+
const opts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
167
|
+
const guard = guardStageAndBranch(cwd);
|
|
168
|
+
if (!guard.ok) return refuse(guard.error);
|
|
169
|
+
if (args.confirm !== true) return refuseConfirm(guard.branch, args.baseBranch);
|
|
170
|
+
const baseBranch = args.baseBranch || 'main';
|
|
171
|
+
const execGit = makeExecGit(cwd, opts);
|
|
172
|
+
const cycle = readCycleFromWorkfile(cwd);
|
|
173
|
+
const tipSha = gitRevParseHead(opts);
|
|
174
|
+
const archiveBranch = `archive/${guard.branch}-${tipSha.slice(0, 7)}`;
|
|
175
|
+
const inputs = buildAttestationInputs({
|
|
176
|
+
cwd, baseBranch, goalText: args.message,
|
|
177
|
+
archiveBranch, archiveTipSha: tipSha, execGit,
|
|
178
|
+
});
|
|
179
|
+
const result = await buildAttestation(inputs);
|
|
180
|
+
return handleBuildResult(result, cwd, cycle, opts);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function createAttestationTools({ tool }) {
|
|
186
|
+
return {
|
|
187
|
+
foundry_attestation_show: createShowTool(tool),
|
|
188
|
+
foundry_attestation_verify: createVerifyTool(tool),
|
|
189
|
+
foundry_attest: createAttestTool(tool),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Tools for creating and validating the five foundry config kinds:
|
|
2
|
+
// artefact-type, law, appraiser, flow, cycle.
|
|
3
|
+
//
|
|
4
|
+
// `_validate_<kind>` tools are read-only — they parse the supplied body
|
|
5
|
+
// and report errors. They run anywhere (no branch guard, no failed-flow
|
|
6
|
+
// guard) so authors can iterate on a draft from any branch.
|
|
7
|
+
//
|
|
8
|
+
// `_create_<kind>` tools mutate the worktree (write the file + commit
|
|
9
|
+
// it) and so carry the full guard stack:
|
|
10
|
+
// gitRepo → foundryRoot → configBranch → notFailed
|
|
11
|
+
// matching the config-tier policy.
|
|
12
|
+
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
|
+
import { create as createArtefactType } from '../../../scripts/lib/config-creators/artefact-type.js';
|
|
15
|
+
import { create as createAppraiser } from '../../../scripts/lib/config-creators/appraiser.js';
|
|
16
|
+
import { create as createFlow } from '../../../scripts/lib/config-creators/flow.js';
|
|
17
|
+
import { create as createCycle } from '../../../scripts/lib/config-creators/cycle.js';
|
|
18
|
+
import { validate as validateArtefactType } from '../../../scripts/lib/config-validators/artefact-type.js';
|
|
19
|
+
import { validate as validateLaw } from '../../../scripts/lib/config-validators/law.js';
|
|
20
|
+
import { validate as validateAppraiser } from '../../../scripts/lib/config-validators/appraiser.js';
|
|
21
|
+
import { validate as validateFlow } from '../../../scripts/lib/config-validators/flow.js';
|
|
22
|
+
import { validate as validateCycle } from '../../../scripts/lib/config-validators/cycle.js';
|
|
23
|
+
import { requireGitRepo, requireFoundryRoot } from '../../../scripts/lib/foundational-guards.js';
|
|
24
|
+
import { requireOnConfigBranch } from '../../../scripts/lib/branch-guard.js';
|
|
25
|
+
import { guarded, notFailedGuard } from '../../../scripts/lib/guards.js';
|
|
26
|
+
import { UnexpectedFilesError } from '../../../scripts/lib/git-bridge.js';
|
|
27
|
+
import { makeIO, makeExec, makeAsyncIO, errorJson, branchIoFactory, asyncIoFactory } from './helpers.js';
|
|
28
|
+
|
|
29
|
+
// --- guard helpers ---------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function gitRepoGuard(_args, context) {
|
|
32
|
+
return requireGitRepo(makeIO(context.worktree));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function foundryRootGuard(_args, context) {
|
|
36
|
+
return requireFoundryRoot(makeIO(context.worktree));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function configBranchGuard(_args, context) {
|
|
40
|
+
return requireOnConfigBranch({ exec: makeExec(context.worktree) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const gateNotFailed = notFailedGuard(makeIO);
|
|
44
|
+
|
|
45
|
+
// `git`-prefixed argv runner that satisfies commitWithPolicy's contract:
|
|
46
|
+
// it accepts argv WITHOUT the `git` prefix and prepends it itself.
|
|
47
|
+
function makeExecFile(cwd) {
|
|
48
|
+
return (argv) => execFileSync('git', argv, { cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CREATE_GUARDS = [gitRepoGuard, foundryRootGuard, configBranchGuard, gateNotFailed];
|
|
52
|
+
const VALIDATE_GUARDS = [gitRepoGuard, foundryRootGuard];
|
|
53
|
+
|
|
54
|
+
// --- tool factories --------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
// Module-level helper: returns a `makeCreate` function bound to `tool` and `baseArgs`.
|
|
57
|
+
function createMakeCreate(tool, baseArgs) {
|
|
58
|
+
return function makeCreate(toolName, creator, extraArgs = {}) {
|
|
59
|
+
const kind = toolName.replace('foundry_config_create_', '');
|
|
60
|
+
let desc = `Create a new ${kind} definition (config-tier; requires a config/* branch).`;
|
|
61
|
+
|
|
62
|
+
if (kind === 'law') {
|
|
63
|
+
desc += ' target must be {kind:"global", file:"<name>.md"} or {kind:"type-specific", typeId:"<id>"}.';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return tool({
|
|
67
|
+
description: desc,
|
|
68
|
+
args: { ...baseArgs, ...extraArgs },
|
|
69
|
+
execute: guarded(toolName, CREATE_GUARDS, async (args, context) => {
|
|
70
|
+
try {
|
|
71
|
+
const io = makeAsyncIO(context.worktree);
|
|
72
|
+
const execFile = makeExecFile(context.worktree);
|
|
73
|
+
const out = await creator({ ...args, io, execFile });
|
|
74
|
+
return JSON.stringify(out);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof UnexpectedFilesError) {
|
|
77
|
+
return JSON.stringify({ error: err.message, affected_files: err.files });
|
|
78
|
+
}
|
|
79
|
+
return errorJson(err);
|
|
80
|
+
}
|
|
81
|
+
}, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Module-level helper: returns a `makeValidate` function bound to `tool` and `baseArgs`.
|
|
87
|
+
function createMakeValidate(tool, baseArgs) {
|
|
88
|
+
return function makeValidate(toolName, validator) {
|
|
89
|
+
return tool({
|
|
90
|
+
description: `Validate a ${toolName.replace('foundry_config_validate_', '')} body without writing it.`,
|
|
91
|
+
args: baseArgs,
|
|
92
|
+
execute: guarded(toolName, VALIDATE_GUARDS, async (args, context) => {
|
|
93
|
+
try {
|
|
94
|
+
const io = makeAsyncIO(context.worktree);
|
|
95
|
+
const out = await validator({ ...args, io });
|
|
96
|
+
return JSON.stringify(out);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return errorJson(err);
|
|
99
|
+
}
|
|
100
|
+
}, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- tool factory ---------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export function createConfigCreateTools({ tool }) {
|
|
108
|
+
const baseArgs = {
|
|
109
|
+
name: tool.schema.string(),
|
|
110
|
+
body: tool.schema.string(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const makeCreate = createMakeCreate(tool, baseArgs);
|
|
114
|
+
const makeValidate = createMakeValidate(tool, baseArgs);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
foundry_config_create_artefact_type: makeCreate('foundry_config_create_artefact_type', createArtefactType),
|
|
118
|
+
foundry_config_create_appraiser: makeCreate('foundry_config_create_appraiser', createAppraiser),
|
|
119
|
+
foundry_config_create_flow: makeCreate('foundry_config_create_flow', createFlow),
|
|
120
|
+
foundry_config_create_cycle: makeCreate('foundry_config_create_cycle', createCycle),
|
|
121
|
+
|
|
122
|
+
foundry_config_validate_artefact_type: makeValidate('foundry_config_validate_artefact_type', validateArtefactType),
|
|
123
|
+
foundry_config_validate_law: makeValidate('foundry_config_validate_law', validateLaw),
|
|
124
|
+
foundry_config_validate_appraiser: makeValidate('foundry_config_validate_appraiser', validateAppraiser),
|
|
125
|
+
foundry_config_validate_flow: makeValidate('foundry_config_validate_flow', validateFlow),
|
|
126
|
+
foundry_config_validate_cycle: makeValidate('foundry_config_validate_cycle', validateCycle),
|
|
127
|
+
};
|
|
128
|
+
}
|