@linzumi/cli 1.0.31 → 1.0.33
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 +1 -1
- package/dist/cloud-supervisor.mjs +11 -11
- package/dist/index.js +345 -330
- package/dist/mcp-server.mjs +44 -29
- package/package.json +1 -1
- package/scripts/f2-task-graph-mcp-e2e.mjs +281 -0
package/package.json
CHANGED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-07-04
|
|
3
|
+
Spec: plans/2026-07-04-projects-followups-stack.md (F2)
|
|
4
|
+
Relationship: Scripted MCP-client execution proof for the project task-graph
|
|
5
|
+
tools. Spawns the REAL shipped MCP server (`linzumi mcp` over stdio, text
|
|
6
|
+
mode, tool scope all) against a locally running kandan server and exercises
|
|
7
|
+
every tool end to end: list projects, read the graph, create a group +
|
|
8
|
+
issues (assignment included), wire dependency edges, verify derived
|
|
9
|
+
launch-readiness flips as prerequisites complete, refuse a cycle (with the
|
|
10
|
+
human-legible path), reorganize (move into/out of a group, reorder), hit the
|
|
11
|
+
optimistic-lock staleness guard, remove an edge, delete a task, and read the
|
|
12
|
+
final graph. Prints a full transcript and exits non-zero on any failed
|
|
13
|
+
expectation.
|
|
14
|
+
|
|
15
|
+
Usage (see kandan/server_v2/scripts/f2_task_graph_mcp_seed.exs for seeding):
|
|
16
|
+
node scripts/f2-task-graph-mcp-e2e.mjs \
|
|
17
|
+
--api-url http://127.0.0.1:4199 \
|
|
18
|
+
--seed-json '<output of the seed script>'
|
|
19
|
+
*/
|
|
20
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
21
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const argValue = (flag) => {
|
|
27
|
+
const index = args.indexOf(flag);
|
|
28
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const apiUrl = argValue('--api-url') ?? 'http://127.0.0.1:4199';
|
|
32
|
+
const seed = JSON.parse(argValue('--seed-json') ?? '{}');
|
|
33
|
+
if (!seed.token || !seed.project_id) {
|
|
34
|
+
console.error(
|
|
35
|
+
'missing --seed-json (run kandan/server_v2/scripts/f2_task_graph_mcp_seed.exs)'
|
|
36
|
+
);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const packageRoot = path.resolve(
|
|
41
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
42
|
+
'..'
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
let failures = 0;
|
|
46
|
+
const check = (label, condition) => {
|
|
47
|
+
const mark = condition ? 'PASS' : 'FAIL';
|
|
48
|
+
if (!condition) failures += 1;
|
|
49
|
+
console.log(` [${mark}] ${label}`);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let step = 0;
|
|
53
|
+
const client = new Client({ name: 'f2-e2e-proof', version: '0.0.0' });
|
|
54
|
+
|
|
55
|
+
const callTool = async (name, params) => {
|
|
56
|
+
step += 1;
|
|
57
|
+
console.log(`\n=== step ${step}: ${name} ${JSON.stringify(params)}`);
|
|
58
|
+
try {
|
|
59
|
+
const result = await client.callTool({ name, arguments: params });
|
|
60
|
+
const text = result.content?.[0]?.text ?? '{}';
|
|
61
|
+
if (result.isError) {
|
|
62
|
+
// Tool-level refusals surface as isError text content (the thrown
|
|
63
|
+
// client error message), not JSON.
|
|
64
|
+
console.log(`TOOL ERROR: ${text}`);
|
|
65
|
+
return { __tool_error__: text };
|
|
66
|
+
}
|
|
67
|
+
const parsed = JSON.parse(text);
|
|
68
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
69
|
+
return parsed;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const message = String(error?.message ?? error);
|
|
72
|
+
console.log(`TOOL ERROR: ${message}`);
|
|
73
|
+
return { __tool_error__: message };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const transport = new StdioClientTransport({
|
|
78
|
+
command: process.execPath,
|
|
79
|
+
args: [
|
|
80
|
+
path.join(packageRoot, 'bin', 'linzumi.js'),
|
|
81
|
+
'mcp',
|
|
82
|
+
'server',
|
|
83
|
+
'--api-url',
|
|
84
|
+
apiUrl,
|
|
85
|
+
'--mode',
|
|
86
|
+
'text',
|
|
87
|
+
'--tool-scope',
|
|
88
|
+
'all',
|
|
89
|
+
],
|
|
90
|
+
env: {
|
|
91
|
+
...process.env,
|
|
92
|
+
LINZUMI_MCP_ACCESS_TOKEN: seed.token,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await client.connect(transport);
|
|
97
|
+
|
|
98
|
+
console.log('=== tools/list');
|
|
99
|
+
const tools = await client.listTools();
|
|
100
|
+
const toolNames = tools.tools.map((tool) => tool.name);
|
|
101
|
+
const expectedTools = [
|
|
102
|
+
'linzumi_list_projects',
|
|
103
|
+
'linzumi_get_project_tasks',
|
|
104
|
+
'linzumi_create_project_task',
|
|
105
|
+
'linzumi_update_project_task',
|
|
106
|
+
'linzumi_delete_project_task',
|
|
107
|
+
'linzumi_add_project_task_dependency',
|
|
108
|
+
'linzumi_remove_project_task_dependency',
|
|
109
|
+
];
|
|
110
|
+
for (const tool of expectedTools) {
|
|
111
|
+
check(`tools/list advertises ${tool}`, toolNames.includes(tool));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 1. Discover the project.
|
|
115
|
+
const projects = await callTool('linzumi_list_projects', {});
|
|
116
|
+
check(
|
|
117
|
+
'list_projects returns the seeded project',
|
|
118
|
+
projects.projects?.some((entry) => entry.project_id === seed.project_id)
|
|
119
|
+
);
|
|
120
|
+
const projectId = seed.project_id;
|
|
121
|
+
|
|
122
|
+
// 2. Empty graph read.
|
|
123
|
+
const emptyGraph = await callTool('linzumi_get_project_tasks', {
|
|
124
|
+
project_id: projectId,
|
|
125
|
+
});
|
|
126
|
+
check('graph starts empty', emptyGraph.tasks?.length === 0);
|
|
127
|
+
|
|
128
|
+
// 3. Create a group and three issues (one assigned, one grouped).
|
|
129
|
+
const group = await callTool('linzumi_create_project_task', {
|
|
130
|
+
project_id: projectId,
|
|
131
|
+
title: 'Milestone 1',
|
|
132
|
+
kind: 'group',
|
|
133
|
+
});
|
|
134
|
+
check('group created', group.task?.kind === 'group');
|
|
135
|
+
|
|
136
|
+
const design = await callTool('linzumi_create_project_task', {
|
|
137
|
+
project_id: projectId,
|
|
138
|
+
title: 'Design the schema',
|
|
139
|
+
description: 'Commented DDL + SDL diff first.',
|
|
140
|
+
parent_id: group.task?.task_id,
|
|
141
|
+
assignee: seed.teammate,
|
|
142
|
+
});
|
|
143
|
+
check(
|
|
144
|
+
'issue created inside the group',
|
|
145
|
+
design.task?.parent_task_id === group.task?.task_id
|
|
146
|
+
);
|
|
147
|
+
check('issue assigned by username', design.task?.assignee === seed.teammate);
|
|
148
|
+
|
|
149
|
+
const implement = await callTool('linzumi_create_project_task', {
|
|
150
|
+
project_id: projectId,
|
|
151
|
+
title: 'Implement the API',
|
|
152
|
+
});
|
|
153
|
+
const docs = await callTool('linzumi_create_project_task', {
|
|
154
|
+
project_id: projectId,
|
|
155
|
+
title: 'Write the docs',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 4. Wire the DAG: implement <- design, docs <- implement.
|
|
159
|
+
const edge1 = await callTool('linzumi_add_project_task_dependency', {
|
|
160
|
+
task_id: implement.task?.task_id,
|
|
161
|
+
depends_on_task_id: design.task?.task_id,
|
|
162
|
+
});
|
|
163
|
+
check(
|
|
164
|
+
'dependency added blocks the dependent',
|
|
165
|
+
edge1.task?.work_state === 'blocked'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await callTool('linzumi_add_project_task_dependency', {
|
|
169
|
+
task_id: docs.task?.task_id,
|
|
170
|
+
depends_on_task_id: implement.task?.task_id,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const wiredGraph = await callTool('linzumi_get_project_tasks', {
|
|
174
|
+
project_id: projectId,
|
|
175
|
+
});
|
|
176
|
+
const wiredStates = Object.fromEntries(
|
|
177
|
+
(wiredGraph.tasks ?? []).map((task) => [task.title, task.work_state])
|
|
178
|
+
);
|
|
179
|
+
check(
|
|
180
|
+
'readiness derives from the edges (design ready)',
|
|
181
|
+
wiredStates['Design the schema'] === 'ready'
|
|
182
|
+
);
|
|
183
|
+
check(
|
|
184
|
+
'implement blocked behind design',
|
|
185
|
+
wiredStates['Implement the API'] === 'blocked'
|
|
186
|
+
);
|
|
187
|
+
check(
|
|
188
|
+
'docs blocked behind implement',
|
|
189
|
+
wiredStates['Write the docs'] === 'blocked'
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// 5. Acyclicity: design <- docs would close the loop; expect the refusal + path.
|
|
193
|
+
const cycle = await callTool('linzumi_add_project_task_dependency', {
|
|
194
|
+
task_id: design.task?.task_id,
|
|
195
|
+
depends_on_task_id: docs.task?.task_id,
|
|
196
|
+
});
|
|
197
|
+
check(
|
|
198
|
+
'cycle refused',
|
|
199
|
+
cycle.status === 'refused' && cycle.reason === 'would_create_cycle'
|
|
200
|
+
);
|
|
201
|
+
check(
|
|
202
|
+
'cycle path is human-legible',
|
|
203
|
+
Array.isArray(cycle.cycle_path) && cycle.cycle_path.length >= 2
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// 6. Status transition: finishing design makes implement ready.
|
|
207
|
+
const done = await callTool('linzumi_update_project_task', {
|
|
208
|
+
task_id: design.task?.task_id,
|
|
209
|
+
status: 'done',
|
|
210
|
+
status_note: 'Schema landed.',
|
|
211
|
+
lock_version: design.task?.lock_version,
|
|
212
|
+
});
|
|
213
|
+
check('status transition applied', done.task?.status === 'done');
|
|
214
|
+
|
|
215
|
+
const readyGraph = await callTool('linzumi_get_project_tasks', {
|
|
216
|
+
project_id: projectId,
|
|
217
|
+
});
|
|
218
|
+
const readyStates = Object.fromEntries(
|
|
219
|
+
(readyGraph.tasks ?? []).map((task) => [task.title, task.work_state])
|
|
220
|
+
);
|
|
221
|
+
check(
|
|
222
|
+
'launch-readiness reflects the completed prerequisite',
|
|
223
|
+
readyStates['Implement the API'] === 'ready'
|
|
224
|
+
);
|
|
225
|
+
check('summary counts one done issue', readyGraph.summary?.done === 1);
|
|
226
|
+
|
|
227
|
+
// 7. Reorganize: move docs into the group, reorder, then back to top level.
|
|
228
|
+
const moved = await callTool('linzumi_update_project_task', {
|
|
229
|
+
task_id: docs.task?.task_id,
|
|
230
|
+
parent_id: group.task?.task_id,
|
|
231
|
+
position: 0,
|
|
232
|
+
});
|
|
233
|
+
check(
|
|
234
|
+
'issue moved into the group',
|
|
235
|
+
moved.task?.parent_task_id === group.task?.task_id
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const movedOut = await callTool('linzumi_update_project_task', {
|
|
239
|
+
task_id: docs.task?.task_id,
|
|
240
|
+
clear_parent: true,
|
|
241
|
+
});
|
|
242
|
+
check('issue moved back to top level', movedOut.task?.parent_task_id === null);
|
|
243
|
+
|
|
244
|
+
// 8. Optimistic lock: replaying design's original lock_version must fail loudly.
|
|
245
|
+
const stale = await callTool('linzumi_update_project_task', {
|
|
246
|
+
task_id: design.task?.task_id,
|
|
247
|
+
title: 'Clobber attempt',
|
|
248
|
+
lock_version: design.task?.lock_version,
|
|
249
|
+
});
|
|
250
|
+
check(
|
|
251
|
+
'stale write refused',
|
|
252
|
+
String(stale.__tool_error__ ?? '').includes('stale')
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// 9. Remove an edge: docs no longer waits on implement.
|
|
256
|
+
const removed = await callTool('linzumi_remove_project_task_dependency', {
|
|
257
|
+
task_id: docs.task?.task_id,
|
|
258
|
+
depends_on_task_id: implement.task?.task_id,
|
|
259
|
+
});
|
|
260
|
+
check('edge removed frees the dependent', removed.task?.work_state === 'ready');
|
|
261
|
+
|
|
262
|
+
// 10. Delete: docs leaves the graph.
|
|
263
|
+
const deleted = await callTool('linzumi_delete_project_task', {
|
|
264
|
+
task_id: docs.task?.task_id,
|
|
265
|
+
});
|
|
266
|
+
check('task deleted', deleted.deleted_task_id === docs.task?.task_id);
|
|
267
|
+
|
|
268
|
+
const finalGraph = await callTool('linzumi_get_project_tasks', {
|
|
269
|
+
project_id: projectId,
|
|
270
|
+
});
|
|
271
|
+
check(
|
|
272
|
+
'final graph has 3 tasks (group + 2 issues)',
|
|
273
|
+
finalGraph.tasks?.length === 3
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await client.close();
|
|
277
|
+
|
|
278
|
+
console.log(
|
|
279
|
+
`\n=== ${failures === 0 ? 'ALL CHECKS PASSED' : `${failures} CHECK(S) FAILED`}`
|
|
280
|
+
);
|
|
281
|
+
process.exit(failures === 0 ? 0 : 1);
|