@linzumi/cli 1.0.34 → 1.0.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,167 @@
1
+ /*
2
+ - Date: 2026-07-04
3
+ Spec: plans/2026-07-04-projects-followups-stack.md (F3)
4
+ Relationship: Behavioral execution proof for the F3 prompt: drives a REAL
5
+ Claude Code session (Claude Agent SDK, the same preset+append system-prompt
6
+ wiring claudeCodeSession.ts uses) with the compiled project-bound developer
7
+ instructions and the REAL shipped stdio Linzumi MCP server, starts the
8
+ session in plan mode, approves the plan-mode exit, and lets the prompt -
9
+ nothing else - drive the transfer of the plan into the project's task DAG.
10
+ Prints the full tool-call transcript. Pair with
11
+ scripts/f3-planning-dag-verify.mjs to snapshot/assert the resulting graph.
12
+
13
+ This intentionally exercises the PROMPT as the artifact under test: the
14
+ user prompt never mentions Linzumi, projects, tasks, or MCP tools.
15
+
16
+ Usage (see kandan/server_v2/scripts/f2_task_graph_mcp_seed.exs for seeding):
17
+ node scripts/f3-planning-dag-e2e.mjs \
18
+ --api-url http://127.0.0.1:4199 \
19
+ --seed-json '<output of the seed script>' \
20
+ --cwd /tmp/f3-demo-repo \
21
+ --prompt 'I want to add tagging and search. Plan it first.'
22
+ */
23
+ import { query } from '@anthropic-ai/claude-agent-sdk';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { existsSync } from 'node:fs';
26
+ import path from 'node:path';
27
+ import { spawnSync } from 'node:child_process';
28
+
29
+ const args = process.argv.slice(2);
30
+ const argValue = (flag) => {
31
+ const index = args.indexOf(flag);
32
+ return index >= 0 ? args[index + 1] : undefined;
33
+ };
34
+
35
+ const apiUrl = argValue('--api-url') ?? 'http://127.0.0.1:4199';
36
+ const seed = JSON.parse(argValue('--seed-json') ?? '{}');
37
+ const cwd = argValue('--cwd') ?? '/tmp/f3-demo-repo';
38
+ const userPrompt = argValue('--prompt');
39
+ if (!seed.token || !seed.project_id || !userPrompt) {
40
+ console.error('missing --seed-json (token, project_id) or --prompt');
41
+ process.exit(2);
42
+ }
43
+
44
+ const packageRoot = path.resolve(
45
+ path.dirname(fileURLToPath(import.meta.url)),
46
+ '..'
47
+ );
48
+
49
+ // Compile the exact production developer instructions for a project-bound
50
+ // Claude Code job (planTool todo-write, projectBound true) via the real
51
+ // compiler - no hand-written prompt text in this harness.
52
+ // tsx may be hoisted to the workspace root by pnpm.
53
+ const tsxBin = [
54
+ path.join(packageRoot, 'node_modules', '.bin', 'tsx'),
55
+ path.join(packageRoot, '..', '..', 'node_modules', '.bin', 'tsx'),
56
+ ].find((candidate) => existsSync(candidate));
57
+ const compiled = spawnSync(
58
+ tsxBin,
59
+ [
60
+ '-e',
61
+ `import { commanderDeveloperInstructions } from './src/commanderDeveloperInstructions';
62
+ process.stdout.write(commanderDeveloperInstructions({
63
+ cwd: ${JSON.stringify(cwd)},
64
+ agentLabel: 'Claude Code',
65
+ planTool: 'todo-write',
66
+ developerPrompt: undefined,
67
+ projectBound: true,
68
+ linzumiContext: undefined,
69
+ }));`,
70
+ ],
71
+ { cwd: packageRoot, encoding: 'utf8' }
72
+ );
73
+ if (compiled.status !== 0) {
74
+ console.error('failed to compile developer instructions:', compiled.stderr);
75
+ process.exit(2);
76
+ }
77
+ const developerInstructions = compiled.stdout;
78
+ console.log(
79
+ `=== compiled developer instructions: ${developerInstructions.length} chars, ` +
80
+ `projectBound directives present: ${developerInstructions.includes('project task DAG')}`
81
+ );
82
+
83
+ const toolCalls = [];
84
+ let exitPlanModeSeen = false;
85
+
86
+ const session = query({
87
+ prompt: userPrompt,
88
+ options: {
89
+ cwd,
90
+ // Production wiring parity (claudeCodeSession.ts): preset system prompt
91
+ // with the compiled Commander developer instructions appended.
92
+ systemPrompt: {
93
+ type: 'preset',
94
+ preset: 'claude_code',
95
+ append: developerInstructions,
96
+ },
97
+ // The user chose plan mode for this planning session.
98
+ permissionMode: 'plan',
99
+ allowedTools: ['mcp__linzumi__*'],
100
+ mcpServers: {
101
+ linzumi: {
102
+ type: 'stdio',
103
+ command: process.execPath,
104
+ args: [
105
+ path.join(packageRoot, 'bin', 'linzumi.js'),
106
+ 'mcp',
107
+ 'server',
108
+ '--api-url',
109
+ apiUrl,
110
+ '--mode',
111
+ 'text',
112
+ '--tool-scope',
113
+ 'all',
114
+ ],
115
+ env: {
116
+ ...process.env,
117
+ LINZUMI_MCP_ACCESS_TOKEN: seed.token,
118
+ },
119
+ },
120
+ },
121
+ // Approve the plan-mode exit (the human accepting the plan) and demo-repo
122
+ // edits; the DAG transfer itself must be prompt-driven, not approved into
123
+ // existence - MCP tools are already trusted via allowedTools.
124
+ canUseTool: async (toolName, input) => {
125
+ if (toolName === 'ExitPlanMode') {
126
+ exitPlanModeSeen = true;
127
+ console.log('\n=== ExitPlanMode requested; approving (plan accepted)');
128
+ }
129
+ return { behavior: 'allow', updatedInput: input };
130
+ },
131
+ },
132
+ });
133
+
134
+ for await (const message of session) {
135
+ if (message.type === 'assistant') {
136
+ for (const block of message.message.content ?? []) {
137
+ if (block.type === 'tool_use') {
138
+ toolCalls.push(block.name);
139
+ const summary =
140
+ block.name.startsWith('mcp__linzumi__') ||
141
+ block.name === 'ExitPlanMode'
142
+ ? JSON.stringify(block.input).slice(0, 400)
143
+ : '';
144
+ console.log(`[tool_use] ${block.name} ${summary}`);
145
+ } else if (block.type === 'text' && block.text.trim() !== '') {
146
+ console.log(`[assistant] ${block.text.slice(0, 600)}`);
147
+ }
148
+ }
149
+ } else if (message.type === 'result') {
150
+ console.log(`\n=== result: ${message.subtype}; turns=${message.num_turns}`);
151
+ }
152
+ }
153
+
154
+ const dagCalls = toolCalls.filter((name) =>
155
+ name.startsWith('mcp__linzumi__linzumi_')
156
+ );
157
+ console.log('\n=== summary');
158
+ console.log(`ExitPlanMode seen: ${exitPlanModeSeen}`);
159
+ console.log(`linzumi MCP calls: ${JSON.stringify(dagCalls)}`);
160
+ const reviewedFirst =
161
+ dagCalls.findIndex((name) => name.includes('get_project_tasks')) !== -1 &&
162
+ dagCalls.findIndex((name) => name.includes('get_project_tasks')) <
163
+ Math.max(
164
+ 0,
165
+ dagCalls.findIndex((name) => name.includes('create_project_task'))
166
+ );
167
+ console.log(`reviewed project state before creating: ${reviewedFirst}`);
@@ -0,0 +1,130 @@
1
+ /*
2
+ - Date: 2026-07-04
3
+ Spec: plans/2026-07-04-projects-followups-stack.md (F3)
4
+ Relationship: DAG snapshot + negative-guarantee verifier for the F3
5
+ behavioral proof (scripts/f3-planning-dag-e2e.mjs). Reads the project graph
6
+ through the REAL shipped stdio MCP server and either prints a snapshot
7
+ (--snapshot out.json) or asserts guarantees against a prior snapshot
8
+ (--baseline prior.json): a named user-created task survives untouched
9
+ (--user-task "title"), no duplicate titles appear, and baseline tasks were
10
+ updated rather than deleted (every baseline id still present).
11
+
12
+ Usage:
13
+ node scripts/f3-planning-dag-verify.mjs --api-url ... --seed-json '...' \
14
+ --snapshot after-session-1.json --user-task "Write launch announcement" \
15
+ [--baseline after-session-0.json]
16
+ */
17
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { readFileSync, writeFileSync } from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ const args = process.argv.slice(2);
24
+ const argValue = (flag) => {
25
+ const index = args.indexOf(flag);
26
+ return index >= 0 ? args[index + 1] : undefined;
27
+ };
28
+
29
+ const apiUrl = argValue('--api-url') ?? 'http://127.0.0.1:4199';
30
+ const seed = JSON.parse(argValue('--seed-json') ?? '{}');
31
+ const snapshotPath = argValue('--snapshot');
32
+ const baselinePath = argValue('--baseline');
33
+ const userTaskTitle = argValue('--user-task');
34
+ if (!seed.token || !seed.project_id) {
35
+ console.error('missing --seed-json (token, project_id)');
36
+ process.exit(2);
37
+ }
38
+
39
+ const packageRoot = path.resolve(
40
+ path.dirname(fileURLToPath(import.meta.url)),
41
+ '..'
42
+ );
43
+
44
+ let failures = 0;
45
+ const check = (label, condition) => {
46
+ const mark = condition ? 'PASS' : 'FAIL';
47
+ if (!condition) failures += 1;
48
+ console.log(` [${mark}] ${label}`);
49
+ };
50
+
51
+ const client = new Client({ name: 'f3-dag-verify', version: '0.0.0' });
52
+ const transport = new StdioClientTransport({
53
+ command: process.execPath,
54
+ args: [
55
+ path.join(packageRoot, 'bin', 'linzumi.js'),
56
+ 'mcp',
57
+ 'server',
58
+ '--api-url',
59
+ apiUrl,
60
+ '--mode',
61
+ 'text',
62
+ '--tool-scope',
63
+ 'all',
64
+ ],
65
+ env: { ...process.env, LINZUMI_MCP_ACCESS_TOKEN: seed.token },
66
+ });
67
+ await client.connect(transport);
68
+
69
+ const result = await client.callTool({
70
+ name: 'linzumi_get_project_tasks',
71
+ arguments: { project_id: seed.project_id },
72
+ });
73
+ const graph = JSON.parse(result.content?.[0]?.text ?? '{}');
74
+ const tasks = graph.tasks ?? [];
75
+
76
+ console.log('=== project task DAG snapshot');
77
+ const byId = new Map(tasks.map((task) => [task.task_id, task]));
78
+ let edgeCount = 0;
79
+ for (const task of tasks) {
80
+ const parent = task.parent_task_id
81
+ ? byId.get(task.parent_task_id)?.title
82
+ : undefined;
83
+ const deps = (task.depends_on_task_ids ?? [])
84
+ .map((dep) => byId.get(dep)?.title ?? dep)
85
+ .join(', ');
86
+ edgeCount += (task.depends_on_task_ids ?? []).length;
87
+ console.log(
88
+ ` [${task.kind}] "${task.title}"` +
89
+ (parent ? ` (in "${parent}")` : '') +
90
+ (task.status ? ` status=${task.status}` : '') +
91
+ (task.work_state ? ` work_state=${task.work_state}` : '') +
92
+ (deps ? ` blocked-by: ${deps}` : '')
93
+ );
94
+ }
95
+ console.log(` total: ${tasks.length} tasks, ${edgeCount} dependency edges`);
96
+
97
+ if (snapshotPath) {
98
+ writeFileSync(snapshotPath, JSON.stringify(graph, null, 2));
99
+ console.log(`snapshot written: ${snapshotPath}`);
100
+ }
101
+
102
+ console.log('\n=== guarantees');
103
+ if (userTaskTitle) {
104
+ check(
105
+ `user-created task "${userTaskTitle}" still present (never deleted)`,
106
+ tasks.some((task) => task.title === userTaskTitle)
107
+ );
108
+ }
109
+ const titles = tasks.map((task) => task.title);
110
+ check(
111
+ 'no duplicate task titles (updated, not re-created)',
112
+ new Set(titles).size === titles.length
113
+ );
114
+ if (baselinePath) {
115
+ const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
116
+ const baselineTasks = baseline.tasks ?? [];
117
+ const currentIds = new Set(tasks.map((task) => task.task_id));
118
+ for (const prior of baselineTasks) {
119
+ check(
120
+ `baseline task "${prior.title}" (id ${prior.task_id}) not deleted`,
121
+ currentIds.has(prior.task_id)
122
+ );
123
+ }
124
+ console.log(
125
+ `baseline: ${baselineTasks.length} tasks; current: ${tasks.length} tasks`
126
+ );
127
+ }
128
+
129
+ await client.close();
130
+ process.exit(failures === 0 ? 0 : 1);