@aion0/forge 0.10.6 → 0.10.17
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/RELEASE_NOTES.md +5 -5
- package/app/api/public-info/[resource]/route.ts +40 -0
- package/app/api/skills/install-local/route.ts +2 -1
- package/cli/mw.mjs +11 -21
- package/components/SettingsModal.tsx +42 -33
- package/components/WorkspaceView.tsx +5 -3
- package/lib/agents/index.ts +8 -9
- package/lib/agents/known-models.ts +75 -0
- package/lib/agents/migrate.ts +14 -3
- package/lib/dirs.ts +6 -26
- package/lib/public-info/fetch.ts +116 -0
- package/lib/public-info/types.ts +38 -0
- package/lib/public-info/use-models-registry.ts +66 -0
- package/lib/settings.ts +34 -4
- package/lib/skills.ts +2 -2
- package/lib/workspace/watch-manager.ts +5 -1
- package/package.json +1 -1
- package/lib/__tests__/foreach-batch-yaml.test.ts +0 -33
- package/lib/__tests__/foreach-before.test.ts +0 -201
- package/lib/__tests__/foreach-parse.test.ts +0 -114
- package/lib/__tests__/foreach-snapshot.test.ts +0 -112
- package/lib/__tests__/foreach-source.test.ts +0 -105
- package/lib/__tests__/foreach-template.test.ts +0 -112
- package/lib/workspace/__tests__/state-machine.test.ts +0 -388
- package/lib/workspace/__tests__/workspace.test.ts +0 -311
- package/scripts/bench/README.md +0 -66
- package/scripts/bench/results/.gitignore +0 -2
- package/scripts/bench/run.ts +0 -635
- package/scripts/bench/tasks/01-text-utils/task.md +0 -26
- package/scripts/bench/tasks/01-text-utils/validator.sh +0 -46
- package/scripts/bench/tasks/02-pagination/setup.sh +0 -19
- package/scripts/bench/tasks/02-pagination/task.md +0 -48
- package/scripts/bench/tasks/02-pagination/validator.sh +0 -69
- package/scripts/bench/tasks/03-bug-fix/setup.sh +0 -82
- package/scripts/bench/tasks/03-bug-fix/task.md +0 -30
- package/scripts/bench/tasks/03-bug-fix/validator.sh +0 -29
- package/scripts/test-agents-migrate.ts +0 -149
- package/scripts/test-mantis.ts +0 -223
- package/scripts/test-memory-local.ts +0 -139
- package/scripts/test-memory-upsert.ts +0 -106
- package/scripts/verify-usage.ts +0 -178
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step-5 verification: snapshotIteration captures node states + correctly
|
|
3
|
-
* advances the iterations[] history. (Full checkPipelineCompletion has
|
|
4
|
-
* filesystem + scheduling side effects — exercised end-to-end in Step 7.)
|
|
5
|
-
*
|
|
6
|
-
* Run: npx tsx lib/__tests__/foreach-snapshot.test.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { snapshotIteration } from '../pipeline';
|
|
10
|
-
|
|
11
|
-
let passed = 0, failed = 0;
|
|
12
|
-
function check(name: string, fn: () => void) {
|
|
13
|
-
try { fn(); console.log(` ✓ ${name}`); passed++; }
|
|
14
|
-
catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function makePipeline(items: unknown[], currentIndex = 0): any {
|
|
18
|
-
return {
|
|
19
|
-
id: 'test123',
|
|
20
|
-
workflowName: 'test',
|
|
21
|
-
status: 'running',
|
|
22
|
-
input: {},
|
|
23
|
-
vars: {},
|
|
24
|
-
nodes: {
|
|
25
|
-
a: { status: 'done', outputs: { x: '1' } },
|
|
26
|
-
b: { status: 'done', outputs: { y: '2' }, error: undefined },
|
|
27
|
-
},
|
|
28
|
-
nodeOrder: ['a', 'b'],
|
|
29
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
30
|
-
forEach: {
|
|
31
|
-
items,
|
|
32
|
-
currentIndex,
|
|
33
|
-
total: items.length,
|
|
34
|
-
asName: 'item',
|
|
35
|
-
onFailure: 'continue',
|
|
36
|
-
iterations: [],
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log('snapshotIteration tests');
|
|
42
|
-
|
|
43
|
-
check('snapshot captures all-done nodes with status="done"', () => {
|
|
44
|
-
const p = makePipeline([1, 2, 3]);
|
|
45
|
-
snapshotIteration(p, false);
|
|
46
|
-
if (p.forEach.iterations.length !== 1) throw new Error('iterations length wrong');
|
|
47
|
-
const iter = p.forEach.iterations[0];
|
|
48
|
-
if (iter.status !== 'done') throw new Error(`status wrong: ${iter.status}`);
|
|
49
|
-
if (iter.index !== 0) throw new Error('index wrong');
|
|
50
|
-
if (iter.nodes.a.status !== 'done') throw new Error('node a status wrong');
|
|
51
|
-
if (iter.nodes.a.outputs.x !== '1') throw new Error('node a output not preserved');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
check('snapshot captures failed iter with status="failed"', () => {
|
|
55
|
-
const p = makePipeline([1, 2]);
|
|
56
|
-
p.nodes.b.status = 'failed';
|
|
57
|
-
p.nodes.b.error = 'boom';
|
|
58
|
-
snapshotIteration(p, true);
|
|
59
|
-
const iter = p.forEach.iterations[0];
|
|
60
|
-
if (iter.status !== 'failed') throw new Error(`status wrong: ${iter.status}`);
|
|
61
|
-
if (iter.nodes.b.error !== 'boom') throw new Error('error not preserved');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
check('startedAt = createdAt for first iter', () => {
|
|
65
|
-
const p = makePipeline([1, 2]);
|
|
66
|
-
snapshotIteration(p, false);
|
|
67
|
-
if (p.forEach.iterations[0].startedAt !== p.createdAt) throw new Error('first startedAt should = createdAt');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
check('startedAt = previous completedAt for subsequent iters', () => {
|
|
71
|
-
const p = makePipeline([1, 2]);
|
|
72
|
-
snapshotIteration(p, false);
|
|
73
|
-
const firstCompleted = p.forEach.iterations[0].completedAt;
|
|
74
|
-
p.forEach.currentIndex = 1;
|
|
75
|
-
snapshotIteration(p, false);
|
|
76
|
-
if (p.forEach.iterations[1].startedAt !== firstCompleted) {
|
|
77
|
-
throw new Error('second iter startedAt should chain from prev completedAt');
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
check('outputs are deep-copied (mutation safety)', () => {
|
|
82
|
-
const p = makePipeline([1]);
|
|
83
|
-
snapshotIteration(p, false);
|
|
84
|
-
const iter = p.forEach.iterations[0];
|
|
85
|
-
// Mutate live node — snapshot should NOT change
|
|
86
|
-
p.nodes.a.outputs.x = 'MUTATED';
|
|
87
|
-
if (iter.nodes.a.outputs.x !== '1') throw new Error('snapshot was mutated; need defensive copy');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
check('multiple iterations accumulate', () => {
|
|
91
|
-
const p = makePipeline([1, 2, 3]);
|
|
92
|
-
snapshotIteration(p, false);
|
|
93
|
-
p.forEach.currentIndex = 1;
|
|
94
|
-
snapshotIteration(p, false);
|
|
95
|
-
p.forEach.currentIndex = 2;
|
|
96
|
-
snapshotIteration(p, true);
|
|
97
|
-
if (p.forEach.iterations.length !== 3) throw new Error('expected 3 iters');
|
|
98
|
-
if (p.forEach.iterations[2].status !== 'failed') throw new Error('iter 3 should be failed');
|
|
99
|
-
if (p.forEach.iterations[0].index !== 0) throw new Error('iter 0 index wrong');
|
|
100
|
-
if (p.forEach.iterations[1].index !== 1) throw new Error('iter 1 index wrong');
|
|
101
|
-
if (p.forEach.iterations[2].index !== 2) throw new Error('iter 2 index wrong');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
check('no-op when pipeline.forEach undefined', () => {
|
|
105
|
-
const p: any = { id: 'x', nodes: {}, createdAt: '2026-01-01' };
|
|
106
|
-
snapshotIteration(p, false);
|
|
107
|
-
// Should not throw, should not add anything
|
|
108
|
-
if (p.forEach !== undefined) throw new Error('should leave forEach undefined');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
112
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step-4 verification: resolveForEachSource resolves + splits items.
|
|
3
|
-
* Run: npx tsx lib/__tests__/foreach-source.test.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { resolveForEachSource } from '../pipeline';
|
|
7
|
-
|
|
8
|
-
let passed = 0, failed = 0;
|
|
9
|
-
function check(name: string, fn: () => void) {
|
|
10
|
-
try { fn(); console.log(` ✓ ${name}`); passed++; }
|
|
11
|
-
catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
|
|
12
|
-
}
|
|
13
|
-
function eqArr(a: unknown[], b: unknown[], label = '') {
|
|
14
|
-
if (a.length !== b.length || a.some((v, i) => v !== b[i])) {
|
|
15
|
-
throw new Error(`${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
console.log('resolveForEachSource tests');
|
|
20
|
-
|
|
21
|
-
check('string source via {{input.x}} template, default split=","', () => {
|
|
22
|
-
const items = resolveForEachSource(
|
|
23
|
-
{ source: '{{input.ids}}' },
|
|
24
|
-
{ ids: '111,222,333' },
|
|
25
|
-
{},
|
|
26
|
-
);
|
|
27
|
-
eqArr(items, ['111', '222', '333']);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
check('string source with whitespace around commas — trimmed', () => {
|
|
31
|
-
const items = resolveForEachSource(
|
|
32
|
-
{ source: '{{input.ids}}' },
|
|
33
|
-
{ ids: ' 111 ,222 , 333 ' },
|
|
34
|
-
{},
|
|
35
|
-
);
|
|
36
|
-
eqArr(items, ['111', '222', '333']);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
check('string source with empty entries dropped', () => {
|
|
40
|
-
const items = resolveForEachSource(
|
|
41
|
-
{ source: '{{input.ids}}' },
|
|
42
|
-
{ ids: '111,,222, ,333,' },
|
|
43
|
-
{},
|
|
44
|
-
);
|
|
45
|
-
eqArr(items, ['111', '222', '333']);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
check('empty input.ids → empty array', () => {
|
|
49
|
-
const items = resolveForEachSource({ source: '{{input.ids}}' }, { ids: '' }, {});
|
|
50
|
-
eqArr(items, []);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
check('input missing → empty string template → empty array', () => {
|
|
54
|
-
const items = resolveForEachSource({ source: '{{input.missing}}' }, {}, {});
|
|
55
|
-
eqArr(items, []);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
check('custom split: ";"', () => {
|
|
59
|
-
const items = resolveForEachSource(
|
|
60
|
-
{ source: '{{input.ids}}', split: ';' },
|
|
61
|
-
{ ids: '111;222;333' },
|
|
62
|
-
{},
|
|
63
|
-
);
|
|
64
|
-
eqArr(items, ['111', '222', '333']);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
check('array literal source — used as-is', () => {
|
|
68
|
-
const items = resolveForEachSource(
|
|
69
|
-
{ source: ['a', 'b', 'c'] },
|
|
70
|
-
{},
|
|
71
|
-
{},
|
|
72
|
-
);
|
|
73
|
-
eqArr(items, ['a', 'b', 'c']);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
check('array literal with objects — preserved', () => {
|
|
77
|
-
const items = resolveForEachSource(
|
|
78
|
-
{ source: [{ iid: 1 }, { iid: 2 }] },
|
|
79
|
-
{},
|
|
80
|
-
{},
|
|
81
|
-
);
|
|
82
|
-
if (items.length !== 2) throw new Error('length wrong');
|
|
83
|
-
if ((items[0] as any).iid !== 1) throw new Error('first item wrong');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
check('vars source resolves', () => {
|
|
87
|
-
const items = resolveForEachSource(
|
|
88
|
-
{ source: '{{vars.list}}' },
|
|
89
|
-
{},
|
|
90
|
-
{ list: 'a,b,c' },
|
|
91
|
-
);
|
|
92
|
-
eqArr(items, ['a', 'b', 'c']);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
check('literal string with no template, just split', () => {
|
|
96
|
-
const items = resolveForEachSource(
|
|
97
|
-
{ source: 'foo,bar' },
|
|
98
|
-
{},
|
|
99
|
-
{},
|
|
100
|
-
);
|
|
101
|
-
eqArr(items, ['foo', 'bar']);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
105
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step-3 verification: resolveTemplate handles for_each context.
|
|
3
|
-
* Run: npx tsx lib/__tests__/foreach-template.test.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { resolveTemplate } from '../pipeline';
|
|
7
|
-
|
|
8
|
-
let passed = 0;
|
|
9
|
-
let failed = 0;
|
|
10
|
-
function check(name: string, fn: () => void) {
|
|
11
|
-
try { fn(); console.log(` ✓ ${name}`); passed++; }
|
|
12
|
-
catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
|
|
13
|
-
}
|
|
14
|
-
function eq(actual: string, expected: string, label = '') {
|
|
15
|
-
if (actual !== expected) {
|
|
16
|
-
throw new Error(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const baseCtx = { input: {}, vars: {}, nodes: {} };
|
|
21
|
-
|
|
22
|
-
console.log('resolveTemplate + for_each tests');
|
|
23
|
-
|
|
24
|
-
check('no forEach in ctx — {{item}} is literal passthrough', () => {
|
|
25
|
-
eq(resolveTemplate('hello {{item}}', baseCtx), 'hello {{item}}');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
check('scalar item: {{<as>}} substitutes', () => {
|
|
29
|
-
const ctx = { ...baseCtx, forEach: { asName: 'bug_id', item: 1213844, index: 0, total: 3 } };
|
|
30
|
-
eq(resolveTemplate('Fix bug {{bug_id}} now', ctx), 'Fix bug 1213844 now');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
check('string item: {{<as>}} preserves string', () => {
|
|
34
|
-
const ctx = { ...baseCtx, forEach: { asName: 'name', item: 'alice', index: 0, total: 1 } };
|
|
35
|
-
eq(resolveTemplate('hi {{name}}', ctx), 'hi alice');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
check('object item: {{<as>.field}} dotted access', () => {
|
|
39
|
-
const ctx = { ...baseCtx, forEach: { asName: 'mr', item: { iid: 42, title: 'fix x' }, index: 0, total: 1 } };
|
|
40
|
-
eq(resolveTemplate('MR !{{mr.iid}}: {{mr.title}}', ctx), 'MR !42: fix x');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
check('object item: missing field → empty string', () => {
|
|
44
|
-
const ctx = { ...baseCtx, forEach: { asName: 'mr', item: { iid: 42 }, index: 0, total: 1 } };
|
|
45
|
-
eq(resolveTemplate('author=@{{mr.author}}', ctx), 'author=@');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
check('object item: {{<as>}} alone → JSON', () => {
|
|
49
|
-
const ctx = { ...baseCtx, forEach: { asName: 'mr', item: { iid: 42, t: 'x' }, index: 0, total: 1 } };
|
|
50
|
-
eq(resolveTemplate('raw={{mr}}', ctx), 'raw={"iid":42,"t":"x"}');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
check('{{loop.index}} + {{loop.total}}', () => {
|
|
54
|
-
const ctx = { ...baseCtx, forEach: { asName: 'item', item: 'x', index: 2, total: 5 } };
|
|
55
|
-
eq(resolveTemplate('Step {{loop.index}} of {{loop.total}}', ctx), 'Step 2 of 5');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
check('shellMode escapes scalar item', () => {
|
|
59
|
-
const ctx = { ...baseCtx, forEach: { asName: 'name', item: "o'brien", index: 0, total: 1 } };
|
|
60
|
-
// shellEscapeAnsiC: ' → \'
|
|
61
|
-
const out = resolveTemplate("echo '{{name}}'", ctx, true);
|
|
62
|
-
if (!out.includes("o\\'brien")) throw new Error(`expected escaped apostrophe, got: ${out}`);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
check('{{raw:<as>}} bypasses shell escape', () => {
|
|
66
|
-
const ctx = { ...baseCtx, forEach: { asName: 'name', item: "o'brien", index: 0, total: 1 } };
|
|
67
|
-
// raw: keeps verbatim
|
|
68
|
-
eq(resolveTemplate('{{raw:name}}', ctx, true), "o'brien");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
check('input.x still works alongside forEach', () => {
|
|
72
|
-
const ctx = { input: { proj: 'FortiNAC' }, vars: {}, nodes: {}, forEach: { asName: 'bug_id', item: 1, index: 0, total: 1 } };
|
|
73
|
-
eq(resolveTemplate('{{input.proj}}/mantis-{{bug_id}}', ctx), 'FortiNAC/mantis-1');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
check('nodes.X.outputs.Y still works alongside forEach', () => {
|
|
77
|
-
const ctx: any = {
|
|
78
|
-
input: {},
|
|
79
|
-
vars: {},
|
|
80
|
-
nodes: { ingest: { status: 'done', outputs: { ctx: 'hello' }, iterations: 1 } },
|
|
81
|
-
forEach: { asName: 'item', item: 'x', index: 0, total: 1 },
|
|
82
|
-
};
|
|
83
|
-
eq(resolveTemplate('{{nodes.ingest.outputs.ctx}}/{{item}}', ctx), 'hello/x');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
check('run.tmp_dir still works alongside forEach', () => {
|
|
87
|
-
const ctx = {
|
|
88
|
-
...baseCtx,
|
|
89
|
-
tmpDir: '/path/.forge/worktrees/pipeline-abc',
|
|
90
|
-
forEach: { asName: 'bug_id', item: 42, index: 0, total: 1 },
|
|
91
|
-
};
|
|
92
|
-
eq(resolveTemplate('{{run.tmp_dir}}/mantis-{{bug_id}}', ctx),
|
|
93
|
-
'/path/.forge/worktrees/pipeline-abc/mantis-42');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
check('forEach.item = null → empty string', () => {
|
|
97
|
-
const ctx = { ...baseCtx, forEach: { asName: 'x', item: null, index: 0, total: 1 } };
|
|
98
|
-
eq(resolveTemplate('[{{x}}]', ctx), '[]');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
check('forEach.item = false → "false"', () => {
|
|
102
|
-
const ctx = { ...baseCtx, forEach: { asName: 'x', item: false, index: 0, total: 1 } };
|
|
103
|
-
eq(resolveTemplate('[{{x}}]', ctx), '[false]');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
check('loop.index works even when item is undefined-but-asName-set (defensive)', () => {
|
|
107
|
-
const ctx = { ...baseCtx, forEach: { asName: 'item', item: undefined, index: 7, total: 9 } };
|
|
108
|
-
eq(resolveTemplate('{{loop.index}}/{{loop.total}}', ctx), '7/9');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
112
|
-
process.exit(failed === 0 ? 0 : 1);
|