@chemx/create-chemx 26.9.25-747 → 26.9.25-943
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 +2 -2
- package/blueprints/molecule-capsule/m-sample-card.spec.ts +10 -4
- package/cli/commands/cmd-router.js +15 -1
- package/cli/commands-schema.js +36 -0
- package/cli/help.d.ts +2 -0
- package/cli/help.js +64 -0
- package/cli/help.spec.js +54 -1
- package/cli/reader-shorthand.spec.js +2 -2
- package/cli/scaffold.js +35 -4
- package/cli/team/team-adversarial-locks.spec.js +71 -0
- package/cli/team/team-commands-lock.js +64 -0
- package/cli/team/team-commands.js +16 -23
- package/cli/team/team-db-locks.js +1 -1
- package/cli/team/team-flags.js +4 -1
- package/cli/team/team-format.js +88 -0
- package/cli/team/team-memory.js +73 -10
- package/cli/team/team-memory.spec.js +25 -0
- package/cli/team/team-multiprocess-concurrency.spec.js +1 -1
- package/cli/theme.js +1 -0
- package/cli/ui-handlers.js +1 -1
- package/cli/verify-helpers.js +34 -15
- package/cli/verify.spec.js +35 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ npx create-chemx my-molecular-app --framework=react --install --yes
|
|
|
59
59
|
|
|
60
60
|
### Package Architecture: Scaffolder vs Command Engine
|
|
61
61
|
Chemical X provides two coordinated packages:
|
|
62
|
-
- **`create-chemx`**: Dedicated zero-
|
|
62
|
+
- **`create-chemx`**: Dedicated zero-configuration project scaffolder (`npm create chemx`). Directly provisions project templates, test suites, and architectural configurations.
|
|
63
63
|
- **`chemx`**: The full Molecular Architecture CLI & AST Query Engine. Manages audits, verifications, AST query lookups, code patching, and multi-agent swarm task coordination.
|
|
64
64
|
|
|
65
65
|
```bash
|
|
@@ -341,7 +341,7 @@ Chemical X enforces seven core architectural directives configured via `chemx pi
|
|
|
341
341
|
|
|
342
342
|
## Model Context Protocol (MCP) Server
|
|
343
343
|
|
|
344
|
-
Chemical X includes a high-performance,
|
|
344
|
+
Chemical X includes a high-performance, native Node.js JSON-RPC 2.0 Stdio MCP server that connects directly to AI agent hosts (Cursor, Claude Desktop, Windsurf, Antigravity, VS Code).
|
|
345
345
|
|
|
346
346
|
### Automatic 1-Step Installation
|
|
347
347
|
|
|
@@ -3,6 +3,12 @@ import React from 'react';
|
|
|
3
3
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
4
4
|
import { MSampleCard, resolveBadgeDescriptor } from './m-sample-card';
|
|
5
5
|
|
|
6
|
+
interface TestElementProps {
|
|
7
|
+
children?: React.ReactElement<TestElementProps>[];
|
|
8
|
+
className?: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
describe('m-sample-card: resolveBadgeDescriptor', () => {
|
|
7
13
|
it('returns archived badge descriptor when status is archived', () => {
|
|
8
14
|
const descriptor = resolveBadgeDescriptor('archived', false);
|
|
@@ -60,15 +66,15 @@ describe('m-sample-card: MSampleCard Component', () => {
|
|
|
60
66
|
title: 'Actionable Unit',
|
|
61
67
|
value: 1200,
|
|
62
68
|
onAction: handleAction
|
|
63
|
-
}) as React.ReactElement
|
|
69
|
+
}) as React.ReactElement<TestElementProps>;
|
|
64
70
|
|
|
65
|
-
const bodyNode = (vnode.props.children as React.ReactElement[])[1];
|
|
66
|
-
const buttonNode = (bodyNode.props.children as React.ReactElement[])[1];
|
|
71
|
+
const bodyNode = (vnode.props.children as React.ReactElement<TestElementProps>[])[1];
|
|
72
|
+
const buttonNode = (bodyNode.props.children as React.ReactElement<TestElementProps>[])[1];
|
|
67
73
|
|
|
68
74
|
expect(buttonNode).toBeDefined();
|
|
69
75
|
expect(buttonNode.props.className).toBe('m-sample-card__action');
|
|
70
76
|
|
|
71
|
-
buttonNode.props.onClick();
|
|
77
|
+
buttonNode.props.onClick?.();
|
|
72
78
|
expect(handleAction).toHaveBeenCalledTimes(1);
|
|
73
79
|
});
|
|
74
80
|
});
|
|
@@ -94,12 +94,26 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
|
|
|
94
94
|
break;
|
|
95
95
|
}
|
|
96
96
|
case 'init': {
|
|
97
|
+
const isHelpRequested = rawArgs.includes('--help') || rawArgs.includes('-h') || rawArgs[1] === 'help';
|
|
98
|
+
if (isHelpRequested) {
|
|
99
|
+
const { printInitHelp } = await import('../help.js');
|
|
100
|
+
printInitHelp();
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const nonFlagArgs = rawArgs.slice(1).filter((arg) => !arg.startsWith('-'));
|
|
104
|
+
const targetSubDir = nonFlagArgs[0] || 'src/chemical-x';
|
|
97
105
|
const { runInit } = await import('../scaffold.js');
|
|
98
|
-
await runInit(
|
|
106
|
+
await runInit(targetSubDir, rawArgs, runAudit);
|
|
99
107
|
break;
|
|
100
108
|
}
|
|
101
109
|
case 'create':
|
|
102
110
|
case 'scaffold': {
|
|
111
|
+
const isHelpRequested = rawArgs.includes('--help') || rawArgs.includes('-h') || rawArgs[1] === 'help';
|
|
112
|
+
if (isHelpRequested) {
|
|
113
|
+
const { printScaffoldHelp } = await import('../help.js');
|
|
114
|
+
printScaffoldHelp();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
103
117
|
const { runScaffold } = await import('../scaffold.js');
|
|
104
118
|
const nonFlagArgs = rawArgs.slice(1).filter((arg) => !arg.startsWith('-'));
|
|
105
119
|
await runScaffold(nonFlagArgs[0], rawArgs, runAudit);
|
package/cli/commands-schema.js
CHANGED
|
@@ -222,5 +222,41 @@ export const COMMANDS_SCHEMA = [
|
|
|
222
222
|
'npx chemx tesseract',
|
|
223
223
|
'npx chemx tesseract --json'
|
|
224
224
|
]
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'init',
|
|
228
|
+
aliases: [],
|
|
229
|
+
usage: 'npx chemx init [directory] [options]',
|
|
230
|
+
summary: 'Unpack blueprints and molecular architecture drop-in files into an existing codebase.',
|
|
231
|
+
description: 'Installs blueprints, domain hooks, and foundation capsules into target directory (default: src/chemical-x).',
|
|
232
|
+
flags: [
|
|
233
|
+
{ flag: '-h, --help', desc: 'Show this help message' },
|
|
234
|
+
{ flag: '--license=<key>', desc: 'Provide commercial license key for enterprise starter kit assets' }
|
|
235
|
+
],
|
|
236
|
+
examples: [
|
|
237
|
+
'npx chemx init',
|
|
238
|
+
'npx chemx init src/chemical-x',
|
|
239
|
+
'npx chemx init packages/ui/src/modules'
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'create',
|
|
244
|
+
aliases: ['scaffold'],
|
|
245
|
+
usage: 'npm create chemx [directory] [options]',
|
|
246
|
+
summary: 'Scaffold a complete new Chemical X Molecular Architecture application.',
|
|
247
|
+
description: 'Generates a full starter project configured with Chemical X molecular architecture, framework presets, and agent steering directives.',
|
|
248
|
+
flags: [
|
|
249
|
+
{ flag: '--framework=<id>', desc: 'Framework flavor: react (default), vue, svelte' },
|
|
250
|
+
{ flag: '--yes, -y', desc: 'Skip interactive prompts and scaffold Community Edition immediately' },
|
|
251
|
+
{ flag: '--install', desc: 'Auto-install dependencies after scaffolding' },
|
|
252
|
+
{ flag: '--headless', desc: 'Run in headless mode for CI/CD and AI agent automation' },
|
|
253
|
+
{ flag: '-h, --help', desc: 'Show this help message' },
|
|
254
|
+
{ flag: '-v, --version', desc: 'Show version number' }
|
|
255
|
+
],
|
|
256
|
+
examples: [
|
|
257
|
+
'npm create chemx my-molecular-app --framework=react',
|
|
258
|
+
'npx create-chemx my-vue-app --framework=vue --yes',
|
|
259
|
+
'npx chemx create my-app --framework=svelte --install'
|
|
260
|
+
]
|
|
225
261
|
}
|
|
226
262
|
];
|
package/cli/help.d.ts
CHANGED
package/cli/help.js
CHANGED
|
@@ -54,3 +54,67 @@ export const printHelp = () => {
|
|
|
54
54
|
|
|
55
55
|
process.stdout.write(lines.join('\n'));
|
|
56
56
|
};
|
|
57
|
+
|
|
58
|
+
export const printInitHelp = () => {
|
|
59
|
+
renderBanner('Chemical X: In-Repo Capsule Drop-in (chemx init)');
|
|
60
|
+
|
|
61
|
+
const BOLD = '\x1b[1m';
|
|
62
|
+
const CYAN = '\x1b[36m';
|
|
63
|
+
const RESET = '\x1b[0m';
|
|
64
|
+
|
|
65
|
+
const lines = [
|
|
66
|
+
`${BOLD}USAGE${RESET}`,
|
|
67
|
+
` ${CYAN}npx chemx init${RESET} [directory] [options]`,
|
|
68
|
+
'',
|
|
69
|
+
`${BOLD}DESCRIPTION${RESET}`,
|
|
70
|
+
' Unpack Chemical X blueprints and molecular architecture drop-in files',
|
|
71
|
+
' into an existing codebase (defaults to src/chemical-x).',
|
|
72
|
+
'',
|
|
73
|
+
`${BOLD}OPTIONS${RESET}`,
|
|
74
|
+
` ${CYAN}-h, --help${RESET} Show this help message`,
|
|
75
|
+
` ${CYAN}--license=<key>${RESET} Provide commercial license key for enterprise starter kit assets`,
|
|
76
|
+
'',
|
|
77
|
+
`${BOLD}EXAMPLES${RESET}`,
|
|
78
|
+
` ${CYAN}npx chemx init${RESET}`,
|
|
79
|
+
` ${CYAN}npx chemx init src/chemical-x${RESET}`,
|
|
80
|
+
` ${CYAN}npx chemx init packages/ui/src/modules${RESET}`,
|
|
81
|
+
''
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
process.stdout.write(lines.join('\n'));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const printScaffoldHelp = () => {
|
|
88
|
+
renderBanner('Chemical X: Project Scaffolder (npm create chemx)');
|
|
89
|
+
|
|
90
|
+
const BOLD = '\x1b[1m';
|
|
91
|
+
const CYAN = '\x1b[36m';
|
|
92
|
+
const RESET = '\x1b[0m';
|
|
93
|
+
|
|
94
|
+
const lines = [
|
|
95
|
+
`${BOLD}USAGE${RESET}`,
|
|
96
|
+
` ${CYAN}npm create chemx${RESET} [directory] [options]`,
|
|
97
|
+
` ${CYAN}npx create-chemx${RESET} [directory] [options]`,
|
|
98
|
+
` ${CYAN}npx chemx create${RESET} [directory] [options]`,
|
|
99
|
+
'',
|
|
100
|
+
`${BOLD}DESCRIPTION${RESET}`,
|
|
101
|
+
' Scaffold a complete new Chemical X Molecular Architecture application.',
|
|
102
|
+
'',
|
|
103
|
+
`${BOLD}OPTIONS${RESET}`,
|
|
104
|
+
` ${CYAN}--framework=<id>${RESET} Framework flavor: react (default), vue, svelte`,
|
|
105
|
+
` ${CYAN}--install${RESET} Auto-install dependencies after scaffolding`,
|
|
106
|
+
` ${CYAN}--skip-install${RESET} Skip installing dependencies`,
|
|
107
|
+
` ${CYAN}--yes, -y${RESET} Skip interactive prompts and scaffold Community Edition immediately`,
|
|
108
|
+
` ${CYAN}--headless${RESET} Run in headless mode for CI/CD and AI agent automation`,
|
|
109
|
+
` ${CYAN}-h, --help${RESET} Show this help message`,
|
|
110
|
+
` ${CYAN}-v, --version${RESET} Show version number`,
|
|
111
|
+
'',
|
|
112
|
+
`${BOLD}EXAMPLES${RESET}`,
|
|
113
|
+
` ${CYAN}npm create chemx my-molecular-app --framework=react${RESET}`,
|
|
114
|
+
` ${CYAN}npx create-chemx my-vue-app --framework=vue --yes${RESET}`,
|
|
115
|
+
` ${CYAN}npx chemx create my-app --framework=svelte --install${RESET}`,
|
|
116
|
+
''
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
process.stdout.write(lines.join('\n'));
|
|
120
|
+
};
|
package/cli/help.spec.js
CHANGED
|
@@ -4,7 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { COMMANDS_SCHEMA } from './commands-schema.js';
|
|
7
|
-
import { printHelp } from './help.js';
|
|
7
|
+
import { printHelp, printInitHelp, printScaffoldHelp } from './help.js';
|
|
8
8
|
|
|
9
9
|
test('commands-schema: contains all 9 core chemical-x CLI commands', () => {
|
|
10
10
|
const expected = ['search', 'read', 'patch', 'mcp', 'build', 'audit', 'generate', 'team', 'verify'];
|
|
@@ -54,3 +54,56 @@ test('cli: unknown command exits immediately with code 1 and error message', ()
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
test('help: printInitHelp and printScaffoldHelp render without throwing', () => {
|
|
59
|
+
assert.doesNotThrow(() => {
|
|
60
|
+
printInitHelp();
|
|
61
|
+
});
|
|
62
|
+
assert.doesNotThrow(() => {
|
|
63
|
+
printScaffoldHelp();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('cli: chemx init --help and -h display usage and do not scaffold /--help/ on disk', () => {
|
|
68
|
+
const cliPath = path.resolve('cli/index.js');
|
|
69
|
+
const helpFlags = ['--help', '-h', 'help'];
|
|
70
|
+
|
|
71
|
+
for (const flag of helpFlags) {
|
|
72
|
+
const res = spawnSync(process.execPath, [cliPath, 'init', flag], {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
timeout: 5000
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(res.status, 0, `chemx init ${flag} should exit with code 0`);
|
|
78
|
+
assert.ok(
|
|
79
|
+
res.stdout.includes('chemx init') && res.stdout.includes('USAGE'),
|
|
80
|
+
`chemx init ${flag} output should include usage instructions`
|
|
81
|
+
);
|
|
82
|
+
assert.strictEqual(
|
|
83
|
+
fs.existsSync(path.resolve(flag)),
|
|
84
|
+
false,
|
|
85
|
+
`chemx init ${flag} must not create directory "${flag}" on disk`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('cli: chemx create --help and scaffold -h display usage', () => {
|
|
91
|
+
const cliPath = path.resolve('cli/index.js');
|
|
92
|
+
const commands = [
|
|
93
|
+
['create', '--help'],
|
|
94
|
+
['scaffold', '-h']
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const [cmd, flag] of commands) {
|
|
98
|
+
const res = spawnSync(process.execPath, [cliPath, cmd, flag], {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
timeout: 5000
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.strictEqual(res.status, 0, `chemx ${cmd} ${flag} should exit with code 0`);
|
|
104
|
+
assert.ok(
|
|
105
|
+
res.stdout.includes('USAGE') && (res.stdout.includes('create chemx') || res.stdout.includes('chemx create')),
|
|
106
|
+
`chemx ${cmd} ${flag} output should include scaffolder usage instructions`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
@@ -49,12 +49,12 @@ describe('Reader Shorthand Syntax', () => {
|
|
|
49
49
|
it('supports MCP chemx({ action: "r" })', async () => {
|
|
50
50
|
const output = await handleChemx({ action: 'r', params: { path: 'package.json:1-3' } });
|
|
51
51
|
assert.ok(output.includes('package.json'));
|
|
52
|
-
assert.ok(output.includes('"name":
|
|
52
|
+
assert.ok(output.includes('"name":'));
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
it('supports MCP chemx({ command: "r path:1-3" })', async () => {
|
|
56
56
|
const output = await handleChemx({ command: 'r package.json:1-3' });
|
|
57
57
|
assert.ok(output.includes('package.json'));
|
|
58
|
-
assert.ok(output.includes('"name":
|
|
58
|
+
assert.ok(output.includes('"name":'));
|
|
59
59
|
});
|
|
60
60
|
});
|
package/cli/scaffold.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runPillarsWizard } from './pillars-wizard.js';
|
|
|
7
7
|
import { resolvePackageManager } from './build/detector.js';
|
|
8
8
|
import { resolveFramework } from './project-detector.js';
|
|
9
9
|
import { getFrameworkConfig, buildScaffoldPackageJson } from './scaffold-frameworks.js';
|
|
10
|
+
import { printInitHelp, printScaffoldHelp } from './help.js';
|
|
10
11
|
|
|
11
12
|
const extractTargetName = (projectName, rawArgs) => {
|
|
12
13
|
if (projectName && !projectName.startsWith('-')) return projectName;
|
|
@@ -25,6 +26,19 @@ const extractTargetName = (projectName, rawArgs) => {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null) => {
|
|
29
|
+
const isHelpRequested =
|
|
30
|
+
rawArgs.includes('--help') ||
|
|
31
|
+
rawArgs.includes('-h') ||
|
|
32
|
+
rawArgs.includes('help') ||
|
|
33
|
+
projectName === '--help' ||
|
|
34
|
+
projectName === '-h' ||
|
|
35
|
+
projectName === 'help';
|
|
36
|
+
|
|
37
|
+
if (isHelpRequested) {
|
|
38
|
+
printScaffoldHelp();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
renderBanner('Chemical X: Molecular Architecture Scaffolder (npm create chemx)');
|
|
29
43
|
|
|
30
44
|
const fwArg = (rawArgs.find((a) => a.startsWith('--framework=')) || '').split('=')[1]
|
|
@@ -125,9 +139,26 @@ export const runScaffold = async (projectName, rawArgs = [], onRunAudit = null)
|
|
|
125
139
|
};
|
|
126
140
|
|
|
127
141
|
export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onRunAudit = null) => {
|
|
142
|
+
const isHelpRequested =
|
|
143
|
+
rawArgs.includes('--help') ||
|
|
144
|
+
rawArgs.includes('-h') ||
|
|
145
|
+
rawArgs.includes('help') ||
|
|
146
|
+
targetSubDir === '--help' ||
|
|
147
|
+
targetSubDir === '-h' ||
|
|
148
|
+
targetSubDir === 'help';
|
|
149
|
+
|
|
150
|
+
if (isHelpRequested) {
|
|
151
|
+
printInitHelp();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const safeTargetSubDir = (targetSubDir && !targetSubDir.startsWith('-'))
|
|
156
|
+
? targetSubDir
|
|
157
|
+
: (rawArgs.slice(1).find((arg) => !arg.startsWith('-')) || 'src/chemical-x');
|
|
158
|
+
|
|
128
159
|
renderBanner('Chemical X: In-Repo Capsule Drop-in');
|
|
129
160
|
|
|
130
|
-
const targetDir = path.resolve(process.cwd(),
|
|
161
|
+
const targetDir = path.resolve(process.cwd(), safeTargetSubDir);
|
|
131
162
|
const licenseKey = await obtainLicenseKey(rawArgs, onRunAudit);
|
|
132
163
|
let files = {};
|
|
133
164
|
|
|
@@ -141,7 +172,7 @@ export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onR
|
|
|
141
172
|
files = await fetchStarterKitFiles(licenseKey);
|
|
142
173
|
}
|
|
143
174
|
|
|
144
|
-
process.stdout.write(`Unpacking blueprints and hooks into: \x1b[36m${
|
|
175
|
+
process.stdout.write(`Unpacking blueprints and hooks into: \x1b[36m${safeTargetSubDir}/\x1b[0m\n`);
|
|
145
176
|
|
|
146
177
|
let count = 0;
|
|
147
178
|
for (const [relPath, content] of Object.entries(files)) {
|
|
@@ -160,9 +191,9 @@ export const runInit = async (targetSubDir = 'src/chemical-x', rawArgs = [], onR
|
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
process.stdout.write(
|
|
163
|
-
`\n\x1b[1m\x1b[32m✔ Successfully installed ${count} Chemical X assets into ${
|
|
194
|
+
`\n\x1b[1m\x1b[32m✔ Successfully installed ${count} Chemical X assets into ${safeTargetSubDir}!\x1b[0m\n\n`
|
|
164
195
|
);
|
|
165
196
|
};
|
|
166
197
|
|
|
167
198
|
export { runGenerateCapsule, runGenerateWizard } from './generator.js';
|
|
168
|
-
export { printHelp } from './help.js';
|
|
199
|
+
export { printHelp, printInitHelp, printScaffoldHelp } from './help.js';
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import test from 'node:test';
|
|
7
7
|
import assert from 'node:assert/strict';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
8
11
|
import { DatabaseSync } from 'node:sqlite';
|
|
12
|
+
import { openIndexDb } from '../search-db.js';
|
|
13
|
+
import { runTeamCli } from './team-commands.js';
|
|
9
14
|
import { initTeamSchema } from './team-schema.js';
|
|
10
15
|
import {
|
|
11
16
|
requestFileLock,
|
|
@@ -103,3 +108,69 @@ test('adversarial-locks: renewal idempotency, rogue release rejection, and expir
|
|
|
103
108
|
assert.equal(expired.length, 1);
|
|
104
109
|
assert.equal(getFileLockStatus(db, file).lease.locked_by, '@next-in-line');
|
|
105
110
|
});
|
|
111
|
+
|
|
112
|
+
test('adversarial-locks: CLI mutual exclusion between two separate agent invocations prevents overwrite and queues second agent', () => {
|
|
113
|
+
const tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-lock-exclusion-'));
|
|
114
|
+
try {
|
|
115
|
+
const db = openIndexDb(tmpCwd);
|
|
116
|
+
initTeamSchema(db);
|
|
117
|
+
const testFile = 'src/components/button.vue';
|
|
118
|
+
|
|
119
|
+
// Agent 1 acquires the lock via CLI
|
|
120
|
+
const res1 = runTeamCli(['lock', 'acquire', testFile, '--as', '@agent-1'], false, tmpCwd);
|
|
121
|
+
assert.equal(res1.granted, true);
|
|
122
|
+
assert.equal(res1.lease.locked_by, '@agent-1');
|
|
123
|
+
|
|
124
|
+
// Agent 2 attempts to acquire the lock on the same file via CLI
|
|
125
|
+
const res2 = runTeamCli(['lock', 'acquire', testFile, '--as', '@agent-2'], false, tmpCwd);
|
|
126
|
+
assert.equal(res2.granted, false);
|
|
127
|
+
assert.equal(res2.queued, true);
|
|
128
|
+
assert.equal(res2.position, 1);
|
|
129
|
+
assert.equal(res2.currentHolder, '@agent-1');
|
|
130
|
+
|
|
131
|
+
// Verify .chemx/index.db state: Agent 1's lease was NOT overwritten
|
|
132
|
+
const leaseRow = db.prepare('SELECT * FROM file_leases WHERE file_path = ?').get(testFile);
|
|
133
|
+
assert.ok(leaseRow, 'Lease row must exist');
|
|
134
|
+
assert.equal(leaseRow.locked_by, '@agent-1', 'Lease must still be held by agent-1, never overwritten');
|
|
135
|
+
|
|
136
|
+
// Verify Agent 2 is recorded in file_lock_queue
|
|
137
|
+
const queueRows = db.prepare('SELECT * FROM file_lock_queue WHERE file_path = ?').all(testFile);
|
|
138
|
+
assert.equal(queueRows.length, 1);
|
|
139
|
+
assert.equal(queueRows[0].agent_id, '@agent-2');
|
|
140
|
+
assert.equal(queueRows[0].status, 'waiting');
|
|
141
|
+
|
|
142
|
+
// Agent 1 releases the lock via CLI
|
|
143
|
+
const relRes = runTeamCli(['lock', 'release', testFile, '--as', '@agent-1'], false, tmpCwd);
|
|
144
|
+
assert.equal(relRes.success, true);
|
|
145
|
+
assert.equal(relRes.promotedWaiter, '@agent-2');
|
|
146
|
+
|
|
147
|
+
// Verify Agent 2 was promoted to active lease
|
|
148
|
+
const promotedLease = db.prepare('SELECT * FROM file_leases WHERE file_path = ?').get(testFile);
|
|
149
|
+
assert.equal(promotedLease.locked_by, '@agent-2');
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('team-cli: help commands display correctly and do not fall through to list', () => {
|
|
156
|
+
const tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'chemx-help-test-'));
|
|
157
|
+
try {
|
|
158
|
+
const db = openIndexDb(tmpCwd);
|
|
159
|
+
initTeamSchema(db);
|
|
160
|
+
|
|
161
|
+
const teamHelp = runTeamCli(['--help'], false, tmpCwd);
|
|
162
|
+
assert.equal(teamHelp.help, true);
|
|
163
|
+
assert.ok(teamHelp.commands.includes('task'));
|
|
164
|
+
|
|
165
|
+
const taskHelp = runTeamCli(['task', '--help'], false, tmpCwd);
|
|
166
|
+
assert.equal(taskHelp.help, true);
|
|
167
|
+
assert.ok(taskHelp.actions.includes('list'));
|
|
168
|
+
assert.ok(taskHelp.actions.includes('claim'));
|
|
169
|
+
|
|
170
|
+
const lockHelp = runTeamCli(['lock', '--help'], false, tmpCwd);
|
|
171
|
+
assert.equal(lockHelp.help, true);
|
|
172
|
+
assert.ok(lockHelp.actions.includes('acquire'));
|
|
173
|
+
} finally {
|
|
174
|
+
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { requestFileLock, releaseFileLock } from './team-db.js';
|
|
2
|
+
import { formatLockHelpCard } from './team-format.js';
|
|
3
|
+
|
|
4
|
+
export const handleLockCommand = (db, nonFlagPositional, flags, isCli) => {
|
|
5
|
+
const isLockHelp = flags.help || nonFlagPositional.includes('--help') || nonFlagPositional.includes('-h') || nonFlagPositional.includes('help');
|
|
6
|
+
if (isLockHelp) {
|
|
7
|
+
if (isCli) process.stdout.write(formatLockHelpCard());
|
|
8
|
+
return { help: true, actions: ['acquire', 'release'] };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let action = 'acquire';
|
|
12
|
+
let file = nonFlagPositional[0];
|
|
13
|
+
if (file === 'acquire') {
|
|
14
|
+
action = 'acquire';
|
|
15
|
+
file = nonFlagPositional[1];
|
|
16
|
+
} else if (file === 'release' || file === 'unlock') {
|
|
17
|
+
action = 'release';
|
|
18
|
+
file = nonFlagPositional[1];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!file) {
|
|
22
|
+
if (isCli) process.stderr.write('\x1b[31m✕ File path required: chemx team lock [acquire|release] <filePath>\x1b[0m\n');
|
|
23
|
+
return { error: 'filePath required' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (action === 'release') {
|
|
27
|
+
return handleUnlockCommand(db, [file], flags, isCli);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const res = requestFileLock(db, file, flags.as || '@agent', {
|
|
31
|
+
purpose: flags.purpose,
|
|
32
|
+
priority: flags.priority,
|
|
33
|
+
pid: flags.pid ? Number(flags.pid) : 0
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (isCli) {
|
|
37
|
+
if (flags.isJson) process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
38
|
+
else if (res.granted) process.stdout.write(`\x1b[32m✔\x1b[0m Acquired lock on ${file}\n`);
|
|
39
|
+
else process.stdout.write(`\x1b[33m⏳\x1b[0m Enqueued in FIFO lock queue at position ${res.position} (held by ${res.currentHolder})\n`);
|
|
40
|
+
}
|
|
41
|
+
return res;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const handleUnlockCommand = (db, nonFlagPositional, flags, isCli) => {
|
|
45
|
+
const isUnlockHelp = flags.help || nonFlagPositional.includes('--help') || nonFlagPositional.includes('-h') || nonFlagPositional.includes('help');
|
|
46
|
+
if (isUnlockHelp) {
|
|
47
|
+
if (isCli) process.stdout.write(formatLockHelpCard());
|
|
48
|
+
return { help: true, actions: ['release'] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const file = nonFlagPositional[0];
|
|
52
|
+
if (!file) {
|
|
53
|
+
if (isCli) process.stderr.write('\x1b[31m✕ File path required: chemx team unlock <filePath>\x1b[0m\n');
|
|
54
|
+
return { error: 'filePath required' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const res = releaseFileLock(db, file, flags.as || '@agent');
|
|
58
|
+
if (isCli) {
|
|
59
|
+
if (flags.isJson) process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
60
|
+
else if (res.success) process.stdout.write(`\x1b[32m✔\x1b[0m Released lock on ${file}\n`);
|
|
61
|
+
else process.stderr.write(`\x1b[31m✕ Unlock failed: ${res.reason}\x1b[0m\n`);
|
|
62
|
+
}
|
|
63
|
+
return res;
|
|
64
|
+
};
|
|
@@ -14,19 +14,18 @@ import {
|
|
|
14
14
|
createTask,
|
|
15
15
|
claimTask,
|
|
16
16
|
updateTaskStatus,
|
|
17
|
-
requestFileLock,
|
|
18
|
-
releaseFileLock,
|
|
19
17
|
registerAgent
|
|
20
18
|
} from './team-db.js';
|
|
21
19
|
import { getAgentMailbox, sendDirectMessage } from './team-db-mailbox.js';
|
|
22
20
|
import { autoGenerateTasksFromAudit, completeTaskWithAudit, queryUnassignedHazards, reconcileAuditTasks } from './team-triage.js';
|
|
23
|
-
import { formatSwarmStatusCard, formatFeedTimeline, formatTaskListCard, formatMailboxCard, formatTaskDetailCard } from './team-format.js';
|
|
21
|
+
import { formatSwarmStatusCard, formatFeedTimeline, formatTaskListCard, formatMailboxCard, formatTaskDetailCard, formatTeamHelpCard, formatTaskHelpCard } from './team-format.js';
|
|
24
22
|
import { getSwarmTokenBreakdown, formatTokenBreakdownCard } from './team-tokens.js';
|
|
25
23
|
import { runAblationComparison, formatAblationCard } from './team-memory.js';
|
|
26
24
|
import { parseFlags } from './team-flags.js';
|
|
27
25
|
import { handleTaskSlotCommand, handleTaskTraceCommand, handleTrainCommand } from './team-commands-vds.js';
|
|
26
|
+
import { handleLockCommand, handleUnlockCommand } from './team-commands-lock.js';
|
|
28
27
|
|
|
29
|
-
const ARG_VAL_FLAGS = ['--target', '--as', '--to', '--agent', '--since', '--limit', '--thread', '--task', '--parent', '--rule', '--priority', '--prio', '--moscow', '--url'];
|
|
28
|
+
const ARG_VAL_FLAGS = ['--target', '--as', '--to', '--agent', '--since', '--limit', '--thread', '--task', '--parent', '--rule', '--priority', '--prio', '--moscow', '--url', '--pid'];
|
|
30
29
|
|
|
31
30
|
export const runTeamCli = (rawArgs = [], isCli = false, cwd = process.cwd()) => {
|
|
32
31
|
const db = openIndexDb(cwd);
|
|
@@ -47,6 +46,12 @@ export const runTeamCli = (rawArgs = [], isCli = false, cwd = process.cwd()) =>
|
|
|
47
46
|
nonFlagPositional.push(a);
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
const isTeamHelp = subCommand === '--help' || subCommand === '-h' || subCommand === 'help' || (subCommand === 'status' && flags.help);
|
|
50
|
+
if (isTeamHelp) {
|
|
51
|
+
if (isCli) process.stdout.write(formatTeamHelpCard());
|
|
52
|
+
return { help: true, commands: ['status', 'task', 'lock', 'unlock', 'feed', 'post', 'inbox', 'dm', 'tokens', 'triage', 'benchmark', 'train'] };
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
if (subCommand === 'status') {
|
|
51
56
|
const status = getSwarmStatus(db);
|
|
52
57
|
if (flags.isJson) {
|
|
@@ -115,6 +120,11 @@ export const runTeamCli = (rawArgs = [], isCli = false, cwd = process.cwd()) =>
|
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
if (subCommand === 'task') {
|
|
123
|
+
const isTaskHelp = flags.help || nonFlagPositional.includes('--help') || nonFlagPositional.includes('-h') || nonFlagPositional.includes('help');
|
|
124
|
+
if (isTaskHelp) {
|
|
125
|
+
if (isCli) process.stdout.write(formatTaskHelpCard());
|
|
126
|
+
return { help: true, actions: ['list', 'show', 'add', 'claim', 'done', 'update', 'comment', 'triage', 'reconcile', 'set-target', 'vds-slot', 'trace'] };
|
|
127
|
+
}
|
|
118
128
|
const taskAction = nonFlagPositional[0] || 'list';
|
|
119
129
|
if (taskAction === 'list') {
|
|
120
130
|
const tasks = listTasks(db, {
|
|
@@ -386,28 +396,11 @@ export const runTeamCli = (rawArgs = [], isCli = false, cwd = process.cwd()) =>
|
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
if (subCommand === 'lock') {
|
|
389
|
-
|
|
390
|
-
const res = requestFileLock(db, file, flags.as || '@agent', {
|
|
391
|
-
purpose: flags.purpose,
|
|
392
|
-
priority: flags.priority
|
|
393
|
-
});
|
|
394
|
-
if (isCli) {
|
|
395
|
-
if (flags.isJson) process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
396
|
-
else if (res.granted) process.stdout.write(`\x1b[32m✔\x1b[0m Acquired lock on ${file}\n`);
|
|
397
|
-
else process.stdout.write(`\x1b[33m⏳\x1b[0m Enqueued in FIFO lock queue at position ${res.position} (held by ${res.currentHolder})\n`);
|
|
398
|
-
}
|
|
399
|
-
return res;
|
|
399
|
+
return handleLockCommand(db, nonFlagPositional, flags, isCli);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
if (subCommand === 'unlock') {
|
|
403
|
-
|
|
404
|
-
const res = releaseFileLock(db, file, flags.as || '@agent');
|
|
405
|
-
if (isCli) {
|
|
406
|
-
if (flags.isJson) process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
407
|
-
else if (res.success) process.stdout.write(`\x1b[32m✔\x1b[0m Released lock on ${file}\n`);
|
|
408
|
-
else process.stderr.write(`\x1b[31m✕ Unlock failed: ${res.reason}\x1b[0m\n`);
|
|
409
|
-
}
|
|
410
|
-
return res;
|
|
403
|
+
return handleUnlockCommand(db, nonFlagPositional, flags, isCli);
|
|
411
404
|
}
|
|
412
405
|
|
|
413
406
|
if (subCommand === 'triage') {
|
|
@@ -24,7 +24,7 @@ export const requestFileLock = (db, filePath, agentId, options = {}) => {
|
|
|
24
24
|
if (!canRequest) return { granted: false, reason: 'missing_args' };
|
|
25
25
|
|
|
26
26
|
const cleanId = normalizeAgentId(agentId);
|
|
27
|
-
const pid = options.pid
|
|
27
|
+
const pid = typeof options.pid === 'number' ? options.pid : 0;
|
|
28
28
|
|
|
29
29
|
return withImmediateTransaction(db, () => {
|
|
30
30
|
cleanExpiredLeases(db);
|
package/cli/team/team-flags.js
CHANGED
|
@@ -15,7 +15,8 @@ export const parseFlags = (args = []) => {
|
|
|
15
15
|
isCompact: args.includes('--compact'),
|
|
16
16
|
markRead: args.includes('--mark-read'),
|
|
17
17
|
force: args.includes('--force') || args.includes('-f'),
|
|
18
|
-
noTargetConfirm: args.includes('--no-target-confirm')
|
|
18
|
+
noTargetConfirm: args.includes('--no-target-confirm'),
|
|
19
|
+
help: args.includes('--help') || args.includes('-h')
|
|
19
20
|
};
|
|
20
21
|
for (let i = 0; i < args.length; i++) {
|
|
21
22
|
const arg = args[i];
|
|
@@ -53,6 +54,8 @@ export const parseFlags = (args = []) => {
|
|
|
53
54
|
if (arg.startsWith('--prio=') || arg.startsWith('--priority=')) flags.priority = parseInt(arg.split('=')[1], 10);
|
|
54
55
|
if ((arg === '--prio' || arg === '--priority') && hasNext) flags.priority = parseInt(nextArg, 10);
|
|
55
56
|
if (arg.startsWith('--purpose=')) flags.purpose = arg.split('=')[1];
|
|
57
|
+
if (arg.startsWith('--pid=')) flags.pid = parseInt(arg.split('=')[1], 10);
|
|
58
|
+
if (arg === '--pid' && hasNext) flags.pid = parseInt(nextArg, 10);
|
|
56
59
|
if (arg.startsWith('--tokens=')) flags.tokens = parseInt(arg.split('=')[1], 10);
|
|
57
60
|
if (arg.startsWith('--prompt-tokens=')) flags.promptTokens = parseInt(arg.split('=')[1], 10);
|
|
58
61
|
if (arg.startsWith('--completion-tokens=')) flags.completionTokens = parseInt(arg.split('=')[1], 10);
|
package/cli/team/team-format.js
CHANGED
|
@@ -179,3 +179,91 @@ export const formatTaskDetailCard = (task, events = []) => {
|
|
|
179
179
|
lines.push(`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`);
|
|
180
180
|
return lines.join('\n');
|
|
181
181
|
};
|
|
182
|
+
|
|
183
|
+
export const formatTeamHelpCard = () => {
|
|
184
|
+
const lines = [
|
|
185
|
+
'',
|
|
186
|
+
'\x1b[1m\x1b[36m⚡ [Chemical X] Multi-Agent Swarm Orchestration (team)\x1b[0m',
|
|
187
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m`,
|
|
188
|
+
' \x1b[1mUsage:\x1b[0m chemx team <command> [options]',
|
|
189
|
+
'',
|
|
190
|
+
' \x1b[1mCommands:\x1b[0m',
|
|
191
|
+
' \x1b[32mstatus\x1b[0m Display swarm agent, task, and lock status card',
|
|
192
|
+
' \x1b[32mtask\x1b[0m [action] Manage agent tasks (list, show, add, claim, done, update)',
|
|
193
|
+
' \x1b[32mlock\x1b[0m [acquire|release] Acquire or release a mutual exclusion file lock',
|
|
194
|
+
' \x1b[32munlock\x1b[0m <file> Release a file lock held by an agent',
|
|
195
|
+
' \x1b[32mfeed\x1b[0m View swarm feed event timeline',
|
|
196
|
+
' \x1b[32mpost\x1b[0m <message> Post broadcast message to swarm feed',
|
|
197
|
+
' \x1b[32minbox\x1b[0m [@agent] View agent mailbox and notifications',
|
|
198
|
+
' \x1b[32mdm\x1b[0m <@agent> <msg> Send direct message to another agent',
|
|
199
|
+
' \x1b[32mtokens\x1b[0m Inspect token consumption and telemetry breakdown',
|
|
200
|
+
' \x1b[32mtriage\x1b[0m Generate tasks automatically from AST audit hazards',
|
|
201
|
+
' \x1b[32mbenchmark\x1b[0m Run memory and token reduction ablation benchmark',
|
|
202
|
+
' \x1b[32mtrain\x1b[0m Train/evaluate vector and pattern indices',
|
|
203
|
+
'',
|
|
204
|
+
' \x1b[1mGlobal Options:\x1b[0m',
|
|
205
|
+
' \x1b[33m--as\x1b[0m <@agent> Execute action as specified agent handle',
|
|
206
|
+
' \x1b[33m--json\x1b[0m Output machine-readable JSON format',
|
|
207
|
+
' \x1b[33m--help, -h\x1b[0m Show help for team or specific subcommands',
|
|
208
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
|
|
209
|
+
];
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const formatTaskHelpCard = () => {
|
|
214
|
+
const lines = [
|
|
215
|
+
'',
|
|
216
|
+
'\x1b[1m\x1b[36m⚡ [Chemical X] Task Coordination Commands (team task)\x1b[0m',
|
|
217
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m`,
|
|
218
|
+
' \x1b[1mUsage:\x1b[0m chemx team task <action> [arguments] [options]',
|
|
219
|
+
'',
|
|
220
|
+
' \x1b[1mActions:\x1b[0m',
|
|
221
|
+
' \x1b[32mlist\x1b[0m List swarm tasks (default action)',
|
|
222
|
+
' \x1b[32mshow\x1b[0m <taskId> Display full details and activity stream for a task',
|
|
223
|
+
' \x1b[32madd\x1b[0m "<title>" Create a new task in the queue',
|
|
224
|
+
' \x1b[32mclaim\x1b[0m <taskId> Claim an unassigned task for an agent',
|
|
225
|
+
' \x1b[32mdone\x1b[0m <taskId> Complete a task with automatic verification audit',
|
|
226
|
+
' \x1b[32mupdate\x1b[0m <taskId> [status] Update task status (in_progress, blocked, done)',
|
|
227
|
+
' \x1b[32mcomment\x1b[0m <taskId> <msg> Post a comment or status update to a task',
|
|
228
|
+
' \x1b[32mtriage\x1b[0m Generate tasks from AST architectural hazards',
|
|
229
|
+
' \x1b[32mreconcile\x1b[0m Auto-resolve tasks whose hazards have been fixed',
|
|
230
|
+
' \x1b[32mset-target\x1b[0m <id> <path> Assign target file path to a task',
|
|
231
|
+
' \x1b[32mvds-slot\x1b[0m <id> <moscow> <p> Set MoSCoW slot and priority on a task',
|
|
232
|
+
' \x1b[32mtrace\x1b[0m <id> <url> Attach provenance trace URL to a task',
|
|
233
|
+
'',
|
|
234
|
+
' \x1b[1mOptions:\x1b[0m',
|
|
235
|
+
' \x1b[33m--as\x1b[0m <@agent> Agent handle (e.g. @agent-1)',
|
|
236
|
+
' \x1b[33m--status\x1b[0m <status> Filter by status (todo, in_progress, blocked, done)',
|
|
237
|
+
' \x1b[33m--agent\x1b[0m <@agent> Filter tasks assigned to agent',
|
|
238
|
+
' \x1b[33m--prio\x1b[0m <1-5> Task priority filter or setting',
|
|
239
|
+
' \x1b[33m--force, -f\x1b[0m Force complete task even with remaining hazards',
|
|
240
|
+
' \x1b[33m--json\x1b[0m Output machine-readable JSON format',
|
|
241
|
+
' \x1b[33m--help, -h\x1b[0m Show this help message',
|
|
242
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
|
|
243
|
+
];
|
|
244
|
+
return lines.join('\n');
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const formatLockHelpCard = () => {
|
|
248
|
+
const lines = [
|
|
249
|
+
'',
|
|
250
|
+
'\x1b[1m\x1b[36m⚡ [Chemical X] Mutual Exclusion File Locking (team lock)\x1b[0m',
|
|
251
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m`,
|
|
252
|
+
' \x1b[1mUsage:\x1b[0m chemx team lock [acquire|release] <filePath> [options]',
|
|
253
|
+
' chemx team unlock <filePath> [options]',
|
|
254
|
+
'',
|
|
255
|
+
' \x1b[1mActions:\x1b[0m',
|
|
256
|
+
' \x1b[32macquire\x1b[0m <path> Acquire mutual exclusion lock (or enqueue in FIFO)',
|
|
257
|
+
' \x1b[32mrelease\x1b[0m <path> Release lock and promote next FIFO waiter',
|
|
258
|
+
'',
|
|
259
|
+
' \x1b[1mOptions:\x1b[0m',
|
|
260
|
+
' \x1b[33m--as\x1b[0m <@agent> Agent handle acquiring/releasing lock (default: @agent)',
|
|
261
|
+
' \x1b[33m--purpose\x1b[0m "<reason>" Reason or intent for acquiring lock',
|
|
262
|
+
' \x1b[33m--priority\x1b[0m <1-5> Priority in FIFO wait queue (1 = highest, default: 2)',
|
|
263
|
+
' \x1b[33m--pid\x1b[0m <pid> Optional process PID for crash-recovery lease reclamation',
|
|
264
|
+
' \x1b[33m--json\x1b[0m Output machine-readable JSON format',
|
|
265
|
+
' \x1b[33m--help, -h\x1b[0m Show this help message',
|
|
266
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
|
|
267
|
+
];
|
|
268
|
+
return lines.join('\n');
|
|
269
|
+
};
|
package/cli/team/team-memory.js
CHANGED
|
@@ -17,7 +17,10 @@ export const recordMemoryInjection = (db, entry = {}) => {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export const recordMemoryUtilization = (db, { taskId, utilizedProvenance = [], causedRetry = false } = {}) => {
|
|
20
|
-
|
|
20
|
+
const hasDb = Boolean(db);
|
|
21
|
+
const hasTaskId = Boolean(taskId);
|
|
22
|
+
const canRecord = hasDb && hasTaskId;
|
|
23
|
+
if (!canRecord) return;
|
|
21
24
|
if (causedRetry) db.prepare('UPDATE agent_memory_log SET caused_retry = 1 WHERE task_id = ?').run(taskId);
|
|
22
25
|
for (const prov of utilizedProvenance) {
|
|
23
26
|
db.prepare('UPDATE agent_memory_log SET was_utilized = 1 WHERE task_id = ? AND (provenance_path = ? OR provenance_path LIKE ?)').run(taskId, prov, `%${prov}%`);
|
|
@@ -51,14 +54,62 @@ export const calculateMemoryMetrics = (db) => {
|
|
|
51
54
|
|
|
52
55
|
export const runAblationComparison = (db) => {
|
|
53
56
|
const m = calculateMemoryMetrics(db);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
const row = db ? db.prepare(`
|
|
58
|
+
SELECT
|
|
59
|
+
COUNT(*) as totalTasks,
|
|
60
|
+
COALESCE(SUM(prompt_tokens), 0) as totalPromptTokens,
|
|
61
|
+
COALESCE(SUM(completion_tokens), 0) as totalCompletionTokens,
|
|
62
|
+
COALESCE(SUM(cached_tokens), 0) as totalCachedTokens,
|
|
63
|
+
COALESCE(SUM(cost_usd), 0) as totalCost
|
|
64
|
+
FROM agent_tasks
|
|
65
|
+
`).get() : null;
|
|
66
|
+
|
|
67
|
+
const taskStats = {
|
|
68
|
+
totalTasks: Number(row?.totalTasks || 0),
|
|
69
|
+
totalPromptTokens: Number(row?.totalPromptTokens || 0),
|
|
70
|
+
totalCompletionTokens: Number(row?.totalCompletionTokens || 0),
|
|
71
|
+
totalCachedTokens: Number(row?.totalCachedTokens || 0),
|
|
72
|
+
totalCost: Number(row?.totalCost || 0)
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const hasTasks = taskStats.totalTasks > 0;
|
|
76
|
+
const hasMemories = m.totalCount > 0;
|
|
77
|
+
const hasRealData = Boolean(hasTasks || hasMemories);
|
|
78
|
+
|
|
79
|
+
if (!hasRealData) {
|
|
80
|
+
const turns = 10;
|
|
81
|
+
const baseTokens = turns * 2500;
|
|
82
|
+
const memTokens = 700;
|
|
83
|
+
const tokensSaved = baseTokens - memTokens;
|
|
84
|
+
const precisionGain = (100 / 12.0).toFixed(1) + 'x';
|
|
85
|
+
return {
|
|
86
|
+
hasRealData: false,
|
|
87
|
+
isSimulated: true,
|
|
88
|
+
workloadTurns: turns,
|
|
89
|
+
memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: 0.0, retrievalPrecisionPct: 100, astQualityScore: 100 },
|
|
90
|
+
memoryDisabled: { mode: 'Stateless Monolithic File Re-read', tokensUsed: baseTokens, staleRetryRatePct: 18.5, retrievalPrecisionPct: 12.0, astQualityScore: 84 },
|
|
91
|
+
delta: { tokenReductionPct: Math.round((tokensSaved / baseTokens) * 100), costAvoidedUsd: Number(((tokensSaved / 1000000) * 10.0).toFixed(4)), retrievalPrecisionGainRatio: precisionGain, staleRetriesEliminated: Math.round(turns * 0.185) }
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const turns = taskStats.totalTasks + m.totalCount;
|
|
96
|
+
const liveTokens = taskStats.totalPromptTokens + taskStats.totalCompletionTokens;
|
|
97
|
+
const memTokens = liveTokens > 0 ? liveTokens : Math.max(turns * 70, m.totalInjectedTokens || 70);
|
|
98
|
+
const baseTokens = turns * 2500 + Math.round(memTokens * 0.5);
|
|
99
|
+
const tokensSaved = Math.max(0, baseTokens - memTokens);
|
|
100
|
+
const tokenReductionPct = baseTokens > 0 ? Math.round((tokensSaved / baseTokens) * 100) : 0;
|
|
101
|
+
const costAvoidedUsd = Number(((tokensSaved / 1000000) * 10.0).toFixed(4));
|
|
102
|
+
const precisionGainRatio = (m.precisionPct / 12.0).toFixed(1) + 'x';
|
|
103
|
+
const retryRatePct = m.totalCount > 0 ? Number(((m.staleRetries / m.totalCount) * 100).toFixed(1)) : 0.0;
|
|
104
|
+
const staleRetriesEliminated = Math.max(0, Math.round(turns * 0.185) - m.staleRetries);
|
|
105
|
+
|
|
57
106
|
return {
|
|
107
|
+
hasRealData: true,
|
|
108
|
+
isSimulated: false,
|
|
58
109
|
workloadTurns: turns,
|
|
59
|
-
memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct:
|
|
110
|
+
memoryEnabled: { mode: 'SQLite AST & Event Memory', tokensUsed: memTokens, staleRetryRatePct: retryRatePct, retrievalPrecisionPct: m.precisionPct, astQualityScore: 100 },
|
|
60
111
|
memoryDisabled: { mode: 'Stateless Monolithic File Re-read', tokensUsed: baseTokens, staleRetryRatePct: 18.5, retrievalPrecisionPct: 12.0, astQualityScore: 84 },
|
|
61
|
-
delta: { tokenReductionPct
|
|
112
|
+
delta: { tokenReductionPct, costAvoidedUsd, retrievalPrecisionGainRatio: precisionGainRatio, staleRetriesEliminated }
|
|
62
113
|
};
|
|
63
114
|
};
|
|
64
115
|
|
|
@@ -66,19 +117,31 @@ export const formatAblationCard = (a) => {
|
|
|
66
117
|
const m = a.memoryEnabled;
|
|
67
118
|
const d = a.memoryDisabled;
|
|
68
119
|
const delta = a.delta;
|
|
120
|
+
const headerSuffix = a.hasRealData ? '[Live Project Telemetry]' : '[Reference Projection - Zero Local Tasks]';
|
|
69
121
|
const lines = [
|
|
70
122
|
'',
|
|
71
|
-
|
|
72
|
-
`\x1b[90m${'─'.repeat(58)}\x1b[0m
|
|
123
|
+
`\x1b[1m\x1b[36m⚡ [Chemical X] Persistent Memory Ablation Benchmark ${headerSuffix}\x1b[0m`,
|
|
124
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m`
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
if (!a.hasRealData) {
|
|
128
|
+
lines.push(
|
|
129
|
+
' \x1b[33mℹ Notice: Zero local task history recorded. Displaying reference projection.\x1b[0m',
|
|
130
|
+
' \x1b[33m Run team tasks and memory operations to generate live project benchmarks.\x1b[0m',
|
|
131
|
+
`\x1b[90m${'─'.repeat(58)}\x1b[0m`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lines.push(
|
|
73
136
|
` \x1b[1mWorkload:\x1b[0m ${a.workloadTurns} agent turns / tasks`,
|
|
74
137
|
` \x1b[1mMemory Mode (SQLite):\x1b[0m ${m.tokensUsed.toLocaleString()} tokens │ Precision: ${m.retrievalPrecisionPct}% │ Retry: ${m.staleRetryRatePct}%`,
|
|
75
138
|
` \x1b[1mBaseline (Stateless):\x1b[0m ${d.tokensUsed.toLocaleString()} tokens │ Precision: ${d.retrievalPrecisionPct}% │ Retry: ${d.staleRetryRatePct}%`,
|
|
76
139
|
`\x1b[90m${'─'.repeat(58)}\x1b[0m`,
|
|
77
140
|
` \x1b[32m✔ Token Reduction:\x1b[0m ${delta.tokenReductionPct}% fewer tokens consumed`,
|
|
78
|
-
` \x1b[32m✔ Cost Avoided:\x1b[0m
|
|
141
|
+
` \x1b[32m✔ Cost Avoided:\x1b[0m ${delta.costAvoidedUsd.toFixed(4)} USD`,
|
|
79
142
|
` \x1b[32m✔ Precision Gain:\x1b[0m ${delta.retrievalPrecisionGainRatio} higher retrieval precision per dollar`,
|
|
80
143
|
` \x1b[32m✔ Stale Retries:\x1b[0m ${delta.staleRetriesEliminated} retries eliminated`,
|
|
81
144
|
`\x1b[90m${'─'.repeat(58)}\x1b[0m\n`
|
|
82
|
-
|
|
145
|
+
);
|
|
83
146
|
return lines.join('\n');
|
|
84
147
|
};
|
|
@@ -68,7 +68,32 @@ test("runAblationComparison and formatAblationCard generate metrics card without
|
|
|
68
68
|
assert.strictEqual(ablation.memoryEnabled.astQualityScore, 100);
|
|
69
69
|
assert.strictEqual(ablation.memoryDisabled.astQualityScore, 84);
|
|
70
70
|
assert.ok(ablation.delta.tokenReductionPct >= 90);
|
|
71
|
+
assert.strictEqual(ablation.hasRealData, false);
|
|
72
|
+
assert.strictEqual(ablation.isSimulated, true);
|
|
71
73
|
const card = formatAblationCard(ablation);
|
|
72
74
|
assert.ok(card.includes("Persistent Memory Ablation Benchmark"));
|
|
75
|
+
assert.ok(card.includes("Reference Projection"));
|
|
76
|
+
assert.strictEqual(card.includes("—"), false);
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
test("runAblationComparison computes dynamic metrics from live task and memory records", () => withTmpCwd((db) => {
|
|
80
|
+
db.prepare(`
|
|
81
|
+
INSERT INTO agent_tasks (title, status, prompt_tokens, completion_tokens, cost_usd, created_at, updated_at)
|
|
82
|
+
VALUES ('Refactor parser', 'done', 1500, 300, 0.05, 1000, 1000)
|
|
83
|
+
`).run();
|
|
84
|
+
|
|
85
|
+
recordMemoryInjection(db, { taskId: 1, provenance: "src/parser.ts", tokens: 200 });
|
|
86
|
+
recordMemoryUtilization(db, { taskId: 1, utilizedProvenance: ["src/parser.ts"], causedRetry: false });
|
|
87
|
+
|
|
88
|
+
const ablation = runAblationComparison(db);
|
|
89
|
+
assert.strictEqual(ablation.hasRealData, true);
|
|
90
|
+
assert.strictEqual(ablation.isSimulated, false);
|
|
91
|
+
assert.strictEqual(ablation.workloadTurns, 2);
|
|
92
|
+
assert.strictEqual(ablation.memoryEnabled.tokensUsed, 1800);
|
|
93
|
+
assert.ok(ablation.delta.tokenReductionPct > 0);
|
|
94
|
+
assert.ok(ablation.delta.retrievalPrecisionGainRatio.endsWith('x'));
|
|
95
|
+
|
|
96
|
+
const card = formatAblationCard(ablation);
|
|
97
|
+
assert.ok(card.includes("Live Project Telemetry"));
|
|
73
98
|
assert.strictEqual(card.includes("—"), false);
|
|
74
99
|
}));
|
|
@@ -129,7 +129,7 @@ test('swarm multi-process: lock automatically releases when holding worker proce
|
|
|
129
129
|
import { openIndexDb } from '${path.resolve('cli/search-schema.js')}';
|
|
130
130
|
import { requestFileLock } from '${path.resolve('cli/team/team-db-locks.js')}';
|
|
131
131
|
const db = openIndexDb('${tmpDir}');
|
|
132
|
-
requestFileLock(db, '${testFile}', '@crashing-worker', { ttlMs: 3600000 });
|
|
132
|
+
requestFileLock(db, '${testFile}', '@crashing-worker', { ttlMs: 3600000, pid: process.pid });
|
|
133
133
|
process.exit(1);
|
|
134
134
|
`;
|
|
135
135
|
|
package/cli/theme.js
CHANGED
package/cli/ui-handlers.js
CHANGED
|
@@ -61,7 +61,7 @@ export const handleSwarmStatus = (db, cwd = process.cwd()) => {
|
|
|
61
61
|
parentId: t.parent_id,
|
|
62
62
|
subtaskCount,
|
|
63
63
|
assignedAgentId: t.assigned_agent_id,
|
|
64
|
-
tokenStamp: `[P: ${p} | C: ${c} | Cost:
|
|
64
|
+
tokenStamp: `[P: ${p} | C: ${c} | Cost: $${cost.toFixed(4)}]`
|
|
65
65
|
};
|
|
66
66
|
});
|
|
67
67
|
|
package/cli/verify-helpers.js
CHANGED
|
@@ -91,37 +91,56 @@ export const parseTestOutput = (stdout = '', stderr = '', exitCode = 0) => {
|
|
|
91
91
|
let skipped = 0;
|
|
92
92
|
let checkmarkPasses = 0;
|
|
93
93
|
let crossmarkFails = 0;
|
|
94
|
+
let hasRunnerSummary = false;
|
|
94
95
|
|
|
95
96
|
for (const line of lines) {
|
|
96
97
|
const clean = stripAnsi(line).trim();
|
|
97
98
|
if (!clean) continue;
|
|
98
99
|
|
|
99
|
-
if (clean.startsWith('✔')) checkmarkPasses++;
|
|
100
|
+
if (clean.startsWith('✔') || clean.startsWith('✓')) checkmarkPasses++;
|
|
100
101
|
if (clean.startsWith('✖') || clean.startsWith('FAIL ')) crossmarkFails++;
|
|
101
102
|
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
103
|
+
const isRunnerSummary = /^Tests:?\s+/i.test(clean);
|
|
104
|
+
if (isRunnerSummary) {
|
|
105
|
+
const passM = clean.match(/(\d+)\s+passed/i);
|
|
106
|
+
const failM = clean.match(/(\d+)\s+failed/i);
|
|
107
|
+
const skipM = clean.match(/(\d+)\s+(?:skipped|todo|pending)/i);
|
|
108
|
+
const totalParenM = clean.match(/\((\d+)\)/);
|
|
109
|
+
const totalWordM = clean.match(/(\d+)\s+total/i);
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
if (passM) passed = parseInt(passM[1], 10);
|
|
112
|
+
if (failM) failed = parseInt(failM[1], 10);
|
|
113
|
+
if (skipM) skipped = parseInt(skipM[1], 10);
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
if (totalParenM) totalTests = parseInt(totalParenM[1], 10);
|
|
116
|
+
else if (totalWordM) totalTests = parseInt(totalWordM[1], 10);
|
|
117
|
+
else totalTests = passed + failed + skipped;
|
|
110
118
|
|
|
111
|
-
|
|
112
|
-
|
|
119
|
+
hasRunnerSummary = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!hasRunnerSummary) {
|
|
124
|
+
const nodeTests = clean.match(/^(?:[ℹ#]\s+)?tests\s+(\d+)$/i);
|
|
125
|
+
if (nodeTests) totalTests = parseInt(nodeTests[1], 10);
|
|
126
|
+
|
|
127
|
+
const nodePass = clean.match(/^(?:[ℹ#]\s+)?pass\s+(\d+)$/i);
|
|
128
|
+
if (nodePass) passed = parseInt(nodePass[1], 10);
|
|
129
|
+
|
|
130
|
+
const nodeFail = clean.match(/^(?:[ℹ#]\s+)?fail\s+(\d+)$/i);
|
|
131
|
+
if (nodeFail) failed = parseInt(nodeFail[1], 10);
|
|
113
132
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (vitestMatch[1]) failed = parseInt(vitestMatch[1], 10);
|
|
117
|
-
if (vitestMatch[2]) passed = parseInt(vitestMatch[2], 10);
|
|
118
|
-
if (vitestMatch[3]) totalTests = parseInt(vitestMatch[3], 10);
|
|
133
|
+
const nodeSkip = clean.match(/^(?:[ℹ#]\s+)?(?:skipped|todo)\s+(\d+)$/i);
|
|
134
|
+
if (nodeSkip) skipped += parseInt(nodeSkip[1], 10);
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
if (passed === 0 && checkmarkPasses > 0) passed = checkmarkPasses;
|
|
123
139
|
if (failed === 0 && crossmarkFails > 0) failed = crossmarkFails;
|
|
124
|
-
if (totalTests === 0) totalTests = passed + failed;
|
|
140
|
+
if (totalTests === 0) totalTests = passed + failed + skipped;
|
|
141
|
+
const isCleanExit = exitCode === 0 && failed === 0;
|
|
142
|
+
const isTestCountUnderflow = isCleanExit && totalTests < passed;
|
|
143
|
+
if (isTestCountUnderflow) totalTests = passed;
|
|
125
144
|
|
|
126
145
|
const failures = [];
|
|
127
146
|
if (exitCode !== 0 || failed > 0) {
|
package/cli/verify.spec.js
CHANGED
|
@@ -93,6 +93,41 @@ test('Verify: parseTestOutput handles all passing tests with zero failures', ()
|
|
|
93
93
|
assert.strictEqual(parsed.failures.length, 0);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test('Verify: parseTestOutput accurately parses Vitest summary and isolates test count from audit rules', () => {
|
|
97
|
+
const vitestOutput = `
|
|
98
|
+
✓ src/atoms/a-button/a-button.spec.ts (6)
|
|
99
|
+
✓ src/molecules/m-sample-card/m-sample-card.spec.ts (8)
|
|
100
|
+
|
|
101
|
+
Test Files 2 passed (2)
|
|
102
|
+
Tests 14 passed (14)
|
|
103
|
+
Start at 08:00:00
|
|
104
|
+
Duration 450ms
|
|
105
|
+
|
|
106
|
+
AST Architecture: A+ (100/100, 44 rules evaluated)
|
|
107
|
+
`;
|
|
108
|
+
const parsed = parseTestOutput(vitestOutput, '', 0);
|
|
109
|
+
assert.strictEqual(parsed.success, true);
|
|
110
|
+
assert.strictEqual(parsed.passed, 14);
|
|
111
|
+
assert.strictEqual(parsed.totalTests, 14);
|
|
112
|
+
assert.strictEqual(parsed.failed, 0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('Verify: parseTestOutput handles Vitest pipe separators with failures and skips', () => {
|
|
116
|
+
const vitestMixedOutput = `
|
|
117
|
+
❯ src/trouble.spec.ts (3)
|
|
118
|
+
✖ broken test assertion
|
|
119
|
+
|
|
120
|
+
Test Files 1 failed (1)
|
|
121
|
+
Tests 1 failed | 1 skipped | 12 passed (14)
|
|
122
|
+
`;
|
|
123
|
+
const parsed = parseTestOutput(vitestMixedOutput, '', 1);
|
|
124
|
+
assert.strictEqual(parsed.success, false);
|
|
125
|
+
assert.strictEqual(parsed.passed, 12);
|
|
126
|
+
assert.strictEqual(parsed.failed, 1);
|
|
127
|
+
assert.strictEqual(parsed.skipped, 1);
|
|
128
|
+
assert.strictEqual(parsed.totalTests, 14);
|
|
129
|
+
});
|
|
130
|
+
|
|
96
131
|
test('Verify: runTypecheckAudit runs in JSON mode without throwing', async () => {
|
|
97
132
|
const report = await runTypecheckAudit(['--json'], false, { print: false });
|
|
98
133
|
assert.ok(typeof report.success === 'boolean');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chemx/create-chemx",
|
|
3
|
-
"version": "26.9.25-
|
|
3
|
+
"version": "26.9.25-943",
|
|
4
4
|
"description": "Chemical X Protocol: Private drop-in architecture starter kit and capsule generator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -59,9 +59,7 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@babel/parser": "^7.29.8",
|
|
61
61
|
"@babel/traverse": "^7.29.8",
|
|
62
|
-
"@babel/types": "^7.29.8"
|
|
63
|
-
"react": "^18.3.1",
|
|
64
|
-
"react-dom": "^18.3.1"
|
|
62
|
+
"@babel/types": "^7.29.8"
|
|
65
63
|
},
|
|
66
64
|
"devDependencies": {
|
|
67
65
|
"@types/babel__traverse": "^7.28.0",
|
|
@@ -69,6 +67,8 @@
|
|
|
69
67
|
"@types/react": "^18.3.31",
|
|
70
68
|
"@types/react-dom": "^18.3.7",
|
|
71
69
|
"chemx": "26.9.20-408",
|
|
70
|
+
"react": "^18.3.1",
|
|
71
|
+
"react-dom": "^18.3.1",
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
}
|
|
74
74
|
}
|