@really-knows-ai/foundry 2.3.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -198
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +393 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +42 -6
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +49 -21
- package/{skills → dist/skills}/add-cycle/SKILL.md +60 -14
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +39 -7
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -90
- package/docs/concepts.md +0 -59
- package/docs/getting-started.md +0 -78
- package/docs/work-spec.md +0 -193
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -105
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
package/scripts/lib/config.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured reads of foundry/ directory contents.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { parseFrontmatter } from './workfile.js';
|
|
7
|
-
|
|
8
|
-
function parseDoc(text) {
|
|
9
|
-
const frontmatter = parseFrontmatter(text);
|
|
10
|
-
const body = text.replace(/^---\n.+?\n---\n?/s, '').trim();
|
|
11
|
-
return { frontmatter, body };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function getCycleDefinition(foundryDir, cycleId, io) {
|
|
15
|
-
const path = join(foundryDir, 'cycles', `${cycleId}.md`);
|
|
16
|
-
if (!(await io.exists(path))) {
|
|
17
|
-
throw new Error(`Cycle not found: ${cycleId}`);
|
|
18
|
-
}
|
|
19
|
-
const text = await io.readFile(path);
|
|
20
|
-
return parseDoc(text);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function getArtefactType(foundryDir, typeId, io) {
|
|
24
|
-
const path = join(foundryDir, 'artefacts', typeId, 'definition.md');
|
|
25
|
-
if (!(await io.exists(path))) {
|
|
26
|
-
throw new Error(`Artefact type not found: ${typeId}`);
|
|
27
|
-
}
|
|
28
|
-
const text = await io.readFile(path);
|
|
29
|
-
return parseDoc(text);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function getLaws(foundryDir, typeId, io) {
|
|
33
|
-
// Handle optional typeId: if typeId is the io object, shift args
|
|
34
|
-
if (typeId && typeof typeId === 'object' && typeof typeId.exists === 'function') {
|
|
35
|
-
io = typeId;
|
|
36
|
-
typeId = null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const laws = [];
|
|
40
|
-
|
|
41
|
-
function parseLaws(text, source) {
|
|
42
|
-
const lines = text.split('\n');
|
|
43
|
-
let currentId = null;
|
|
44
|
-
let currentLines = [];
|
|
45
|
-
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
const heading = line.match(/^## (.+)/);
|
|
48
|
-
if (heading) {
|
|
49
|
-
if (currentId) {
|
|
50
|
-
laws.push({ id: currentId, text: currentLines.join('\n').trim(), source });
|
|
51
|
-
}
|
|
52
|
-
currentId = heading[1];
|
|
53
|
-
currentLines = [];
|
|
54
|
-
} else if (currentId) {
|
|
55
|
-
currentLines.push(line);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (currentId) {
|
|
59
|
-
laws.push({ id: currentId, text: currentLines.join('\n').trim(), source });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Global laws
|
|
64
|
-
const globalDir = join(foundryDir, 'laws');
|
|
65
|
-
if (await io.exists(globalDir)) {
|
|
66
|
-
const files = await io.readDir(globalDir);
|
|
67
|
-
const mdFiles = files.filter(f => f.endsWith('.md')).sort();
|
|
68
|
-
for (const file of mdFiles) {
|
|
69
|
-
const text = await io.readFile(join(globalDir, file));
|
|
70
|
-
parseLaws(text, `laws/${file}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Type-specific laws
|
|
75
|
-
if (typeId) {
|
|
76
|
-
const typeLawsPath = join(foundryDir, 'artefacts', typeId, 'laws.md');
|
|
77
|
-
if (await io.exists(typeLawsPath)) {
|
|
78
|
-
const text = await io.readFile(typeLawsPath);
|
|
79
|
-
parseLaws(text, `artefacts/${typeId}/laws.md`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return laws;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export async function getValidation(foundryDir, typeId, io) {
|
|
87
|
-
const path = join(foundryDir, 'artefacts', typeId, 'validation.md');
|
|
88
|
-
if (!(await io.exists(path))) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const text = await io.readFile(path);
|
|
92
|
-
const entries = [];
|
|
93
|
-
const lines = text.split('\n');
|
|
94
|
-
let currentId = null;
|
|
95
|
-
let currentCommand = null;
|
|
96
|
-
let currentFailure = null;
|
|
97
|
-
|
|
98
|
-
function flush() {
|
|
99
|
-
if (currentId && currentCommand) {
|
|
100
|
-
const entry = { id: currentId, command: currentCommand };
|
|
101
|
-
if (currentFailure) entry.failureMeans = currentFailure;
|
|
102
|
-
entries.push(entry);
|
|
103
|
-
}
|
|
104
|
-
currentId = null;
|
|
105
|
-
currentCommand = null;
|
|
106
|
-
currentFailure = null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
const heading = line.match(/^## (.+)/);
|
|
111
|
-
if (heading) {
|
|
112
|
-
flush();
|
|
113
|
-
currentId = heading[1].trim();
|
|
114
|
-
} else if (currentId) {
|
|
115
|
-
const cmdMatch = line.match(/^Command:\s*(.+)/);
|
|
116
|
-
const failMatch = line.match(/^Failure means:\s*(.+)/);
|
|
117
|
-
if (cmdMatch) currentCommand = cmdMatch[1].trim().replace(/^`|`$/g, '');
|
|
118
|
-
if (failMatch) currentFailure = failMatch[1].trim();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
flush();
|
|
122
|
-
return entries;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export async function getAppraisers(foundryDir, io) {
|
|
126
|
-
const dir = join(foundryDir, 'appraisers');
|
|
127
|
-
if (!(await io.exists(dir))) return [];
|
|
128
|
-
const files = await io.readDir(dir);
|
|
129
|
-
const mdFiles = files.filter(f => f.endsWith('.md')).sort();
|
|
130
|
-
const result = [];
|
|
131
|
-
for (const file of mdFiles) {
|
|
132
|
-
const text = await io.readFile(join(dir, file));
|
|
133
|
-
const { frontmatter, body } = parseDoc(text);
|
|
134
|
-
const entry = { id: frontmatter.id, personality: body };
|
|
135
|
-
if (frontmatter.model) entry.model = frontmatter.model;
|
|
136
|
-
result.push(entry);
|
|
137
|
-
}
|
|
138
|
-
return result;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function getFlow(foundryDir, flowId, io) {
|
|
142
|
-
const path = join(foundryDir, 'flows', `${flowId}.md`);
|
|
143
|
-
if (!(await io.exists(path))) {
|
|
144
|
-
throw new Error(`Flow not found: ${flowId}`);
|
|
145
|
-
}
|
|
146
|
-
const text = await io.readFile(path);
|
|
147
|
-
return parseDoc(text);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function selectAppraisers(foundryDir, typeId, countOverride, io) {
|
|
151
|
-
// Handle optional countOverride
|
|
152
|
-
if (countOverride && typeof countOverride === 'object' && typeof countOverride.exists === 'function') {
|
|
153
|
-
io = countOverride;
|
|
154
|
-
countOverride = null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const { frontmatter } = await getArtefactType(foundryDir, typeId, io);
|
|
158
|
-
const appraiserConfig = frontmatter.appraisers || {};
|
|
159
|
-
const count = countOverride || appraiserConfig.count || 3;
|
|
160
|
-
const allowed = appraiserConfig.allowed || null;
|
|
161
|
-
|
|
162
|
-
const allAppraisers = await getAppraisers(foundryDir, io);
|
|
163
|
-
let pool = allowed
|
|
164
|
-
? allAppraisers.filter(a => allowed.includes(a.id))
|
|
165
|
-
: allAppraisers;
|
|
166
|
-
|
|
167
|
-
if (pool.length === 0) return [];
|
|
168
|
-
|
|
169
|
-
// Round-robin distribute
|
|
170
|
-
const result = [];
|
|
171
|
-
for (let i = 0; i < count; i++) {
|
|
172
|
-
result.push(pool[i % pool.length]);
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
// Matrix: [current][target] => set of allowed stageBases
|
|
4
|
-
const MATRIX = {
|
|
5
|
-
open: { actioned: ['forge'], 'wont-fix': ['forge'] },
|
|
6
|
-
actioned: { approved: ['quench', 'appraise', 'human-appraise'], rejected: ['quench', 'appraise', 'human-appraise'] },
|
|
7
|
-
'wont-fix': { approved: ['appraise', 'human-appraise'], rejected: ['appraise', 'human-appraise'] },
|
|
8
|
-
rejected: { actioned: ['forge'], 'wont-fix': ['forge'] },
|
|
9
|
-
approved: {}, // terminal
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function validateTransition(current, target, stageBase) {
|
|
13
|
-
const row = MATRIX[current];
|
|
14
|
-
if (!row) return { ok: false, reason: `unknown state: ${current}` };
|
|
15
|
-
const allowedStages = row[target];
|
|
16
|
-
if (!allowedStages) return { ok: false, reason: `invalid transition ${current} → ${target}` };
|
|
17
|
-
if (!allowedStages.includes(stageBase)) {
|
|
18
|
-
return { ok: false, reason: `stage ${stageBase} cannot transition ${current} → ${target}` };
|
|
19
|
-
}
|
|
20
|
-
return { ok: true };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function hashText(text) {
|
|
24
|
-
return createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
25
|
-
}
|
package/scripts/lib/feedback.js
DELETED
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feedback parsing and manipulation utilities for WORK.md.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { extractAllTags } from './tags.js';
|
|
6
|
-
import { validateTransition, hashText } from './feedback-transitions.js';
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Parsing
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
export function parseFeedbackItem(line) {
|
|
13
|
-
const item = { raw: line, state: 'unknown', tags: [], resolved: false };
|
|
14
|
-
|
|
15
|
-
if (line.startsWith('- [ ]')) {
|
|
16
|
-
item.state = 'open';
|
|
17
|
-
} else if (line.startsWith('- [x]')) {
|
|
18
|
-
item.state = 'actioned';
|
|
19
|
-
} else if (line.startsWith('- [~]')) {
|
|
20
|
-
item.state = 'wont-fix';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (line.includes('| approved')) {
|
|
24
|
-
item.resolved = true;
|
|
25
|
-
} else if (line.includes('| rejected')) {
|
|
26
|
-
item.state = 'rejected';
|
|
27
|
-
item.resolved = false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
item.tags = extractAllTags(line);
|
|
31
|
-
|
|
32
|
-
return item;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function parseFeedback(text, cycle, artefacts) {
|
|
36
|
-
const cycleFiles = new Set();
|
|
37
|
-
for (const art of artefacts) {
|
|
38
|
-
if (art.cycle === cycle) {
|
|
39
|
-
cycleFiles.add(art.file || '');
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const filterByFile = cycleFiles.size > 0;
|
|
43
|
-
|
|
44
|
-
const items = [];
|
|
45
|
-
let currentFile = null;
|
|
46
|
-
let inFeedback = false;
|
|
47
|
-
let feedbackLevel = 0;
|
|
48
|
-
|
|
49
|
-
for (const line of text.split('\n')) {
|
|
50
|
-
const stripped = line.trim();
|
|
51
|
-
|
|
52
|
-
if (stripped === '# Feedback' || stripped === '## Feedback') {
|
|
53
|
-
inFeedback = true;
|
|
54
|
-
feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Exit feedback on a heading at the same or higher level
|
|
59
|
-
if (inFeedback && /^#{1,2} /.test(stripped)) {
|
|
60
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
61
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
62
|
-
inFeedback = false;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!inFeedback) continue;
|
|
68
|
-
|
|
69
|
-
// File sub-headings are one level below the Feedback heading
|
|
70
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
71
|
-
if (stripped.startsWith(fileHeadingPrefix)) {
|
|
72
|
-
currentFile = stripped.slice(fileHeadingPrefix.length).trim();
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if ((!filterByFile || cycleFiles.has(currentFile)) && /^- \[/.test(stripped)) {
|
|
77
|
-
items.push(parseFeedbackItem(stripped));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return items;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Manipulation
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
|
-
export function addFeedbackItem(text, file, itemText, tag) {
|
|
89
|
-
// Dedup by (file, tag, text hash): if any existing item under this file
|
|
90
|
-
// heading has the same tag and the same itemText, return without mutating.
|
|
91
|
-
const existing = collectItemsForFile(text, file);
|
|
92
|
-
const h = hashText(itemText);
|
|
93
|
-
for (const ex of existing) {
|
|
94
|
-
if (ex.tags.includes(`#${tag}`) && hashText(ex.coreText) === h) {
|
|
95
|
-
return { text, deduped: true };
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const newItem = `- [ ] ${itemText} #${tag}`;
|
|
100
|
-
const lines = text.split('\n');
|
|
101
|
-
|
|
102
|
-
// Find ## Feedback section
|
|
103
|
-
let feedbackIdx = -1;
|
|
104
|
-
let feedbackLevel = 0;
|
|
105
|
-
for (let i = 0; i < lines.length; i++) {
|
|
106
|
-
const stripped = lines[i].trim();
|
|
107
|
-
if (stripped === '## Feedback') {
|
|
108
|
-
feedbackIdx = i;
|
|
109
|
-
feedbackLevel = 2;
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
if (stripped === '# Feedback') {
|
|
113
|
-
feedbackIdx = i;
|
|
114
|
-
feedbackLevel = 1;
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
120
|
-
const fileHeading = `${fileHeadingPrefix}${file}`;
|
|
121
|
-
|
|
122
|
-
if (feedbackIdx === -1) {
|
|
123
|
-
// No Feedback section — append one
|
|
124
|
-
lines.push('', '## Feedback', '', `### ${file}`, newItem);
|
|
125
|
-
return { text: lines.join('\n'), deduped: false };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Find the file heading within the feedback section
|
|
129
|
-
let fileIdx = -1;
|
|
130
|
-
let sectionEnd = lines.length; // end of feedback section
|
|
131
|
-
for (let i = feedbackIdx + 1; i < lines.length; i++) {
|
|
132
|
-
const stripped = lines[i].trim();
|
|
133
|
-
// Check if we've left the feedback section
|
|
134
|
-
if (/^#{1,2} /.test(stripped)) {
|
|
135
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
136
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
137
|
-
sectionEnd = i;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (stripped === fileHeading.trim()) {
|
|
142
|
-
fileIdx = i;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (fileIdx === -1) {
|
|
147
|
-
// File heading doesn't exist — add it before section end
|
|
148
|
-
lines.splice(sectionEnd, 0, '', fileHeading, newItem);
|
|
149
|
-
return { text: lines.join('\n'), deduped: false };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Find last item under this file heading
|
|
153
|
-
let insertIdx = fileIdx + 1;
|
|
154
|
-
for (let i = fileIdx + 1; i < sectionEnd; i++) {
|
|
155
|
-
const stripped = lines[i].trim();
|
|
156
|
-
if (stripped.startsWith(fileHeadingPrefix)) break; // next file heading
|
|
157
|
-
if (/^- \[/.test(stripped)) {
|
|
158
|
-
insertIdx = i + 1;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
lines.splice(insertIdx, 0, newItem);
|
|
163
|
-
return { text: lines.join('\n'), deduped: false };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function actionFeedbackItem(text, file, index, stageBase) {
|
|
167
|
-
return transformFeedbackItemWithValidation(text, file, index, 'actioned', stageBase, (line) =>
|
|
168
|
-
line.replace('- [ ]', '- [x]')
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function wontfixFeedbackItem(text, file, index, reason, stageBase) {
|
|
173
|
-
return transformFeedbackItemWithValidation(text, file, index, 'wont-fix', stageBase, (line) =>
|
|
174
|
-
line.replace('- [ ]', '- [~]') + ` | wont-fix: ${reason}`
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function resolveFeedbackItem(text, file, index, resolution, reason, stageBase) {
|
|
179
|
-
return transformFeedbackItemWithValidation(text, file, index, resolution, stageBase, (line) => {
|
|
180
|
-
if (resolution === 'approved') {
|
|
181
|
-
return line + ' | approved';
|
|
182
|
-
}
|
|
183
|
-
return line + ` | rejected: ${reason}`;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// Listing
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
export function listFeedback(text, cycle, artefacts, filterFile) {
|
|
192
|
-
const cycleFiles = new Set();
|
|
193
|
-
for (const art of artefacts) {
|
|
194
|
-
if (art.cycle === cycle) {
|
|
195
|
-
cycleFiles.add(art.file || '');
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const filterByCycle = cycleFiles.size > 0;
|
|
199
|
-
|
|
200
|
-
const results = [];
|
|
201
|
-
let currentFile = null;
|
|
202
|
-
let fileIndex = 0;
|
|
203
|
-
let inFeedback = false;
|
|
204
|
-
let feedbackLevel = 0;
|
|
205
|
-
|
|
206
|
-
for (const line of text.split('\n')) {
|
|
207
|
-
const stripped = line.trim();
|
|
208
|
-
|
|
209
|
-
if (stripped === '# Feedback' || stripped === '## Feedback') {
|
|
210
|
-
inFeedback = true;
|
|
211
|
-
feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (inFeedback && /^#{1,2} /.test(stripped)) {
|
|
216
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
217
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
218
|
-
inFeedback = false;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!inFeedback) continue;
|
|
224
|
-
|
|
225
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
226
|
-
if (stripped.startsWith(fileHeadingPrefix)) {
|
|
227
|
-
currentFile = stripped.slice(fileHeadingPrefix.length).trim();
|
|
228
|
-
fileIndex = 0;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if ((!filterByCycle || cycleFiles.has(currentFile)) && /^- \[/.test(stripped)) {
|
|
233
|
-
if (!filterFile || filterFile === currentFile) {
|
|
234
|
-
const item = parseFeedbackItem(stripped);
|
|
235
|
-
results.push({
|
|
236
|
-
file: currentFile,
|
|
237
|
-
index: fileIndex,
|
|
238
|
-
text: item.raw,
|
|
239
|
-
state: item.state,
|
|
240
|
-
tags: item.tags,
|
|
241
|
-
resolved: item.resolved,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
fileIndex++;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return results;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Detect feedback items stuck in a deadlock — rejected N or more times.
|
|
253
|
-
* A deadlock occurs when forge-appraise cycles keep rejecting the same item.
|
|
254
|
-
*/
|
|
255
|
-
export function detectDeadlocks(feedback, history, threshold = 3) {
|
|
256
|
-
// Count forge→appraise cycles (each pair = one iteration)
|
|
257
|
-
const forgeAppraiseCount = history.filter(
|
|
258
|
-
e => (e.stage || '').split(':')[0] === 'appraise'
|
|
259
|
-
).length;
|
|
260
|
-
|
|
261
|
-
if (forgeAppraiseCount < threshold) return [];
|
|
262
|
-
|
|
263
|
-
// Items that are still rejected after threshold iterations are deadlocked
|
|
264
|
-
return feedback.filter(f => f.state === 'rejected' || f.state === 'open');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
// Internal helpers
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Collect feedback items under a specific file heading, returning the parsed
|
|
273
|
-
* representation plus the "core text" (item body with tag and trailing resolution
|
|
274
|
-
* stripped) for dedup hashing.
|
|
275
|
-
*/
|
|
276
|
-
function collectItemsForFile(text, file) {
|
|
277
|
-
const items = [];
|
|
278
|
-
const lines = text.split('\n');
|
|
279
|
-
let inFeedback = false;
|
|
280
|
-
let feedbackLevel = 0;
|
|
281
|
-
let currentFile = null;
|
|
282
|
-
|
|
283
|
-
for (const line of lines) {
|
|
284
|
-
const stripped = line.trim();
|
|
285
|
-
|
|
286
|
-
if (stripped === '# Feedback' || stripped === '## Feedback') {
|
|
287
|
-
inFeedback = true;
|
|
288
|
-
feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (inFeedback && /^#{1,2} /.test(stripped)) {
|
|
293
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
294
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
295
|
-
inFeedback = false;
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (!inFeedback) continue;
|
|
301
|
-
|
|
302
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
303
|
-
if (stripped.startsWith(fileHeadingPrefix)) {
|
|
304
|
-
currentFile = stripped.slice(fileHeadingPrefix.length).trim();
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (currentFile === file && /^- \[/.test(stripped)) {
|
|
309
|
-
const parsed = parseFeedbackItem(stripped);
|
|
310
|
-
// Strip checkbox, tags, and trailing `| approved` / `| rejected: ...` /
|
|
311
|
-
// `| wont-fix: ...` to get the core author-supplied text for dedup.
|
|
312
|
-
let core = stripped.replace(/^- \[[ x~]\]\s*/, '');
|
|
313
|
-
core = core.replace(/\s*\|\s*(approved|rejected[^|]*|wont-fix[^|]*)\s*$/, '');
|
|
314
|
-
for (const t of parsed.tags) {
|
|
315
|
-
core = core.replace(t, '');
|
|
316
|
-
}
|
|
317
|
-
core = core.trim();
|
|
318
|
-
items.push({ line: stripped, state: parsed.state, tags: parsed.tags, coreText: core });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return items;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Read the line at (file, index) and return its current feedback state
|
|
327
|
-
* (or null if not found).
|
|
328
|
-
*/
|
|
329
|
-
function readItemState(text, file, index) {
|
|
330
|
-
const lines = text.split('\n');
|
|
331
|
-
let inFeedback = false;
|
|
332
|
-
let feedbackLevel = 0;
|
|
333
|
-
let currentFile = null;
|
|
334
|
-
let fileIndex = 0;
|
|
335
|
-
|
|
336
|
-
for (let i = 0; i < lines.length; i++) {
|
|
337
|
-
const stripped = lines[i].trim();
|
|
338
|
-
|
|
339
|
-
if (stripped === '# Feedback' || stripped === '## Feedback') {
|
|
340
|
-
inFeedback = true;
|
|
341
|
-
feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (inFeedback && /^#{1,2} /.test(stripped)) {
|
|
346
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
347
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
348
|
-
inFeedback = false;
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (!inFeedback) continue;
|
|
354
|
-
|
|
355
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
356
|
-
if (stripped.startsWith(fileHeadingPrefix)) {
|
|
357
|
-
currentFile = stripped.slice(fileHeadingPrefix.length).trim();
|
|
358
|
-
fileIndex = 0;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (currentFile === file && /^- \[/.test(stripped)) {
|
|
363
|
-
if (fileIndex === index) {
|
|
364
|
-
const parsed = parseFeedbackItem(stripped);
|
|
365
|
-
// Map parseFeedbackItem's (state, resolved) pair onto state-machine states:
|
|
366
|
-
// - `| approved` → terminal "approved"
|
|
367
|
-
// - `| rejected` → "rejected" (parseFeedbackItem already sets this)
|
|
368
|
-
// - bare `[x]` → "actioned"
|
|
369
|
-
// - bare `[~]` → "wont-fix"
|
|
370
|
-
// - bare `[ ]` → "open"
|
|
371
|
-
if (parsed.resolved) return 'approved';
|
|
372
|
-
return parsed.state;
|
|
373
|
-
}
|
|
374
|
-
fileIndex++;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function transformFeedbackItemWithValidation(text, file, index, target, stageBase, transform) {
|
|
381
|
-
if (stageBase !== undefined) {
|
|
382
|
-
const current = readItemState(text, file, index);
|
|
383
|
-
if (!current) {
|
|
384
|
-
return { ok: false, error: `feedback item not found: file=${file} index=${index}` };
|
|
385
|
-
}
|
|
386
|
-
const v = validateTransition(current, target, stageBase);
|
|
387
|
-
if (!v.ok) {
|
|
388
|
-
return { ok: false, error: v.reason };
|
|
389
|
-
}
|
|
390
|
-
const updated = transformFeedbackItem(text, file, index, transform);
|
|
391
|
-
return { ok: true, text: updated };
|
|
392
|
-
}
|
|
393
|
-
// Backward-compatible path: return plain string.
|
|
394
|
-
return transformFeedbackItem(text, file, index, transform);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function transformFeedbackItem(text, file, index, transform) {
|
|
398
|
-
const lines = text.split('\n');
|
|
399
|
-
let inFeedback = false;
|
|
400
|
-
let feedbackLevel = 0;
|
|
401
|
-
let currentFile = null;
|
|
402
|
-
let fileIndex = 0;
|
|
403
|
-
|
|
404
|
-
for (let i = 0; i < lines.length; i++) {
|
|
405
|
-
const stripped = lines[i].trim();
|
|
406
|
-
|
|
407
|
-
if (stripped === '# Feedback' || stripped === '## Feedback') {
|
|
408
|
-
inFeedback = true;
|
|
409
|
-
feedbackLevel = stripped.startsWith('## ') ? 2 : 1;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (inFeedback && /^#{1,2} /.test(stripped)) {
|
|
414
|
-
const level = stripped.startsWith('## ') ? 2 : 1;
|
|
415
|
-
if (level <= feedbackLevel && stripped !== '# Feedback' && stripped !== '## Feedback') {
|
|
416
|
-
inFeedback = false;
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (!inFeedback) continue;
|
|
422
|
-
|
|
423
|
-
const fileHeadingPrefix = feedbackLevel === 1 ? '## ' : '### ';
|
|
424
|
-
if (stripped.startsWith(fileHeadingPrefix)) {
|
|
425
|
-
currentFile = stripped.slice(fileHeadingPrefix.length).trim();
|
|
426
|
-
fileIndex = 0;
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (currentFile === file && /^- \[/.test(stripped)) {
|
|
431
|
-
if (fileIndex === index) {
|
|
432
|
-
lines[i] = transform(lines[i]);
|
|
433
|
-
return lines.join('\n');
|
|
434
|
-
}
|
|
435
|
-
fileIndex++;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return text;
|
|
440
|
-
}
|
package/scripts/lib/finalize.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// scripts/lib/finalize.js
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { minimatch } from 'minimatch';
|
|
4
|
-
|
|
5
|
-
const TOOL_MANAGED = [
|
|
6
|
-
'WORK.md',
|
|
7
|
-
'WORK.history.yaml',
|
|
8
|
-
];
|
|
9
|
-
const TOOL_MANAGED_PREFIX = ['.foundry/'];
|
|
10
|
-
|
|
11
|
-
function changedFiles(cwd, baseSha) {
|
|
12
|
-
const tracked = execSync(`git diff --name-only ${baseSha} HEAD`, { cwd }).toString().split('\n').filter(Boolean);
|
|
13
|
-
const diffUnstaged = execSync('git diff --name-only', { cwd }).toString().split('\n').filter(Boolean);
|
|
14
|
-
const untracked = execSync('git ls-files --others --exclude-standard', { cwd }).toString().split('\n').filter(Boolean);
|
|
15
|
-
return [...new Set([...tracked, ...diffUnstaged, ...untracked])];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isToolManaged(f) {
|
|
19
|
-
if (TOOL_MANAGED.includes(f)) return true;
|
|
20
|
-
return TOOL_MANAGED_PREFIX.some(p => f.startsWith(p));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function finalizeStage({ cwd, baseSha, stageBase, cycleDef, artefactTypes, registerArtefact }) {
|
|
24
|
-
const files = changedFiles(cwd, baseSha).filter(f => !isToolManaged(f));
|
|
25
|
-
const allowedPatterns = stageBase === 'forge'
|
|
26
|
-
? (artefactTypes[cycleDef.outputArtefactType]?.filePatterns ?? [])
|
|
27
|
-
: [];
|
|
28
|
-
const unexpected = [];
|
|
29
|
-
const matched = [];
|
|
30
|
-
for (const f of files) {
|
|
31
|
-
const hit = allowedPatterns.find(p => minimatch(f, p));
|
|
32
|
-
if (hit) matched.push(f);
|
|
33
|
-
else unexpected.push(f);
|
|
34
|
-
}
|
|
35
|
-
if (unexpected.length) return { ok: false, error: 'unexpected_files', files: unexpected };
|
|
36
|
-
const artefacts = matched.map(file => {
|
|
37
|
-
registerArtefact({ file, type: cycleDef.outputArtefactType, status: 'draft' });
|
|
38
|
-
return { file, type: cycleDef.outputArtefactType, status: 'draft' };
|
|
39
|
-
});
|
|
40
|
-
return { ok: true, artefacts };
|
|
41
|
-
}
|