@really-knows-ai/foundry 2.3.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -369
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +393 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +39 -9
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +46 -24
- package/{skills → dist/skills}/add-cycle/SKILL.md +57 -17
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +36 -10
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -100
- package/docs/concepts.md +0 -122
- package/docs/getting-started.md +0 -187
- package/docs/work-spec.md +0 -207
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -111
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attestation payload builder.
|
|
3
|
+
*
|
|
4
|
+
* Constructs a deterministic, cryptographically verifiable record of work completion.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { parseFrontmatter } from '../workfile.js';
|
|
10
|
+
import { parseArtefactsTable } from '../artefacts.js';
|
|
11
|
+
import { parseAllHistoryEntries } from '../history.js';
|
|
12
|
+
import { sha256Text, sortPaths } from './hash.js';
|
|
13
|
+
|
|
14
|
+
function defaultIo() {
|
|
15
|
+
return {
|
|
16
|
+
readFile: (filePath) => readFileSync(filePath, 'utf8'),
|
|
17
|
+
fileExists: (filePath) => existsSync(filePath),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readWorkFiles(cwd, io) {
|
|
22
|
+
const { readFile, fileExists } = io ?? defaultIo();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
workText: readFile(path.join(cwd, 'WORK.md')),
|
|
26
|
+
historyText: fileExists(path.join(cwd, 'WORK.history.yaml')) ? readFile(path.join(cwd, 'WORK.history.yaml')) : '',
|
|
27
|
+
feedbackText: fileExists(path.join(cwd, 'WORK.feedback.yaml')) ? readFile(path.join(cwd, 'WORK.feedback.yaml')) : '',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseAndSortHistoryEntries(historyText) {
|
|
32
|
+
const allHistoryEntries = parseAllHistoryEntries(historyText);
|
|
33
|
+
return allHistoryEntries.slice().sort((a, b) => {
|
|
34
|
+
const seqA = typeof a.seq === 'number' ? a.seq : 0;
|
|
35
|
+
const seqB = typeof b.seq === 'number' ? b.seq : 0;
|
|
36
|
+
return seqA - seqB;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildStageRecord(entry) {
|
|
41
|
+
const record = {
|
|
42
|
+
changed_files: entry.changed_files ? sortPaths(entry.changed_files) : [],
|
|
43
|
+
cycle: entry.cycle,
|
|
44
|
+
iteration: entry.iteration,
|
|
45
|
+
open_feedback: entry.open_feedback ?? 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (entry.route !== undefined) {
|
|
49
|
+
record.route = entry.route;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
record.stage = entry.stage;
|
|
53
|
+
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildStagesFromEntries(sortedEntries) {
|
|
58
|
+
return sortedEntries.map(buildStageRecord);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function def(value, fallback) {
|
|
62
|
+
return value || fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildContract(frontmatter) {
|
|
66
|
+
return {
|
|
67
|
+
allowed_write_scope: def(frontmatter['allowed-write-scope'], []),
|
|
68
|
+
entry_cycle: frontmatter.cycle,
|
|
69
|
+
expected_output_types: def(frontmatter['expected-output-types'], []),
|
|
70
|
+
flow_id: frontmatter.flow,
|
|
71
|
+
required_deterministic_checks: def(frontmatter['required-deterministic-checks'], []),
|
|
72
|
+
required_human_gates: def(frontmatter['required-human-gates'], null),
|
|
73
|
+
required_stages: def(frontmatter.stages, []),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildGovernance(frontmatter, workText, historyText, feedbackText) {
|
|
78
|
+
return {
|
|
79
|
+
config_commit: def(frontmatter['config-commit'], null),
|
|
80
|
+
workfile_hashes: {
|
|
81
|
+
'WORK.md': sha256Text(workText),
|
|
82
|
+
'WORK.history.yaml': sha256Text(historyText),
|
|
83
|
+
'WORK.feedback.yaml': sha256Text(feedbackText),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildAttestationPayload({ cwd, goalText, archiveBranch, archiveTipSha, io }) {
|
|
89
|
+
const { workText, historyText, feedbackText } = readWorkFiles(cwd, io);
|
|
90
|
+
|
|
91
|
+
const frontmatter = parseFrontmatter(workText);
|
|
92
|
+
const artefacts = parseArtefactsTable(workText);
|
|
93
|
+
|
|
94
|
+
const sortedEntries = parseAndSortHistoryEntries(historyText);
|
|
95
|
+
const stages = buildStagesFromEntries(sortedEntries);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
contract: buildContract(frontmatter),
|
|
99
|
+
governance: buildGovernance(frontmatter, workText, historyText, feedbackText),
|
|
100
|
+
outputs: artefacts.map(row => ({ path: row.file, status: row.status })),
|
|
101
|
+
process: { stages },
|
|
102
|
+
request: { goal_text: goalText },
|
|
103
|
+
schema: 'foundry-attestation/v1',
|
|
104
|
+
work_branch_archive: { name: archiveBranch, tip_sha: archiveTipSha },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attestation commit message renderer.
|
|
3
|
+
*
|
|
4
|
+
* Formats commit messages with human-readable summaries and canonical attestation blocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function renderAttestedCommitMessage({ humanSummary, payloadJson }) {
|
|
8
|
+
return [
|
|
9
|
+
humanSummary,
|
|
10
|
+
'',
|
|
11
|
+
'-----BEGIN FOUNDRY ATTESTATION-----',
|
|
12
|
+
payloadJson,
|
|
13
|
+
'-----END FOUNDRY ATTESTATION-----',
|
|
14
|
+
'',
|
|
15
|
+
].join('\n');
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { extractAttestationBlock } from './parse.js';
|
|
3
|
+
|
|
4
|
+
export function verifyAttestationRef({ cwd, ref = 'HEAD' }) {
|
|
5
|
+
execFileSync('git', ['verify-commit', ref], { cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
6
|
+
const message = execFileSync('git', ['log', '-1', '--pretty=%B', ref], { cwd, encoding: 'utf8', stdio: 'pipe' });
|
|
7
|
+
const json = extractAttestationBlock(message);
|
|
8
|
+
let payload;
|
|
9
|
+
try {
|
|
10
|
+
payload = JSON.parse(json);
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error(`malformed attestation JSON: ${json}`);
|
|
13
|
+
}
|
|
14
|
+
return { status: 'verified', schema: payload.schema, payload };
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const CONFIG_RE = /^config\/[^/]+$/;
|
|
2
|
+
const WORK_RE = /^work\/.+$/;
|
|
3
|
+
export const DRY_RUN_RE = /^dry-run\/[^/]+\/[^/]+$/;
|
|
4
|
+
|
|
5
|
+
function tryRevParse(io) {
|
|
6
|
+
try {
|
|
7
|
+
const out = io.exec(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
8
|
+
if (out && out !== 'HEAD') return out;
|
|
9
|
+
} catch { /* fall through to symbolic-ref */ }
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function trySymbolicRef(io) {
|
|
14
|
+
try {
|
|
15
|
+
const sym = io.exec(['git', 'symbolic-ref', '--short', 'HEAD']).trim();
|
|
16
|
+
if (sym) return sym;
|
|
17
|
+
} catch { /* truly detached or not a repo */ }
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function currentBranch(io) {
|
|
22
|
+
// `git rev-parse --abbrev-ref HEAD` exits non-zero on a fresh repo with
|
|
23
|
+
// no commits (unborn branch) and on a non-repo directory. Fall back to
|
|
24
|
+
// `git symbolic-ref --short HEAD` which still resolves the unborn
|
|
25
|
+
// branch's name; if that also fails (truly detached or non-repo), treat
|
|
26
|
+
// as "no current branch" so the guard returns a structured refusal
|
|
27
|
+
// envelope rather than throwing.
|
|
28
|
+
return tryRevParse(io) || trySymbolicRef(io) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function describe(branch) {
|
|
32
|
+
return branch === null ? 'detached HEAD' : `'${branch}'`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requireOnConfigBranch(io) {
|
|
36
|
+
const b = currentBranch(io);
|
|
37
|
+
if (b && CONFIG_RE.test(b)) return { ok: true };
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error:
|
|
41
|
+
`this tool requires a config/<description> branch (strict; ` +
|
|
42
|
+
`dry-run/<x>/<y> does not count); currently on ${describe(b)}. ` +
|
|
43
|
+
`Use foundry_git_branch({ kind: "config", description: "..." }) ` +
|
|
44
|
+
`from main first.`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function requireOnFlowBranch(io) {
|
|
49
|
+
const b = currentBranch(io);
|
|
50
|
+
if (b && (WORK_RE.test(b) || DRY_RUN_RE.test(b))) return { ok: true };
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error:
|
|
54
|
+
`this tool requires a work/<flow>-<desc> or ` +
|
|
55
|
+
`dry-run/<x>/<y> branch; currently on ${describe(b)}. ` +
|
|
56
|
+
`Use foundry_git_branch({ kind: "work", flowId, description }) ` +
|
|
57
|
+
`from main, or { kind: "dry-run", flowId, description } from a config branch.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function requireOnConfigOrFlowBranch(io) {
|
|
62
|
+
const b = currentBranch(io);
|
|
63
|
+
if (b && (CONFIG_RE.test(b) || WORK_RE.test(b) || DRY_RUN_RE.test(b))) {
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error:
|
|
69
|
+
`this tool requires a config/* or work/* or dry-run/*/* ` +
|
|
70
|
+
`branch; currently on ${describe(b)}.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { validate } from '../config-validators/appraiser.js';
|
|
3
|
+
import { makeCreator } from './factory.js';
|
|
4
|
+
|
|
5
|
+
export const create = makeCreator({
|
|
6
|
+
kind: { human: 'appraiser', underscored: 'appraiser' },
|
|
7
|
+
pathFor: (args) => join('foundry', 'appraisers', `${args.name}.md`),
|
|
8
|
+
validator: validate,
|
|
9
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { validate } from '../config-validators/artefact-type.js';
|
|
3
|
+
import { makeCreator } from './factory.js';
|
|
4
|
+
|
|
5
|
+
export const create = makeCreator({
|
|
6
|
+
kind: { human: 'artefact-type', underscored: 'artefact_type' },
|
|
7
|
+
pathFor: (args) => join('foundry', 'artefacts', args.name, 'definition.md'),
|
|
8
|
+
validator: validate,
|
|
9
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { validate } from '../config-validators/cycle.js';
|
|
3
|
+
import { makeCreator } from './factory.js';
|
|
4
|
+
|
|
5
|
+
const KIND = 'cycle';
|
|
6
|
+
|
|
7
|
+
export const create = makeCreator({
|
|
8
|
+
kind: KIND,
|
|
9
|
+
pathFor: (args) => join('foundry', 'cycles', `${args.name}.md`),
|
|
10
|
+
validator: validate,
|
|
11
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { commitWithPolicy } from '../git-bridge.js';
|
|
3
|
+
|
|
4
|
+
function runCustomValidation(customValidation, args) {
|
|
5
|
+
if (!customValidation) return null;
|
|
6
|
+
const result = customValidation(args);
|
|
7
|
+
return result.ok ? null : result;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function checkFileExists(io, path) {
|
|
11
|
+
if (await io.exists(path)) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
errors: [`${path} already exists; this tool only creates new files — to update, edit by hand on this config/* branch`],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normaliseKind(kind) {
|
|
21
|
+
return typeof kind === 'string' ? { human: kind, underscored: kind } : kind;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makeCreator({ kind, pathFor, validator, customValidation }) {
|
|
25
|
+
return async function create(args) {
|
|
26
|
+
const customResult = runCustomValidation(customValidation, args);
|
|
27
|
+
if (customResult) return customResult;
|
|
28
|
+
|
|
29
|
+
const v = await validator({ name: args.name, body: args.body, io: args.io });
|
|
30
|
+
if (!v.ok) return { ok: false, errors: v.errors };
|
|
31
|
+
|
|
32
|
+
const path = pathFor(args);
|
|
33
|
+
const existsError = await checkFileExists(args.io, path);
|
|
34
|
+
if (existsError) return existsError;
|
|
35
|
+
|
|
36
|
+
await args.io.mkdirp(dirname(path));
|
|
37
|
+
await args.io.writeFile(path, args.body);
|
|
38
|
+
|
|
39
|
+
const kindNormalised = normaliseKind(kind);
|
|
40
|
+
|
|
41
|
+
const sha = commitWithPolicy({
|
|
42
|
+
message: `config: add ${kindNormalised.human} ${args.name}\n\nvia foundry_config_create_${kindNormalised.underscored}`,
|
|
43
|
+
allowedPatterns: ['foundry/**'],
|
|
44
|
+
execFile: args.execFile,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { ok: true, path, sha };
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { validate } from '../config-validators/flow.js';
|
|
3
|
+
import { makeCreator } from './factory.js';
|
|
4
|
+
|
|
5
|
+
const KIND = 'flow';
|
|
6
|
+
|
|
7
|
+
export const create = makeCreator({
|
|
8
|
+
kind: KIND,
|
|
9
|
+
pathFor: (args) => join('foundry', 'flows', `${args.name}.md`),
|
|
10
|
+
validator: validate,
|
|
11
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tryParseFrontmatter,
|
|
3
|
+
requireNonEmptyString,
|
|
4
|
+
validateIdMatch,
|
|
5
|
+
bodyAfterFrontmatter,
|
|
6
|
+
} from './helpers.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate an appraiser definition body.
|
|
10
|
+
*
|
|
11
|
+
* Checks the rules the runtime depends on: `getAppraisers()` reads
|
|
12
|
+
* `frontmatter.id`, optional `frontmatter.model`, and the body prose. The
|
|
13
|
+
* `add-appraiser` skill additionally requires a human-facing `name`.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} opts
|
|
16
|
+
* @param {string} opts.name Slugged identifier (matches frontmatter.id).
|
|
17
|
+
* @param {string} opts.body Full markdown body.
|
|
18
|
+
* @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
|
|
19
|
+
*/
|
|
20
|
+
export async function validate({ name, body }) {
|
|
21
|
+
const parsed = tryParseFrontmatter(body);
|
|
22
|
+
if (!parsed.ok) return { ok: false, errors: parsed.errors };
|
|
23
|
+
const fm = parsed.fm;
|
|
24
|
+
|
|
25
|
+
const errors = [
|
|
26
|
+
requireNonEmptyString(fm.id, 'frontmatter.id'),
|
|
27
|
+
validateIdMatch(fm, name),
|
|
28
|
+
requireNonEmptyString(fm.name, 'frontmatter.name'),
|
|
29
|
+
checkModel(fm),
|
|
30
|
+
checkBody(body),
|
|
31
|
+
].filter(Boolean);
|
|
32
|
+
|
|
33
|
+
return errors.length ? { ok: false, errors } : { ok: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkModel(fm) {
|
|
37
|
+
if (fm.model !== undefined && (typeof fm.model !== 'string' || !fm.model.trim())) {
|
|
38
|
+
return 'frontmatter.model, when present, must be a non-empty string';
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function checkBody(body) {
|
|
44
|
+
const afterFm = bodyAfterFrontmatter(body);
|
|
45
|
+
if (!afterFm) {
|
|
46
|
+
return 'body must contain a personality description after the frontmatter';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
tryParseFrontmatter,
|
|
3
|
+
requireNonEmptyString,
|
|
4
|
+
validateNameMatch,
|
|
5
|
+
requireHeading,
|
|
6
|
+
validateStringArrayEntries,
|
|
7
|
+
} from './helpers.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate an artefact-type definition body.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} opts.name Slugged identifier (matches frontmatter.name).
|
|
14
|
+
* @param {string} opts.body Full markdown body.
|
|
15
|
+
* @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
|
|
16
|
+
*/
|
|
17
|
+
export async function validate({ name, body }) {
|
|
18
|
+
const parsed = tryParseFrontmatter(body);
|
|
19
|
+
if (!parsed.ok) return { ok: false, errors: parsed.errors };
|
|
20
|
+
const fm = parsed.fm;
|
|
21
|
+
|
|
22
|
+
const errors = [
|
|
23
|
+
requireNonEmptyString(fm.name, 'frontmatter.name'),
|
|
24
|
+
validateNameMatch(fm, name),
|
|
25
|
+
checkFilePatterns(fm),
|
|
26
|
+
requireHeading(body, 'Definition'),
|
|
27
|
+
].filter(Boolean);
|
|
28
|
+
|
|
29
|
+
return errors.length ? { ok: false, errors } : { ok: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkFilePatterns(fm) {
|
|
33
|
+
const patterns = fm['file-patterns'];
|
|
34
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
35
|
+
return 'frontmatter.file-patterns is required and must be a non-empty array of glob strings';
|
|
36
|
+
}
|
|
37
|
+
return validateStringArrayEntries(patterns, 'frontmatter.file-patterns');
|
|
38
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
tryParseFrontmatter,
|
|
4
|
+
requireNonEmptyString,
|
|
5
|
+
validateIdMatch,
|
|
6
|
+
validateStringArrayEntries,
|
|
7
|
+
} from './helpers.js';
|
|
8
|
+
|
|
9
|
+
const VALID_INPUT_TYPES = new Set(['any-of', 'all-of']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate a cycle definition body.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} opts
|
|
15
|
+
* @param {string} opts.name Slugged identifier (matches frontmatter.id).
|
|
16
|
+
* @param {string} opts.body Full markdown body.
|
|
17
|
+
* @param {object} opts.io IO adapter with `exists(path)`.
|
|
18
|
+
* @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
|
|
19
|
+
*/
|
|
20
|
+
export async function validate({ name, body, io }) {
|
|
21
|
+
const parsed = tryParseFrontmatter(body);
|
|
22
|
+
if (!parsed.ok) return { ok: false, errors: parsed.errors };
|
|
23
|
+
const fm = parsed.fm;
|
|
24
|
+
|
|
25
|
+
const errors = [
|
|
26
|
+
requireNonEmptyString(fm.id, 'frontmatter.id'),
|
|
27
|
+
validateIdMatch(fm, name),
|
|
28
|
+
requireNonEmptyString(fm.name, 'frontmatter.name'),
|
|
29
|
+
await checkOutputType(fm, io),
|
|
30
|
+
...await checkInputs(fm, io),
|
|
31
|
+
...await checkTargets(fm, io),
|
|
32
|
+
].filter(Boolean);
|
|
33
|
+
|
|
34
|
+
return errors.length ? { ok: false, errors } : { ok: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function checkOutputType(fm, io) {
|
|
38
|
+
const outputType = fm['output-type'];
|
|
39
|
+
const strErr = requireNonEmptyString(outputType, 'frontmatter.output-type');
|
|
40
|
+
if (strErr) return strErr;
|
|
41
|
+
const filePath = join('foundry', 'artefacts', outputType, 'definition.md');
|
|
42
|
+
if (!(await io.exists(filePath))) {
|
|
43
|
+
return `output-type references artefact type "${outputType}" but ${filePath} does not exist`;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function checkInputs(fm, io) {
|
|
49
|
+
const inputs = fm.inputs;
|
|
50
|
+
if (inputs === undefined) return [];
|
|
51
|
+
|
|
52
|
+
const shapeErrors = checkInputsShape(inputs);
|
|
53
|
+
if (shapeErrors.length) return shapeErrors;
|
|
54
|
+
|
|
55
|
+
const typeErrors = checkInputType(inputs);
|
|
56
|
+
const artefactErrors = await checkInputArtefacts(inputs, io);
|
|
57
|
+
return [...typeErrors, ...artefactErrors];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function checkInputsShape(inputs) {
|
|
61
|
+
const errors = [];
|
|
62
|
+
const isInvalidShape = typeof inputs !== 'object' || inputs === null || Array.isArray(inputs);
|
|
63
|
+
if (isInvalidShape) {
|
|
64
|
+
errors.push('frontmatter.inputs, when present, must be an object with type and artefacts');
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function checkInputType(inputs) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
if (!VALID_INPUT_TYPES.has(inputs.type)) {
|
|
72
|
+
errors.push('frontmatter.inputs.type must be one of: any-of, all-of');
|
|
73
|
+
}
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function checkInputArtefacts(inputs, io) {
|
|
78
|
+
const artefacts = inputs.artefacts;
|
|
79
|
+
const noArtefacts = !Array.isArray(artefacts) || artefacts.length === 0;
|
|
80
|
+
if (noArtefacts) {
|
|
81
|
+
return ['frontmatter.inputs.artefacts must be a non-empty array of artefact-type ids'];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const entryErr = validateStringArrayEntries(artefacts, 'frontmatter.inputs.artefacts');
|
|
85
|
+
const refErrors = await validateArtefactRefs(artefacts, io);
|
|
86
|
+
return entryErr ? [entryErr, ...refErrors] : refErrors;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function validateArtefactRefs(artefacts, io) {
|
|
90
|
+
const errors = [];
|
|
91
|
+
for (const id of artefacts) {
|
|
92
|
+
const isValidId = typeof id === 'string' && id.trim();
|
|
93
|
+
if (!isValidId) continue;
|
|
94
|
+
const refErr = await validateArtefactRef(id, io, 'inputs.artefacts');
|
|
95
|
+
if (refErr) errors.push(refErr);
|
|
96
|
+
}
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function validateArtefactRef(id, io, label) {
|
|
101
|
+
const filePath = join('foundry', 'artefacts', id, 'definition.md');
|
|
102
|
+
if (!(await io.exists(filePath))) {
|
|
103
|
+
return `${label} references artefact type "${id}" but ${filePath} does not exist`;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function checkTargets(fm, io) {
|
|
109
|
+
const targets = fm.targets;
|
|
110
|
+
if (targets === undefined) return [];
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(targets)) {
|
|
113
|
+
return ['frontmatter.targets, when present, must be an array of cycle ids'];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const entryErr = validateStringArrayEntries(targets, 'frontmatter.targets');
|
|
117
|
+
const refErrors = await validateCycleRefs(targets, io);
|
|
118
|
+
return entryErr ? [entryErr, ...refErrors] : refErrors;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function validateCycleRefs(targets, io) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
for (const id of targets) {
|
|
124
|
+
if (typeof id !== 'string' || !id.trim()) continue;
|
|
125
|
+
const filePath = join('foundry', 'cycles', `${id}.md`);
|
|
126
|
+
if (!(await io.exists(filePath))) {
|
|
127
|
+
errors.push(`targets references cycle "${id}" but ${filePath} does not exist`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return errors;
|
|
131
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
tryParseFrontmatter,
|
|
4
|
+
requireNonEmptyString,
|
|
5
|
+
validateIdMatch,
|
|
6
|
+
requireHeading,
|
|
7
|
+
validateStringArrayEntries,
|
|
8
|
+
} from './helpers.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate a flow definition body.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} opts
|
|
14
|
+
* @param {string} opts.name Slugged identifier (matches frontmatter.id).
|
|
15
|
+
* @param {string} opts.body Full markdown body.
|
|
16
|
+
* @param {object} opts.io IO adapter with `exists(path)`.
|
|
17
|
+
* @returns {Promise<{ok: true} | {ok: false, errors: string[]}>}
|
|
18
|
+
*/
|
|
19
|
+
export async function validate({ name, body, io }) {
|
|
20
|
+
const parsed = tryParseFrontmatter(body);
|
|
21
|
+
if (!parsed.ok) return { ok: false, errors: parsed.errors };
|
|
22
|
+
const fm = parsed.fm;
|
|
23
|
+
|
|
24
|
+
const errors = [
|
|
25
|
+
requireNonEmptyString(fm.id, 'frontmatter.id'),
|
|
26
|
+
validateIdMatch(fm, name),
|
|
27
|
+
requireNonEmptyString(fm.name, 'frontmatter.name'),
|
|
28
|
+
await checkStartingCycles(fm, io),
|
|
29
|
+
requireHeading(body, 'Cycles'),
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
|
|
32
|
+
return errors.length ? { ok: false, errors } : { ok: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function checkStartingCycles(fm, io) {
|
|
36
|
+
const starting = fm['starting-cycles'];
|
|
37
|
+
const isEmpty = !Array.isArray(starting) || starting.length === 0;
|
|
38
|
+
if (isEmpty) {
|
|
39
|
+
return 'frontmatter.starting-cycles is required and must be a non-empty array of cycle ids';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const entryErr = validateStringArrayEntries(starting, 'frontmatter.starting-cycles');
|
|
43
|
+
if (entryErr) return entryErr;
|
|
44
|
+
|
|
45
|
+
return await checkCycleRefsExist(starting, io, 'starting-cycles');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function checkCycleRefsExist(ids, io, label) {
|
|
49
|
+
for (const id of ids) {
|
|
50
|
+
if (typeof id !== 'string' || !id.trim()) continue;
|
|
51
|
+
const filePath = join('foundry', 'cycles', `${id}.md`);
|
|
52
|
+
if (!(await io.exists(filePath))) {
|
|
53
|
+
return `${label} references cycle "${id}" but ${filePath} does not exist`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseFrontmatter } from '../workfile.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared helpers for config validators.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse frontmatter from a body string.
|
|
9
|
+
* @param {string} body
|
|
10
|
+
* @returns {{ok: true, fm: object} | {ok: false, errors: string[]}}
|
|
11
|
+
*/
|
|
12
|
+
export function tryParseFrontmatter(body) {
|
|
13
|
+
if (!/^---\n[\s\S]*?\n---/.test(body)) {
|
|
14
|
+
return { ok: false, errors: ['frontmatter is missing or unparseable'] };
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return { ok: true, fm: parseFrontmatter(body) };
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return { ok: false, errors: [`frontmatter is unparseable: ${err.message}`] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check that a value is a non-empty string.
|
|
25
|
+
* @param {unknown} value
|
|
26
|
+
* @param {string} fieldName
|
|
27
|
+
* @returns {string|null} Error message or null.
|
|
28
|
+
*/
|
|
29
|
+
export function requireNonEmptyString(value, fieldName) {
|
|
30
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
31
|
+
return `${fieldName} is required and must be a non-empty string`;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate that frontmatter.id matches the supplied name.
|
|
38
|
+
* @param {object} fm
|
|
39
|
+
* @param {string} name
|
|
40
|
+
* @returns {string|null}
|
|
41
|
+
*/
|
|
42
|
+
export function validateIdMatch(fm, name) {
|
|
43
|
+
if (fm.id && fm.id !== name) {
|
|
44
|
+
return `frontmatter.id (${fm.id}) must match the supplied name (${name})`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate that frontmatter.name matches the supplied name.
|
|
51
|
+
* @param {object} fm
|
|
52
|
+
* @param {string} name
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
export function validateNameMatch(fm, name) {
|
|
56
|
+
if (fm.name && fm.name !== name) {
|
|
57
|
+
return `frontmatter.name (${fm.name}) must match the supplied name (${name})`;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract body content after frontmatter.
|
|
64
|
+
* @param {string} body
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function bodyAfterFrontmatter(body) {
|
|
68
|
+
return body.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check that the body contains a markdown heading matching the given text.
|
|
73
|
+
* @param {string} body
|
|
74
|
+
* @param {string} headingText e.g. "Definition" or "Cycles"
|
|
75
|
+
* @returns {string|null}
|
|
76
|
+
*/
|
|
77
|
+
export function requireHeading(body, headingText) {
|
|
78
|
+
const pattern = new RegExp(`^##\\s+${headingText}\\s*$`, 'm');
|
|
79
|
+
if (!pattern.test(body)) {
|
|
80
|
+
return `body must contain a "## ${headingText}" section`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate that an array contains only non-empty strings.
|
|
87
|
+
* @param {unknown[]} arr
|
|
88
|
+
* @param {string} fieldName
|
|
89
|
+
* @returns {string|null}
|
|
90
|
+
*/
|
|
91
|
+
export function validateStringArrayEntries(arr, fieldName) {
|
|
92
|
+
if (arr.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
93
|
+
return `every ${fieldName} entry must be a non-empty string`;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|