@really-knows-ai/foundry 1.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.
@@ -0,0 +1,193 @@
1
+ # WORK.md Spec
2
+
3
+ WORK.md is created at the start of a foundry flow on a work branch. It is the shared state between all stages in all foundry cycles. It is transient — it exists only for the duration of the foundry flow.
4
+
5
+ ## Frontmatter
6
+
7
+ ```yaml
8
+ ---
9
+ flow: <flow-id>
10
+ cycle: <current-cycle-id>
11
+ stages: [forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]
12
+ max-iterations: 3
13
+ ---
14
+ ```
15
+
16
+ Fields:
17
+ - `flow` — the foundry flow being executed
18
+ - `cycle` — the current foundry cycle id
19
+ - `stages` — the ordered route for this foundry cycle, set when the foundry cycle starts. Each entry uses `base:alias` format where `base` is the stage type (`forge`, `quench`, `appraise`, or `hitl`) and `alias` is a human-readable name for what that stage does in this cycle. Determined from the artefact type: if `validation.md` exists, include `quench`; always include `forge` and `appraise`. A `hitl` stage can be included for human-in-the-loop checkpoints.
20
+ - `max-iterations` — how many forge passes before the foundry cycle is blocked (default: 3)
21
+
22
+ The `stages` list is the happy path. Sort follows it but loops back to `forge` when unresolved feedback demands it.
23
+
24
+ ### Who sets what
25
+
26
+ - `flow` — set by the foundry flow skill at foundry flow start, never changes
27
+ - `cycle` — set by the foundry flow skill when starting each foundry cycle
28
+ - `stages` — set by the foundry cycle skill when starting each foundry cycle (reads artefact type to determine if quench is needed)
29
+ - `max-iterations` — set by the foundry cycle skill (default 3, could be overridden in foundry cycle definition)
30
+
31
+ ## Sections
32
+
33
+ ### Goal
34
+
35
+ Free text describing what the foundry flow is producing and any context the human provided. Written once at foundry flow start, not modified after.
36
+
37
+ ### Artefacts
38
+
39
+ A table tracking every artefact produced by the foundry flow.
40
+
41
+ ```markdown
42
+ # Artefacts
43
+
44
+ | File | Type | Cycle | Status |
45
+ |------|------|-------|--------|
46
+ | petitions/login-change.md | petition | write-petition | draft |
47
+ | features/login-change.feature | gherkin | petition-to-gherkin | draft |
48
+ ```
49
+
50
+ Statuses:
51
+ - `draft` — artefact exists but has not cleared all stages
52
+ - `done` — artefact has cleared all stages
53
+ - `blocked` — artefact hit iteration limit or a violation
54
+
55
+ ### Feedback
56
+
57
+ Grouped by artefact file path. Each item is a checklist entry with a tag indicating its source.
58
+
59
+ ```markdown
60
+ # Feedback
61
+
62
+ ## petitions/login-change.md
63
+
64
+ - [ ] Missing "Acceptance Criteria" section #validation
65
+ - [x] Justification is circular #law:justified-change | approved
66
+ - [~] Could be more concise #law:clear-language | wont-fix: brevity would lose necessary context | approved
67
+ ```
68
+
69
+ #### Tags
70
+
71
+ - `#validation` — from a deterministic quench command
72
+ - `#law:<law-id>` — from subjective appraise, tied to a specific law
73
+ - `#hitl` — from human-provided feedback at a hitl checkpoint
74
+
75
+ #### Lifecycle states
76
+
77
+ ```
78
+ - [ ] issue #tag open, needs forge action
79
+ - [x] issue #tag actioned, needs approval
80
+ - [~] issue #tag | wont-fix: <reason> declined by forge, needs approval (appraise only)
81
+ - [x] issue #tag | approved resolved
82
+ - [~] issue #tag | wont-fix: <reason> | approved resolved
83
+ - [x] issue #tag | rejected: <reason> re-opened
84
+ - [~] issue #tag | wont-fix: <reason> | rejected re-opened
85
+ ```
86
+
87
+ #### Rules
88
+
89
+ - Validation feedback (`#validation`) cannot be wont-fixed
90
+ - Feedback is never deleted — it stays as a record of the iteration history
91
+ - New feedback is appended, not inserted
92
+ - Items are grouped under the artefact they relate to
93
+
94
+ ## Who writes what
95
+
96
+ | Section | Written by | Updated by |
97
+ |---------|-----------|------------|
98
+ | Frontmatter (`flow`) | foundry flow skill | nobody |
99
+ | Frontmatter (`cycle`, `stages`, `max-iterations`) | foundry cycle skill | foundry cycle skill (reset on each new cycle) |
100
+ | Goal | foundry flow skill | nobody |
101
+ | Artefacts | forge skill (registers new) | foundry cycle skill (status changes) |
102
+ | Feedback | quench skill, appraise skill, hitl skill | forge skill (actioned/wont-fix), quench/appraise/hitl skill (approved/rejected) |
103
+
104
+ ## WORK.history.yaml
105
+
106
+ A separate file (`WORK.history.yaml`) alongside WORK.md. Append-only log of every stage execution.
107
+
108
+ ```yaml
109
+ - timestamp: "2026-04-17T14:32:01Z"
110
+ cycle: write-petition
111
+ stage: forge:draft-petition
112
+ iteration: 1
113
+ comment: Initial petition draft created
114
+
115
+ - timestamp: "2026-04-17T14:32:45Z"
116
+ cycle: write-petition
117
+ stage: quench:validate-petition
118
+ iteration: 1
119
+ comment: 2 validation issues found
120
+
121
+ - timestamp: "2026-04-17T14:33:12Z"
122
+ cycle: write-petition
123
+ stage: forge:draft-petition
124
+ iteration: 2
125
+ comment: Addressed 2 validation issues
126
+
127
+ - timestamp: "2026-04-17T14:33:30Z"
128
+ cycle: write-petition
129
+ stage: quench:validate-petition
130
+ iteration: 2
131
+ comment: Validation passed
132
+
133
+ - timestamp: "2026-04-17T14:34:00Z"
134
+ cycle: write-petition
135
+ stage: appraise:review-petition
136
+ iteration: 2
137
+ comment: No issues found, cycle complete
138
+ ```
139
+
140
+ ### Fields
141
+
142
+ - `timestamp` — ISO 8601 UTC
143
+ - `cycle` — which foundry cycle this entry belongs to
144
+ - `stage` — which stage just completed, in `base:alias` format (e.g. `forge:draft-petition`, `quench:validate-petition`, `appraise:review-petition`, `hitl:human-review`)
145
+ - `iteration` — the current iteration number (increments each time forge runs within a cycle)
146
+ - `comment` — brief description of what happened
147
+
148
+ ### Rules
149
+
150
+ - Append-only — never edit or delete entries
151
+ - Every stage skill appends an entry when it completes
152
+ - The sort script reads this to determine what has happened in the current foundry cycle
153
+ - Iteration is derived from counting forge entries for the current foundry cycle
154
+
155
+ ### Who writes
156
+
157
+ Every stage skill (forge, quench, appraise, hitl) appends an entry when it finishes.
158
+
159
+ ## Example
160
+
161
+ A complete WORK.md mid-foundry flow:
162
+
163
+ ```markdown
164
+ ---
165
+ flow: make-haiku
166
+ cycle: haiku-creation
167
+ stages: [forge:write-haiku, quench:check-syllables, appraise:evaluate-quality]
168
+ max-iterations: 3
169
+ ---
170
+
171
+ # Goal
172
+
173
+ Write a haiku about autumn rain. Should evoke loneliness
174
+ and the sound of rain on leaves.
175
+
176
+ # Artefacts
177
+
178
+ | File | Type | Cycle | Status |
179
+ |------|------|-------|--------|
180
+ | petitions/autumn-rain-haiku.md | petition | haiku-ideation | done |
181
+ | haiku/autumn-rain.md | haiku | haiku-creation | draft |
182
+
183
+ # Feedback
184
+
185
+ ## petitions/autumn-rain-haiku.md
186
+
187
+ - [x] Acceptance criteria should mention seasonal reference #law:clear-acceptance-criteria | approved
188
+
189
+ ## haiku/autumn-rain.md
190
+
191
+ - [ ] Line 2 has 8 syllables, expected 7 #validation
192
+ - [x] No seasonal reference detected #law:seasonal-reference | approved
193
+ ```
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@really-knows-ai/foundry",
3
+ "version": "1.0.0",
4
+ "description": "A structured framework for AI-driven artefact creation with deterministic routing, quality gates, and iterative refinement cycles.",
5
+ "type": "module",
6
+ "main": ".opencode/plugins/foundry.js",
7
+ "license": "MIT",
8
+ "author": "Really Knows AI",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/really-knows-ai/foundry.git"
12
+ },
13
+ "homepage": "https://github.com/really-knows-ai/foundry",
14
+ "keywords": [
15
+ "foundry",
16
+ "ai",
17
+ "artefact",
18
+ "cycle",
19
+ "routing",
20
+ "quality",
21
+ "opencode",
22
+ "plugin"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.3.0"
26
+ },
27
+ "scripts": {
28
+ "test": "node --test tests/sort.test.js"
29
+ },
30
+ "dependencies": {
31
+ "js-yaml": "^4.1.0",
32
+ "minimatch": "^10.2.5"
33
+ },
34
+ "files": [
35
+ ".opencode/",
36
+ "skills/",
37
+ "scripts/",
38
+ "docs/work-spec.md",
39
+ "docs/concepts.md",
40
+ "docs/getting-started.md",
41
+ "README.md",
42
+ "LICENSE"
43
+ ]
44
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared tag validation utilities used by sort.js and validate-tags.js.
3
+ */
4
+
5
+ import { readFileSync, existsSync, readdirSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Constants
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export const VALID_TAG_RE = /^(#validation|#hitl|#law:[\w-]+)$/;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Law collection
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export function collectLawIds(foundryDir) {
19
+ const ids = new Set();
20
+
21
+ const lawsDir = join(foundryDir, 'laws');
22
+ if (existsSync(lawsDir)) {
23
+ for (const file of readdirSync(lawsDir)) {
24
+ if (!file.endsWith('.md')) continue;
25
+ const text = readFileSync(join(lawsDir, file), 'utf-8');
26
+ for (const id of extractLawHeadings(text)) ids.add(id);
27
+ }
28
+ }
29
+
30
+ const artefactsDir = join(foundryDir, 'artefacts');
31
+ if (existsSync(artefactsDir)) {
32
+ for (const typeDir of readdirSync(artefactsDir)) {
33
+ const lawsPath = join(artefactsDir, typeDir, 'laws.md');
34
+ if (!existsSync(lawsPath)) continue;
35
+ const text = readFileSync(lawsPath, 'utf-8');
36
+ for (const id of extractLawHeadings(text)) ids.add(id);
37
+ }
38
+ }
39
+
40
+ return ids;
41
+ }
42
+
43
+ export function extractLawHeadings(text) {
44
+ const ids = [];
45
+ for (const line of text.split('\n')) {
46
+ const match = line.match(/^## (.+)$/);
47
+ if (match) ids.push(match[1].trim());
48
+ }
49
+ return ids;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Tag extraction
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Extract all hash-tags from a feedback line.
58
+ * Returns an array of strings like ['#validation', '#law:brevity'].
59
+ */
60
+ export function extractAllTags(line) {
61
+ return (line.match(/#[\w][\w:-]*/g) || []);
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Validation
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Validate all feedback tags in the Feedback section of WORK.md text.
70
+ *
71
+ * Returns an array of error strings. Empty array = all valid.
72
+ */
73
+ export function validateTags(workText, foundryDir) {
74
+ const lawIds = collectLawIds(foundryDir);
75
+ const errors = [];
76
+ let inFeedback = false;
77
+ let lineNum = 0;
78
+
79
+ for (const line of workText.split('\n')) {
80
+ lineNum++;
81
+ const stripped = line.trim();
82
+
83
+ if (stripped === '# Feedback') { inFeedback = true; continue; }
84
+ if (inFeedback && stripped.startsWith('# ') && stripped !== '# Feedback') {
85
+ inFeedback = false; continue;
86
+ }
87
+ if (!inFeedback || !(/^- \[/.test(stripped))) continue;
88
+
89
+ const tags = extractAllTags(stripped);
90
+ if (tags.length === 0) {
91
+ errors.push({ line: lineNum, message: 'Feedback item has no tag', raw: stripped });
92
+ continue;
93
+ }
94
+
95
+ for (const tag of tags) {
96
+ if (!VALID_TAG_RE.test(tag)) {
97
+ errors.push({ line: lineNum, message: `Unknown tag: ${tag}`, raw: stripped });
98
+ } else if (tag.startsWith('#law:')) {
99
+ const lawId = tag.slice(5);
100
+ if (!lawIds.has(lawId)) {
101
+ errors.push({ line: lineNum, message: `Law not found: ${lawId}`, raw: stripped });
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ return errors;
108
+ }