@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.
Files changed (170) hide show
  1. package/README.md +200 -198
  2. package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
  3. package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
  4. package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
  5. package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
  6. package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
  7. package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
  8. package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
  9. package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
  10. package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
  11. package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
  12. package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
  13. package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
  14. package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
  15. package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
  16. package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
  17. package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
  18. package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
  19. package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
  20. package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
  21. package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
  22. package/dist/.opencode/plugins/foundry.js +105 -0
  23. package/dist/CHANGELOG.md +490 -0
  24. package/dist/LICENSE +21 -0
  25. package/dist/README.md +278 -0
  26. package/dist/docs/README.md +59 -0
  27. package/dist/docs/architecture.md +434 -0
  28. package/dist/docs/concepts.md +396 -0
  29. package/dist/docs/getting-started.md +345 -0
  30. package/dist/docs/memory-maintenance.md +176 -0
  31. package/dist/docs/tools.md +1411 -0
  32. package/dist/docs/work-spec.md +283 -0
  33. package/dist/scripts/lib/artefacts.js +151 -0
  34. package/dist/scripts/lib/assay/loader.js +151 -0
  35. package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
  36. package/dist/scripts/lib/assay/permissions.js +52 -0
  37. package/dist/scripts/lib/assay/run.js +219 -0
  38. package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
  39. package/dist/scripts/lib/attestation/attest.js +111 -0
  40. package/dist/scripts/lib/attestation/canonical-json.js +109 -0
  41. package/dist/scripts/lib/attestation/hash.js +17 -0
  42. package/dist/scripts/lib/attestation/parse.js +14 -0
  43. package/dist/scripts/lib/attestation/payload.js +106 -0
  44. package/dist/scripts/lib/attestation/render.js +16 -0
  45. package/dist/scripts/lib/attestation/verify.js +15 -0
  46. package/dist/scripts/lib/branch-guard.js +72 -0
  47. package/dist/scripts/lib/config-creators/appraiser.js +9 -0
  48. package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
  49. package/dist/scripts/lib/config-creators/cycle.js +11 -0
  50. package/dist/scripts/lib/config-creators/factory.js +49 -0
  51. package/dist/scripts/lib/config-creators/flow.js +11 -0
  52. package/dist/scripts/lib/config-validators/appraiser.js +49 -0
  53. package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
  54. package/dist/scripts/lib/config-validators/cycle.js +131 -0
  55. package/dist/scripts/lib/config-validators/flow.js +57 -0
  56. package/dist/scripts/lib/config-validators/helpers.js +96 -0
  57. package/dist/scripts/lib/config-validators/law.js +96 -0
  58. package/dist/scripts/lib/config.js +393 -0
  59. package/dist/scripts/lib/failed-flow.js +131 -0
  60. package/dist/scripts/lib/feedback-store.js +249 -0
  61. package/dist/scripts/lib/feedback-transitions.js +105 -0
  62. package/dist/scripts/lib/finalize.js +70 -0
  63. package/dist/scripts/lib/foundational-guards.js +13 -0
  64. package/dist/scripts/lib/git-bridge.js +77 -0
  65. package/dist/scripts/lib/git-finish/work-finish.js +233 -0
  66. package/dist/scripts/lib/git-policy.js +101 -0
  67. package/dist/scripts/lib/guards.js +125 -0
  68. package/dist/scripts/lib/history.js +132 -0
  69. package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
  70. package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
  71. package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
  72. package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
  73. package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
  74. package/dist/scripts/lib/memory/admin/dump.js +47 -0
  75. package/dist/scripts/lib/memory/admin/helpers.js +31 -0
  76. package/dist/scripts/lib/memory/admin/init.js +170 -0
  77. package/dist/scripts/lib/memory/admin/live-store.js +76 -0
  78. package/dist/scripts/lib/memory/admin/reembed.js +285 -0
  79. package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
  80. package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
  81. package/dist/scripts/lib/memory/admin/reset.js +24 -0
  82. package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
  83. package/dist/scripts/lib/memory/admin/validate.js +19 -0
  84. package/dist/scripts/lib/memory/config.js +149 -0
  85. package/dist/scripts/lib/memory/cozo.js +136 -0
  86. package/dist/scripts/lib/memory/drift.js +71 -0
  87. package/dist/scripts/lib/memory/embeddings.js +128 -0
  88. package/dist/scripts/lib/memory/frontmatter.js +75 -0
  89. package/dist/scripts/lib/memory/ndjson.js +84 -0
  90. package/dist/scripts/lib/memory/paths.js +25 -0
  91. package/dist/scripts/lib/memory/permissions.js +41 -0
  92. package/dist/scripts/lib/memory/prompt.js +109 -0
  93. package/dist/scripts/lib/memory/query.js +56 -0
  94. package/dist/scripts/lib/memory/reads.js +109 -0
  95. package/dist/scripts/lib/memory/schema.js +64 -0
  96. package/dist/scripts/lib/memory/search.js +73 -0
  97. package/dist/scripts/lib/memory/singleton.js +49 -0
  98. package/dist/scripts/lib/memory/store.js +162 -0
  99. package/dist/scripts/lib/memory/types.js +93 -0
  100. package/dist/scripts/lib/memory/validate.js +58 -0
  101. package/dist/scripts/lib/memory/writes.js +40 -0
  102. package/{scripts → dist/scripts}/lib/pending.js +7 -2
  103. package/dist/scripts/lib/secret.js +59 -0
  104. package/{scripts → dist/scripts}/lib/slug.js +3 -2
  105. package/dist/scripts/lib/snapshot/finish.js +103 -0
  106. package/dist/scripts/lib/snapshot/inspect.js +253 -0
  107. package/dist/scripts/lib/snapshot/render.js +55 -0
  108. package/dist/scripts/lib/sort-fs-check.js +121 -0
  109. package/dist/scripts/lib/sort-routing.js +101 -0
  110. package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
  111. package/{scripts → dist/scripts}/lib/state.js +4 -0
  112. package/dist/scripts/lib/token.js +57 -0
  113. package/dist/scripts/lib/tracing.js +59 -0
  114. package/dist/scripts/lib/ulid.js +100 -0
  115. package/dist/scripts/lib/validator-jsonl.js +162 -0
  116. package/{scripts → dist/scripts}/lib/workfile.js +38 -20
  117. package/dist/scripts/orchestrate-cycle.js +215 -0
  118. package/dist/scripts/orchestrate-phases.js +314 -0
  119. package/dist/scripts/orchestrate.js +163 -0
  120. package/dist/scripts/sort.js +278 -0
  121. package/{skills → dist/skills}/add-appraiser/SKILL.md +42 -6
  122. package/{skills → dist/skills}/add-artefact-type/SKILL.md +49 -21
  123. package/{skills → dist/skills}/add-cycle/SKILL.md +60 -14
  124. package/dist/skills/add-extractor/SKILL.md +133 -0
  125. package/{skills → dist/skills}/add-flow/SKILL.md +39 -7
  126. package/dist/skills/add-law/SKILL.md +191 -0
  127. package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
  128. package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
  129. package/{skills → dist/skills}/appraise/SKILL.md +62 -13
  130. package/dist/skills/assay/SKILL.md +72 -0
  131. package/dist/skills/change-embedding-model/SKILL.md +58 -0
  132. package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
  133. package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
  134. package/dist/skills/dry-run/SKILL.md +116 -0
  135. package/{skills → dist/skills}/flow/SKILL.md +15 -2
  136. package/dist/skills/forge/SKILL.md +121 -0
  137. package/dist/skills/human-appraise/SKILL.md +153 -0
  138. package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
  139. package/dist/skills/init-memory/SKILL.md +92 -0
  140. package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
  141. package/dist/skills/quench/SKILL.md +99 -0
  142. package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
  143. package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
  144. package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
  145. package/dist/skills/reset-memory/SKILL.md +54 -0
  146. package/dist/skills/upgrade-foundry/SKILL.md +192 -0
  147. package/package.json +34 -17
  148. package/.opencode/plugins/foundry.js +0 -761
  149. package/CHANGELOG.md +0 -90
  150. package/docs/concepts.md +0 -59
  151. package/docs/getting-started.md +0 -78
  152. package/docs/work-spec.md +0 -193
  153. package/scripts/lib/artefacts.js +0 -124
  154. package/scripts/lib/config.js +0 -175
  155. package/scripts/lib/feedback-transitions.js +0 -25
  156. package/scripts/lib/feedback.js +0 -440
  157. package/scripts/lib/finalize.js +0 -41
  158. package/scripts/lib/history.js +0 -59
  159. package/scripts/lib/secret.js +0 -23
  160. package/scripts/lib/tags.js +0 -108
  161. package/scripts/lib/token.js +0 -26
  162. package/scripts/orchestrate.js +0 -418
  163. package/scripts/sort.js +0 -370
  164. package/scripts/validate-tags.js +0 -54
  165. package/skills/add-law/SKILL.md +0 -105
  166. package/skills/forge/SKILL.md +0 -88
  167. package/skills/human-appraise/SKILL.md +0 -82
  168. package/skills/quench/SKILL.md +0 -62
  169. package/skills/upgrade-foundry/SKILL.md +0 -216
  170. /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
+ }