@really-knows-ai/foundry 3.4.0 → 3.5.2
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/dist/.opencode/plugins/foundry-tools/artefact-tools.js +32 -43
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +9 -2
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +2 -2
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +0 -13
- package/dist/CHANGELOG.md +29 -0
- package/dist/docs/architecture.md +1 -1
- package/dist/docs/tools.md +5 -28
- package/dist/docs/work-spec.md +2 -2
- package/dist/scripts/appraise-module.js +17 -5
- package/dist/scripts/lib/artefacts.js +134 -131
- package/dist/scripts/lib/attestation/attest.js +10 -18
- package/dist/scripts/lib/attestation/payload.js +36 -10
- package/dist/scripts/lib/finalize.js +5 -5
- package/dist/scripts/lib/validation.js +20 -6
- package/dist/scripts/lib/workfile.js +0 -3
- package/dist/scripts/orchestrate-cycle.js +5 -34
- package/dist/scripts/orchestrate-phases.js +63 -40
- package/dist/scripts/orchestrate.js +4 -5
- package/dist/scripts/quench-module.js +29 -20
- package/dist/scripts/sort.js +0 -1
- package/dist/skills/add-cycle/SKILL.md +6 -2
- package/dist/skills/add-flow/SKILL.md +5 -2
- package/dist/skills/appraise/SKILL.md +3 -3
- package/dist/skills/human-appraise/SKILL.md +6 -7
- package/dist/skills/orchestrate/SKILL.md +4 -5
- package/dist/skills/quench/SKILL.md +4 -4
- package/package.json +1 -1
|
@@ -1,58 +1,47 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { readFileSync,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { makeIO
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { getArtefactFiles } from '../../../scripts/lib/artefacts.js';
|
|
4
|
+
import { getCycleDefinition } from '../../../scripts/lib/config.js';
|
|
5
|
+
import { parseFrontmatter } from '../../../scripts/lib/workfile.js';
|
|
6
|
+
import { makeIO } from './helpers.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function readWorkCycleId(worktree) {
|
|
9
|
+
const workPath = path.join(worktree, 'WORK.md');
|
|
10
|
+
if (!existsSync(workPath)) throw new Error('WORK.md not found');
|
|
11
|
+
const frontmatter = parseFrontmatter(readFileSync(workPath, 'utf-8'));
|
|
12
|
+
if (!frontmatter.cycle) throw new Error('current cycle not found in WORK.md frontmatter');
|
|
13
|
+
return frontmatter.cycle;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readOutputType(foundryDir, cycleId, io) {
|
|
17
|
+
let cd;
|
|
18
|
+
try {
|
|
19
|
+
cd = await getCycleDefinition(foundryDir, cycleId, io);
|
|
20
|
+
} catch { return null; }
|
|
21
|
+
return cd.frontmatter && cd.frontmatter['output-type'];
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
function makeListTool(tool) {
|
|
11
25
|
return tool({
|
|
12
|
-
description: 'List
|
|
13
|
-
args: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return JSON.stringify(
|
|
26
|
+
description: 'List artefact changes on the current work branch for the current cycle. Returns [{ file, state }] entries.',
|
|
27
|
+
args: {},
|
|
28
|
+
async execute(_args, context) {
|
|
29
|
+
try {
|
|
30
|
+
const cycleId = readWorkCycleId(context.worktree);
|
|
31
|
+
const io = makeIO(context.worktree);
|
|
32
|
+
const outputType = await readOutputType('foundry', cycleId, io);
|
|
33
|
+
if (!outputType) return JSON.stringify([]);
|
|
34
|
+
const artefacts = await getArtefactFiles('foundry', outputType, io, { baseBranch: 'main' });
|
|
35
|
+
return JSON.stringify(artefacts);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return JSON.stringify({ error: err.message });
|
|
20
38
|
}
|
|
21
|
-
const text = readFileSync(workPath, 'utf-8');
|
|
22
|
-
const rows = parseArtefactsTable(text);
|
|
23
|
-
const filtered = args.cycle ? rows.filter(r => r.cycle === args.cycle) : rows;
|
|
24
|
-
return JSON.stringify(filtered);
|
|
25
39
|
},
|
|
26
40
|
});
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
export function createArtefactTools({ tool }) {
|
|
30
44
|
return {
|
|
31
|
-
// NOTE: `foundry_artefacts_add` was removed in v2.2.0. Artefacts are now
|
|
32
|
-
// registered automatically by the orchestrator's internal finalize step as drafts,
|
|
33
|
-
// then promoted to done|blocked via `foundry_artefacts_set_status`.
|
|
34
|
-
foundry_artefacts_set_status: tool({
|
|
35
|
-
description: 'Update the status of an artefact in WORK.md (done|blocked only)',
|
|
36
|
-
args: {
|
|
37
|
-
file: tool.schema.string().describe('Artefact file path'),
|
|
38
|
-
status: tool.schema.string().describe('New status (done|blocked)'),
|
|
39
|
-
},
|
|
40
|
-
execute: guarded('foundry_artefacts_set_status', [flowBranchGuard, gateNotFailed], async (args, context) => {
|
|
41
|
-
const io = makeIO(context.worktree);
|
|
42
|
-
const guard = requireNoActiveStage(io);
|
|
43
|
-
if (!guard.ok) return JSON.stringify({ error: `foundry_artefacts_set_status ${guard.error}` });
|
|
44
|
-
const workPath = path.join(context.worktree, 'WORK.md');
|
|
45
|
-
const text = readFileSync(workPath, 'utf-8');
|
|
46
|
-
try {
|
|
47
|
-
const updated = setArtefactStatus(text, args.file, args.status);
|
|
48
|
-
writeFileSync(workPath, updated, 'utf-8');
|
|
49
|
-
return JSON.stringify({ ok: true });
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return JSON.stringify({ error: e.message });
|
|
52
|
-
}
|
|
53
|
-
}, { branchIo: branchIoFactory, io: asyncIoFactory }),
|
|
54
|
-
}),
|
|
55
|
-
|
|
56
45
|
foundry_artefacts_list: makeListTool(tool),
|
|
57
46
|
};
|
|
58
47
|
}
|
|
@@ -131,11 +131,18 @@ function commitAttestation(cwd, cycle, content, opts) {
|
|
|
131
131
|
function buildAttestationInputs(opts) {
|
|
132
132
|
return {
|
|
133
133
|
cwd: opts.cwd,
|
|
134
|
+
foundryDir: 'foundry',
|
|
134
135
|
baseBranch: opts.baseBranch,
|
|
136
|
+
branchBaseSha: opts.branchBaseSha,
|
|
135
137
|
goalText: opts.goalText,
|
|
136
138
|
archiveBranch: opts.archiveBranch,
|
|
137
139
|
archiveTipSha: opts.archiveTipSha,
|
|
138
|
-
io: {
|
|
140
|
+
io: {
|
|
141
|
+
readFile: (p) => readFileSync(p, 'utf8'),
|
|
142
|
+
fileExists: (p) => existsSync(p),
|
|
143
|
+
exists: (p) => existsSync(p),
|
|
144
|
+
exec: (args) => execFileSync(args[0], args.slice(1), { cwd: opts.cwd, encoding: 'utf8' }),
|
|
145
|
+
},
|
|
139
146
|
execGit: opts.execGit,
|
|
140
147
|
};
|
|
141
148
|
}
|
|
@@ -151,7 +158,7 @@ function createAttestTool(tool) {
|
|
|
151
158
|
return tool({
|
|
152
159
|
description:
|
|
153
160
|
'Verify the current work cycle is complete (all required stages ran, no unresolved ' +
|
|
154
|
-
'feedback
|
|
161
|
+
'feedback) and commit an ATTEST.md attestation file to the work branch. ' +
|
|
155
162
|
'foundry_git_finish will not merge without this commit at HEAD.',
|
|
156
163
|
args: {
|
|
157
164
|
baseBranch: tool.schema.string().optional()
|
|
@@ -160,7 +160,7 @@ function cycleArgs(s) { return {
|
|
|
160
160
|
inputs: s.object({
|
|
161
161
|
type: s.enum(['any-of', 'all-of']).describe('Contract type: any-of (at least one) or all-of (all must exist)'),
|
|
162
162
|
artefacts: s.array(s.string()).describe('Artefact type IDs this cycle reads'),
|
|
163
|
-
}).optional().describe('Input contract for this cycle'),
|
|
163
|
+
}).optional().describe('Input contract for this cycle. Omit for source cycles that start from the user goal; empty artefacts arrays are invalid.'),
|
|
164
164
|
targets: s.array(s.string()).optional().describe('Downstream cycle IDs this cycle can route to'),
|
|
165
165
|
humanAppraise: s.boolean().optional().describe('Include human-appraise in every iteration'),
|
|
166
166
|
deadlockAppraise: s.boolean().optional().describe('Route to human-appraise on LLM appraiser deadlock'),
|
|
@@ -173,7 +173,7 @@ function cycleArgs(s) { return {
|
|
|
173
173
|
read: s.array(s.string()).describe('Memory store keys this cycle can read'),
|
|
174
174
|
write: s.array(s.string()).describe('Memory store keys this cycle can write'),
|
|
175
175
|
}).optional().describe('Flow memory permissions'),
|
|
176
|
-
models: s.object({}).optional().describe('Per-stage model overrides (e.g. { forge: "
|
|
176
|
+
models: s.object({}).optional().describe('Per-stage model overrides (e.g. { forge: "opencode-go/deepseek-v4-flash", appraise: "opencode-go/qwen3.6-plus" }). Preserve user-selected stage models.'),
|
|
177
177
|
description: s.string().optional().describe('Prose description placed after frontmatter'),
|
|
178
178
|
}; }
|
|
179
179
|
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
1
|
import { execFileSync } from 'child_process';
|
|
3
2
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
5
3
|
import { signToken } from '../../../scripts/lib/token.js';
|
|
6
4
|
import { readOrCreateSecret } from '../../../scripts/lib/secret.js';
|
|
7
5
|
import { getCycleDefinition, getArtefactType } from '../../../scripts/lib/config.js';
|
|
8
|
-
import { addArtefactRow } from '../../../scripts/lib/artefacts.js';
|
|
9
6
|
import { stageBaseOf } from '../../../scripts/lib/stage-guard.js';
|
|
10
7
|
import { finalizeStage } from '../../../scripts/lib/finalize.js';
|
|
11
8
|
import { commitWithPolicy } from '../../../scripts/lib/git-bridge.js';
|
|
@@ -40,15 +37,6 @@ function createGitBridge(cwd) {
|
|
|
40
37
|
};
|
|
41
38
|
}
|
|
42
39
|
|
|
43
|
-
function makeRegisterArtefact(cwd, cycleId) {
|
|
44
|
-
const workPath = path.join(cwd, 'WORK.md');
|
|
45
|
-
return ({ file, type, status }) => {
|
|
46
|
-
const text = readFileSync(workPath, 'utf-8');
|
|
47
|
-
const updated = addArtefactRow(text, { file, type, cycle: cycleId, status });
|
|
48
|
-
writeFileSync(workPath, updated, 'utf-8');
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
40
|
async function createFinalize(cwd, io) {
|
|
53
41
|
return async ({ cycleId, stage, baseSha }) => {
|
|
54
42
|
let cycleDoc;
|
|
@@ -78,7 +66,6 @@ async function createFinalize(cwd, io) {
|
|
|
78
66
|
cycleDef,
|
|
79
67
|
artefactTypes,
|
|
80
68
|
io,
|
|
81
|
-
registerArtefact: makeRegisterArtefact(cwd, cycleId),
|
|
82
69
|
});
|
|
83
70
|
return result;
|
|
84
71
|
};
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.5.2] - 2026-05-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Guide agents to omit empty `inputs` for source cycles and preserve stage-specific model overrides when creating cycles.
|
|
8
|
+
|
|
9
|
+
## [3.5.1] - 2026-05-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Run CI against the active Node release lines: 22.x, 24.x, 25.x, and 26.x.
|
|
14
|
+
- Use Node 22-compatible `mock.module({ namedExports })` test mocks.
|
|
15
|
+
|
|
16
|
+
## [3.5.0] - 2026-05-22
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Add branch-based artefact discovery with `getArtefactFiles` and git change-state tracking.
|
|
21
|
+
- Remove the `WORK.md` artefact table, artefact registration side effects, and per-artefact status updates.
|
|
22
|
+
- Update quench, appraise, orchestration, plugin tools, and attestation to use branch artefact discovery.
|
|
23
|
+
- Remove `foundry_artefacts_set_status` and update `foundry_artefacts_list` to return `{file, state}` entries.
|
|
24
|
+
- Remove artefact table generation from new `WORK.md` files.
|
|
25
|
+
- Update tests and docs for branch artefact discovery.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- Include all non-deleted artefact files in missing-model violation payloads.
|
|
30
|
+
- Thread base-branch selection through artefact discovery contexts.
|
|
31
|
+
|
|
3
32
|
## [3.4.0] - 2026-05-20
|
|
4
33
|
|
|
5
34
|
### Changed
|
|
@@ -238,7 +238,7 @@ Input artefacts (files matching an input type's `file-patterns`) are read-only.
|
|
|
238
238
|
When an unrecoverable error occurs (e.g. assay extractor abort, invalid JSONL, or memory-sync failure), the orchestrator marks `WORK.md` frontmatter with `status: failed` and a `reason`. The flow is then locked:
|
|
239
239
|
|
|
240
240
|
- **Blocked tools.** All mutation tools refuse to run and return an error referencing the failure reason:
|
|
241
|
-
- **Lifecycle:** `foundry_stage_begin`, `foundry_orchestrate`, `foundry_workfile_create
|
|
241
|
+
- **Lifecycle:** `foundry_stage_begin`, `foundry_orchestrate`, `foundry_workfile_create`
|
|
242
242
|
- **Stage work:** `foundry_assay_run`, `foundry_validate_run`
|
|
243
243
|
- **Feedback writes:** `foundry_feedback_add`, `foundry_feedback_action`, `foundry_feedback_wontfix`, `foundry_feedback_resolve` (`foundry_feedback_list` remains callable)
|
|
244
244
|
- **Appraiser selection:** `foundry_appraisers_select`
|
package/dist/docs/tools.md
CHANGED
|
@@ -59,7 +59,6 @@ state machine, see [`docs/concepts.md`](./concepts.md) and
|
|
|
59
59
|
|
|
60
60
|
**Artefacts**
|
|
61
61
|
- [`foundry_artefacts_list`](#foundry_artefacts_list)
|
|
62
|
-
- [`foundry_artefacts_set_status`](#foundry_artefacts_set_status)
|
|
63
62
|
|
|
64
63
|
**Feedback**
|
|
65
64
|
- [`foundry_feedback_add`](#foundry_feedback_add)
|
|
@@ -326,41 +325,19 @@ flow** (escape hatch).
|
|
|
326
325
|
|
|
327
326
|
### `foundry_artefacts_list`
|
|
328
327
|
|
|
329
|
-
> List
|
|
330
|
-
> callers should always pass the current cycle to avoid stale rows.
|
|
328
|
+
> List artefact changes on the current work branch for the current cycle.
|
|
331
329
|
|
|
332
330
|
**Args:**
|
|
333
|
-
-
|
|
331
|
+
- none.
|
|
334
332
|
|
|
335
|
-
**Returns:** array of `{file,
|
|
336
|
-
|
|
333
|
+
**Returns:** array of `{file, state}` entries. `{error: "WORK.md not found"}`
|
|
334
|
+
if `WORK.md` is missing, or `{error: "current cycle not found in WORK.md frontmatter"}`
|
|
335
|
+
if `WORK.md` has no current cycle.
|
|
337
336
|
|
|
338
337
|
**Stage requirements:** none.
|
|
339
338
|
|
|
340
339
|
**Side effects:** none.
|
|
341
340
|
|
|
342
|
-
### `foundry_artefacts_set_status`
|
|
343
|
-
|
|
344
|
-
> Update the status of an artefact in `WORK.md` (`done` | `blocked`
|
|
345
|
-
> only).
|
|
346
|
-
|
|
347
|
-
**Args:**
|
|
348
|
-
- `file` (string, required): Artefact file path.
|
|
349
|
-
- `status` (string, required): New status (`done` | `blocked`).
|
|
350
|
-
|
|
351
|
-
**Returns:** `{ ok: true }`. On invalid input: `{ error: <message> }`.
|
|
352
|
-
|
|
353
|
-
**Stage requirements:** requires no active stage. Refuses on failed
|
|
354
|
-
flow.
|
|
355
|
-
|
|
356
|
-
**Failure modes:**
|
|
357
|
-
- Invalid status, unknown file, malformed table → error from
|
|
358
|
-
`setArtefactStatus`.
|
|
359
|
-
|
|
360
|
-
**Side effects:** rewrites `WORK.md`.
|
|
361
|
-
|
|
362
|
-
---
|
|
363
|
-
|
|
364
341
|
## Feedback
|
|
365
342
|
|
|
366
343
|
All feedback tools (except `foundry_feedback_list`) require an active
|
package/dist/docs/work-spec.md
CHANGED
|
@@ -168,11 +168,11 @@ A crash between the two steps leaves the live file untouched.
|
|
|
168
168
|
| Frontmatter (`flow`, `cycle`) | `foundry_workfile_create` (flow skill) | updated in place as the flow advances between cycles |
|
|
169
169
|
| Frontmatter (`stages`, `max-iterations`, `human-appraise`, `deadlock-appraise`, `deadlock-iterations`, `models`) | `foundry_orchestrate` (first call of each cycle, internally) | reset on each new cycle |
|
|
170
170
|
| Goal | `foundry_workfile_create` (flow skill) | nobody |
|
|
171
|
-
|
|
|
171
|
+
| Artefact files | forge stage writes files on disk | git tracks file changes; cycle-level state records completion or failure |
|
|
172
172
|
| `WORK.feedback.yaml` | `foundry_feedback_add` (`quench` / `appraise` / `human-appraise`) | `foundry_feedback_action` / `foundry_feedback_wontfix` (forge), `foundry_feedback_resolve` (source stage / human-appraise override); sort writes only deadlocked snapshots |
|
|
173
173
|
| `WORK.history.yaml` | `foundry_orchestrate` | `foundry_orchestrate` |
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
Artefact files are discovered from branch changes matching the cycle output type's `file-patterns`.
|
|
176
176
|
|
|
177
177
|
## WORK.history.yaml
|
|
178
178
|
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* so the orchestrator can re-sort and determine the next action.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import { selectAppraisers, getLaws } from './lib/config.js';
|
|
14
|
+
import { getArtefactFiles } from './lib/artefacts.js';
|
|
15
|
+
import { selectAppraisers, getLaws, getCycleDefinition } from './lib/config.js';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Public API — gather
|
|
@@ -27,6 +27,8 @@ import { selectAppraisers, getLaws } from './lib/config.js';
|
|
|
27
27
|
* @param {string} ctx.cycleId
|
|
28
28
|
* @param {object} ctx.io
|
|
29
29
|
* @param {string} ctx.foundryDir
|
|
30
|
+
* @param {string} [ctx.baseBranch] - Git base branch for diff comparison,
|
|
31
|
+
* defaults to 'main'.
|
|
30
32
|
* @param {string} [ctx.defaultModel] - Fallback model when an appraiser has no
|
|
31
33
|
* explicit model.
|
|
32
34
|
* @returns {Promise<{action: string, tasks: Array, stage: string, cycle: string}>}
|
|
@@ -36,12 +38,19 @@ export async function gatherAppraiseContext(ctx) {
|
|
|
36
38
|
return violation('cycleId is required', []);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const
|
|
41
|
+
const cd = await getCycleDefinition(ctx.foundryDir, ctx.cycleId, ctx.io);
|
|
42
|
+
const outputType = cd.frontmatter['output-type'];
|
|
43
|
+
if (!outputType) {
|
|
44
|
+
return violation(`cycle ${ctx.cycleId} missing output-type field`, []);
|
|
45
|
+
}
|
|
46
|
+
const baseBranch = ctx.baseBranch || 'main';
|
|
47
|
+
const artefacts = await getArtefactFiles(ctx.foundryDir, outputType, ctx.io, { baseBranch });
|
|
40
48
|
if (artefacts.length === 0) {
|
|
41
49
|
return emptyDispatch(ctx.cycleId);
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
const
|
|
52
|
+
const typedArtefacts = artefacts.map(artefact => ({ ...artefact, type: outputType }));
|
|
53
|
+
const tasks = await collectTasks(typedArtefacts, ctx);
|
|
45
54
|
|
|
46
55
|
return {
|
|
47
56
|
action: 'dispatch_multi',
|
|
@@ -91,7 +100,10 @@ async function resolveTypeEntry(typeId, cache, ctx) {
|
|
|
91
100
|
* Build and append appraiser tasks for a single artefact.
|
|
92
101
|
*/
|
|
93
102
|
function addTasksForArtefact(tasks, artefact, entry, ctx) {
|
|
94
|
-
|
|
103
|
+
let content = '';
|
|
104
|
+
if (artefact.state !== 'deleted') {
|
|
105
|
+
content = ctx.io.readFile(artefact.file);
|
|
106
|
+
}
|
|
95
107
|
|
|
96
108
|
for (const appraiser of entry.appraisers) {
|
|
97
109
|
const prompt = buildAppraiserPrompt({
|
|
@@ -1,163 +1,166 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Artefact discovery and branch change utilities.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Resolves the branch base SHA, collects changed files on the flow branch,
|
|
5
|
+
* filters by artefact type file-patterns, and returns the artefact change set.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
import { minimatch } from 'minimatch';
|
|
9
|
+
import { sortPaths } from './attestation/hash.js';
|
|
10
|
+
import { getArtefactType } from './config.js';
|
|
11
|
+
|
|
12
|
+
// --- Shared branch artefact discovery ---
|
|
13
|
+
|
|
14
|
+
const STATUS_HANDLERS = {
|
|
15
|
+
A: (parts) => [{ file: parts[1], state: 'new' }],
|
|
16
|
+
M: (parts) => [{ file: parts[1], state: 'modified' }],
|
|
17
|
+
T: (parts) => [{ file: parts[1], state: 'modified' }],
|
|
18
|
+
U: (parts) => [{ file: parts[1], state: 'modified' }],
|
|
19
|
+
D: (parts) => [{ file: parts[1], state: 'deleted' }],
|
|
20
|
+
R: (parts) => [
|
|
21
|
+
{ file: parts[1], state: 'deleted' },
|
|
22
|
+
{ file: parts[2], state: 'new' },
|
|
23
|
+
],
|
|
24
|
+
C: (parts) => [{ file: parts[2], state: 'new' }],
|
|
25
|
+
};
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseTableRow(line) {
|
|
22
|
-
const cols = line.split('|').slice(1, -1).map(c => c.trim());
|
|
23
|
-
return cols.length >= 4 ? cols : null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// --- Status validation ---
|
|
27
|
-
|
|
28
|
-
function validateStatus(newStatus) {
|
|
29
|
-
if (newStatus === 'draft') {
|
|
30
|
-
throw new Error('status draft not permitted; artefacts are registered automatically during orchestration');
|
|
31
|
-
}
|
|
32
|
-
if (!['done', 'blocked'].includes(newStatus)) {
|
|
33
|
-
throw new Error(`invalid status: ${newStatus}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// --- Table boundary detection ---
|
|
38
|
-
|
|
39
|
-
function findTableHeader(lines) {
|
|
40
|
-
for (let i = 0; i < lines.length; i++) {
|
|
41
|
-
if (isTableHeader(lines[i].trim())) return i;
|
|
42
|
-
}
|
|
43
|
-
return -1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function findTableSeparator(lines, afterIdx) {
|
|
47
|
-
for (let i = afterIdx + 1; i < lines.length; i++) {
|
|
48
|
-
if (isTableSeparator(lines[i].trim())) return i;
|
|
49
|
-
}
|
|
50
|
-
return -1;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getTableBounds(lines) {
|
|
54
|
-
const headerIdx = findTableHeader(lines);
|
|
55
|
-
if (headerIdx < 0) return null;
|
|
56
|
-
const sepIdx = findTableSeparator(lines, headerIdx);
|
|
57
|
-
if (sepIdx < 0) return null;
|
|
58
|
-
return { headerIdx, sepIdx };
|
|
27
|
+
/**
|
|
28
|
+
* Parse a single git diff --name-status line into one or more { file, state } entries.
|
|
29
|
+
* Uses a lookup table to map status codes to handlers.
|
|
30
|
+
* @param {string} line - A line from git diff --name-status output
|
|
31
|
+
* @returns {Array<{file: string, state: string}>}
|
|
32
|
+
*/
|
|
33
|
+
function parseDiffStatusLine(line) {
|
|
34
|
+
const parts = line.split('\t');
|
|
35
|
+
const handler = STATUS_HANDLERS[parts[0][0]];
|
|
36
|
+
return handler ? handler(parts) : [];
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Parse git diff --name-status output into an array of { file, state } entries.
|
|
41
|
+
* @param {string} output - Raw output from git diff --name-status
|
|
42
|
+
* @returns {Array<{file: string, state: string}>}
|
|
43
|
+
*/
|
|
44
|
+
function parseDiffOutput(output) {
|
|
45
|
+
const entries = [];
|
|
46
|
+
if (!output) return entries;
|
|
47
|
+
for (const line of output.trim().split('\n')) {
|
|
48
|
+
if (!line) continue;
|
|
49
|
+
for (const entry of parseDiffStatusLine(line)) {
|
|
50
|
+
entries.push(entry);
|
|
51
|
+
}
|
|
65
52
|
}
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function formatTableRow(cols) {
|
|
70
|
-
return '| ' + cols.join(' | ') + ' |';
|
|
53
|
+
return entries;
|
|
71
54
|
}
|
|
72
55
|
|
|
73
56
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
57
|
+
* Collect changed files from the branch since a given base SHA.
|
|
58
|
+
* Combines committed, unstaged, staged, and untracked changes.
|
|
59
|
+
*
|
|
60
|
+
* Sources are ordered by increasing priority: committed, unstaged, staged, untracked.
|
|
61
|
+
* This function does not deduplicate per-file entries; call dedupeArtefactChanges for that.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} branchBaseSha - The merge-base SHA for the branch
|
|
64
|
+
* @param {object} io - IO interface with exec
|
|
65
|
+
* @returns {Array<{file: string, state: string}>}
|
|
77
66
|
*/
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
cycle: cols[2],
|
|
93
|
-
status: cols[3],
|
|
94
|
-
});
|
|
67
|
+
function getBranchChangedFiles(branchBaseSha, io) {
|
|
68
|
+
const changes = [];
|
|
69
|
+
|
|
70
|
+
// Diff-based sources: committed, unstaged, staged
|
|
71
|
+
changes.push(...parseDiffOutput(io.exec(['git', 'diff', '--name-status', `${branchBaseSha}..HEAD`])));
|
|
72
|
+
changes.push(...parseDiffOutput(io.exec(['git', 'diff', '--name-status'])));
|
|
73
|
+
changes.push(...parseDiffOutput(io.exec(['git', 'diff', '--cached', '--name-status'])));
|
|
74
|
+
|
|
75
|
+
// Untracked files (not in git)
|
|
76
|
+
const untracked = io.exec(['git', 'ls-files', '--others', '--exclude-standard']);
|
|
77
|
+
if (untracked) {
|
|
78
|
+
for (const file of untracked.trim().split('\n')) {
|
|
79
|
+
if (!file) continue;
|
|
80
|
+
changes.push({ file, state: 'new' });
|
|
95
81
|
}
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
return
|
|
84
|
+
return changes;
|
|
99
85
|
}
|
|
100
86
|
|
|
101
87
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
88
|
+
* Deduplicate artefact changes, keeping the most recent state for each file.
|
|
89
|
+
* Because later git sources (staged, untracked) take precedence over earlier
|
|
90
|
+
* ones (committed), the last occurrence of a file wins.
|
|
91
|
+
*
|
|
92
|
+
* @param {Array<{file: string, state: string}>} changes
|
|
93
|
+
* @returns {Array<{file: string, state: string}>}
|
|
106
94
|
*/
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (!bounds) {
|
|
112
|
-
throw new Error('Artefacts table not found');
|
|
95
|
+
function dedupeArtefactChanges(changes) {
|
|
96
|
+
const seen = new Map();
|
|
97
|
+
for (const { file, state } of changes) {
|
|
98
|
+
seen.set(file, state);
|
|
113
99
|
}
|
|
114
|
-
|
|
115
|
-
const endIdx = findTableEnd(lines, bounds.sepIdx + 1);
|
|
116
|
-
const insertAt = endIdx > bounds.sepIdx + 1 ? endIdx - 1 : bounds.sepIdx;
|
|
117
|
-
const newRow = `| ${file} | ${type} | ${cycle} | ${status} |`;
|
|
118
|
-
lines.splice(insertAt + 1, 0, newRow);
|
|
119
|
-
return lines.join('\n');
|
|
100
|
+
return Array.from(seen.entries()).map(([file, state]) => ({ file, state }));
|
|
120
101
|
}
|
|
121
102
|
|
|
122
103
|
/**
|
|
123
|
-
*
|
|
124
|
-
* @param {
|
|
125
|
-
* @param {string}
|
|
126
|
-
* @
|
|
127
|
-
* @returns {string} Updated text
|
|
104
|
+
* Resolve the merge-base SHA between HEAD and a base branch.
|
|
105
|
+
* @param {object} io - IO interface with exec
|
|
106
|
+
* @param {string} [baseBranch='main'] - Base branch name
|
|
107
|
+
* @returns {string} The merge-base commit SHA
|
|
128
108
|
*/
|
|
129
|
-
export function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const lines = text.split('\n');
|
|
133
|
-
const bounds = getTableBounds(lines);
|
|
134
|
-
|
|
135
|
-
if (!bounds) {
|
|
136
|
-
throw new Error(`File not found in artefacts table: ${file}`);
|
|
109
|
+
export function resolveBranchBaseSha(io, baseBranch = 'main') {
|
|
110
|
+
if (!io.exec) {
|
|
111
|
+
throw new Error('io.exec is required for resolveBranchBaseSha');
|
|
137
112
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (let i = bounds.sepIdx + 1; i < endIdx; i++) {
|
|
142
|
-
const cols = parseTableRow(lines[i].trim());
|
|
143
|
-
if (cols && cols[0] === file) {
|
|
144
|
-
cols[3] = newStatus;
|
|
145
|
-
lines[i] = formatTableRow(cols);
|
|
146
|
-
return lines.join('\n');
|
|
147
|
-
}
|
|
113
|
+
const sha = io.exec(['git', 'merge-base', 'HEAD', baseBranch]).trim();
|
|
114
|
+
if (!sha) {
|
|
115
|
+
throw new Error(`Failed to resolve merge-base for HEAD and ${baseBranch}`);
|
|
148
116
|
}
|
|
117
|
+
return sha;
|
|
118
|
+
}
|
|
149
119
|
|
|
150
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Extract file-patterns from an artefact type definition frontmatter.
|
|
122
|
+
* Returns an empty array when frontmatter is absent or contains no patterns.
|
|
123
|
+
* @param {object} def - Artefact type definition with frontmatter
|
|
124
|
+
* @returns {Array<string>}
|
|
125
|
+
*/
|
|
126
|
+
function getFilePatterns(def) {
|
|
127
|
+
const fm = def.frontmatter;
|
|
128
|
+
return Array.isArray(fm && fm['file-patterns']) ? fm['file-patterns'] : [];
|
|
151
129
|
}
|
|
152
130
|
|
|
153
131
|
/**
|
|
154
|
-
* Get
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
132
|
+
* Get artefact files for a given artefact type using shared branch discovery.
|
|
133
|
+
*
|
|
134
|
+
* Reads the artefact type definition, resolves the branch base, collects changed
|
|
135
|
+
* files on the flow branch, filters by the type's file-patterns, and returns a
|
|
136
|
+
* deterministically sorted list of { file, state } entries.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} foundryDir - Path to the foundry directory
|
|
139
|
+
* @param {string} typeId - Artefact type identifier
|
|
140
|
+
* @param {object} io - IO interface with exec, readFile, exists
|
|
141
|
+
* @param {object} [options={}] - Optional parameters
|
|
142
|
+
* @param {string} [options.baseBranch='main'] - Base branch for merge-base resolution
|
|
143
|
+
* @param {string} [options.branchBaseSha] - Pre-resolved merge-base SHA (takes precedence)
|
|
144
|
+
* @returns {Promise<Array<{file: string, state: string}>>}
|
|
158
145
|
*/
|
|
159
|
-
export function
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
146
|
+
export async function getArtefactFiles(foundryDir, typeId, io, options = {}) {
|
|
147
|
+
const def = await getArtefactType(foundryDir, typeId, io);
|
|
148
|
+
const patterns = getFilePatterns(def);
|
|
149
|
+
|
|
150
|
+
if (patterns.length === 0) return [];
|
|
151
|
+
|
|
152
|
+
const baseBranch = options.baseBranch || 'main';
|
|
153
|
+
const branchBaseSha = options.branchBaseSha || resolveBranchBaseSha(io, baseBranch);
|
|
154
|
+
const changedFiles = getBranchChangedFiles(branchBaseSha, io);
|
|
155
|
+
const changes = dedupeArtefactChanges(changedFiles);
|
|
156
|
+
const matching = changes.filter(({ file }) =>
|
|
157
|
+
patterns.some(pattern => minimatch(file, pattern))
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Sort deterministically by file path
|
|
161
|
+
const sorted = sortPaths(matching.map(({ file }) => file));
|
|
162
|
+
const order = new Map(sorted.map((file, idx) => [file, idx]));
|
|
163
|
+
const result = [...matching].sort((a, b) => order.get(a.file) - order.get(b.file));
|
|
164
|
+
|
|
165
|
+
return result;
|
|
163
166
|
}
|