@chem-x/starter-kit 26.9.23-1269 → 26.9.24-393

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.
@@ -13,8 +13,12 @@ const RED = '\x1b[31m';
13
13
 
14
14
  export const ensureChemxDir = (cwd = process.cwd()) => {
15
15
  const dir = path.resolve(cwd, '.chemx');
16
- if (!fs.existsSync(dir)) {
17
- fs.mkdirSync(dir, { recursive: true });
16
+ try {
17
+ if (!fs.existsSync(dir)) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+ } catch {
21
+ return dir;
18
22
  }
19
23
 
20
24
  // Ensure .chemx is gitignored if git repo exists
@@ -165,7 +169,11 @@ export const getAuditBaseline = (cwd = process.cwd()) => {
165
169
  export const setAuditBaseline = (snapshot, cwd = process.cwd()) => {
166
170
  ensureChemxDir(cwd);
167
171
  const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
168
- fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
172
+ try {
173
+ fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
174
+ } catch {
175
+ // Read-only filesystem in sandbox
176
+ }
169
177
  return snapshot;
170
178
  };
171
179
 
@@ -196,7 +204,11 @@ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
196
204
  // Keep last 50 snapshots
197
205
  const trimmed = history.slice(-50);
198
206
  const historyPath = path.resolve(cwd, '.chemx', 'history.json');
199
- fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
207
+ try {
208
+ fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
209
+ } catch {
210
+ // Read-only filesystem in sandbox
211
+ }
200
212
 
201
213
  // Auto-establish first run or lower score as baseline floor
202
214
  const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
@@ -206,8 +218,12 @@ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
206
218
  const baselineScore = currentBaseline?.health?.score ?? 101;
207
219
 
208
220
  if (!currentBaseline || currentScore < baselineScore) {
209
- fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
210
- isNewBaseline = true;
221
+ try {
222
+ fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
223
+ isNewBaseline = true;
224
+ } catch {
225
+ // Read-only filesystem in sandbox
226
+ }
211
227
  }
212
228
 
213
229
  const baseline = getAuditBaseline(cwd);
@@ -1,7 +1,7 @@
1
1
  import * as t from '@babel/types';
2
2
  import { RULE_REGISTRY } from './rules-registry.js';
3
3
 
4
- const VERB_PREFIX_REGEX = /^(add|remove|toggle|set|get|fetch|load|save|delete|clear|reset|refresh|update|create|edit|send|sync|init|dispose|trigger|post|put|patch|apply|dismiss|select|deselect|filter|sort|search|open|close|show|hide|start|stop|cancel|execute|run|retry|submit|validate|mutate|next|prev|step|increment|decrement|insert|upsert|drop|enable|disable|register|unregister|login|logout|checkout|publish|archive|discard|confirm|undo|redo|claim|complete|acquire|release|download|upload|export|import|handle)([A-Z0-9_].*)?$/;
4
+ const VERB_PREFIX_REGEX = /^(add|remove|toggle|set|get|fetch|load|save|delete|clear|reset|refresh|update|create|edit|send|sync|init|setup|dispose|trigger|post|put|patch|apply|dismiss|select|deselect|filter|sort|search|open|close|show|hide|start|stop|cancel|execute|run|retry|submit|validate|mutate|next|prev|step|increment|decrement|insert|upsert|drop|enable|disable|register|unregister|login|logout|checkout|publish|archive|discard|confirm|undo|redo|claim|complete|acquire|release|download|upload|export|import|handle|play|pause|resume|seek|unlock|restore|advance|focus|notify|broadcast|copy|format)([A-Z0-9_].*)?$/;
5
5
 
6
6
  const STATUS_HEURISTIC_REGEX = /^(is[A-Z]|has[A-Z]|can[A-Z]|should[A-Z])|.*(Error|Status)$/;
7
7
  const STATUS_EXACT_NAMES = new Set(['status', 'error', 'loading', 'pending', 'ready']);
@@ -36,7 +36,7 @@ const renderTreeLines = (node, depth = 0) => {
36
36
  return lines;
37
37
  };
38
38
 
39
- export const formatGroupedPromptViolations = (violations = []) => {
39
+ export const formatGroupedPromptViolations = (violations = [], options = {}) => {
40
40
  const hasNoViolations = violations.length === 0;
41
41
  if (hasNoViolations) return [];
42
42
 
@@ -49,11 +49,14 @@ export const formatGroupedPromptViolations = (violations = []) => {
49
49
  lines.push(`${idx + 1}. ${sevColor}[${rg.rule}]${RESET} ${BOLD}(${countLabel})${RESET}`);
50
50
  lines.push(` Hazard: ${rg.hazard}`);
51
51
  lines.push(` Directive: ${CYAN}${rg.directive}${RESET}`);
52
- lines.push(' Locations:');
52
+ lines.push(` Action: chemx team task list --rule=${rg.rule}`);
53
53
 
54
- const tree = buildPathTree(rg.violations);
55
- const treeLines = renderTreeLines(tree, 0);
56
- lines.push(...treeLines);
54
+ if (options.detailedLocations) {
55
+ lines.push(' Locations:');
56
+ const tree = buildPathTree(rg.violations);
57
+ const treeLines = renderTreeLines(tree, 0);
58
+ lines.push(...treeLines);
59
+ }
57
60
  lines.push('');
58
61
  });
59
62
 
@@ -62,10 +65,10 @@ export const formatGroupedPromptViolations = (violations = []) => {
62
65
 
63
66
  export const buildAgentCommandsSection = () => [
64
67
  '### AI AGENT DISCOVERY & REFACTORING COMMANDS:',
65
- '- Query & Inspect: Run `pnpm q "<target>" --inspect` (or `npx chemx search "<target>" --inspect`) to inspect component props, symbols, and hooks before editing.',
66
- '- Tier Filter: Run `pnpm q "<query>" --tier=molecule` (or `atom`, `organism`, `hook`) to find related capsules.',
67
- '- Zero-Overhead JSON: Run `pnpm q "<query>" --json` for minified AST metadata without burning context tokens on whole files.',
68
- '- Re-Verify Score: Run `npx chemx audit` after refactoring to ensure Molecular Health Index (MHI) and letter grades improve.'
68
+ '- Claim Work: Run `chemx team task list --status=queued` and claim via `chemx team task claim <id> --as=@coder`.',
69
+ '- Inspect Target: Run `chemx read <target_path> --outline` to inspect AST signatures without token bloat.',
70
+ '- Verify & Auto-Resolve: Run `chemx verify`. Clean files auto-reconcile tasks to done in SQLite.',
71
+ '- MCP Invocations: If invoking via MCP, pass command string directly via `chemx({ command: "..." })`.'
69
72
  ].join('\n');
70
73
 
71
74
  export const buildGradeFPrompt = (report, options = {}) => {
@@ -10,7 +10,8 @@ import {
10
10
  buildAiSlopPrompt,
11
11
  buildHotspotsPrompt,
12
12
  buildPillarPrompt,
13
- buildMasterPrompt
13
+ buildMasterPrompt,
14
+ formatGroupedPromptViolations
14
15
  } from './prompts.js';
15
16
 
16
17
  test('deduplicateRolePreambles: handles invalid and empty inputs', () => {
@@ -314,3 +315,53 @@ test('deduplicateRefactoringCommands: collapses multiple occurrences to single b
314
315
  assert.ok(result.includes('Section 2 content'));
315
316
  assert.ok(result.includes('- Command B'));
316
317
  });
318
+
319
+ test('formatGroupedPromptViolations: outputs token-compact task action by default', () => {
320
+ const violations = [
321
+ {
322
+ rule: 'SECURITY_RAW_HTML_INJECTION',
323
+ severity: 'CRITICAL',
324
+ hazard: 'Unsanitized HTML injection',
325
+ directive: 'Sanitize dynamic HTML via DOMPurify',
326
+ filePath: 'src/view-a.vue',
327
+ line: 10
328
+ },
329
+ {
330
+ rule: 'SECURITY_RAW_HTML_INJECTION',
331
+ severity: 'CRITICAL',
332
+ hazard: 'Unsanitized HTML injection',
333
+ directive: 'Sanitize dynamic HTML via DOMPurify',
334
+ filePath: 'src/view-b.vue',
335
+ line: 25
336
+ }
337
+ ];
338
+
339
+ const lines = formatGroupedPromptViolations(violations);
340
+ const text = lines.join('\n');
341
+
342
+ assert.ok(text.includes('[SECURITY_RAW_HTML_INJECTION]'));
343
+ assert.ok(text.includes('(2 items)'));
344
+ assert.ok(text.includes('Action: chemx team task list --rule=SECURITY_RAW_HTML_INJECTION'));
345
+ // Does not dump file locations tree by default
346
+ assert.ok(!text.includes('Locations:'));
347
+ assert.ok(!text.includes('view-a.vue:10'));
348
+ });
349
+
350
+ test('formatGroupedPromptViolations: renders file tree when detailedLocations is enabled', () => {
351
+ const violations = [
352
+ {
353
+ rule: 'SECURITY_RAW_HTML_INJECTION',
354
+ severity: 'CRITICAL',
355
+ hazard: 'Unsanitized HTML injection',
356
+ directive: 'Sanitize dynamic HTML via DOMPurify',
357
+ filePath: 'src/view-a.vue',
358
+ line: 10
359
+ }
360
+ ];
361
+
362
+ const lines = formatGroupedPromptViolations(violations, { detailedLocations: true });
363
+ const text = lines.join('\n');
364
+
365
+ assert.ok(text.includes('Locations:'));
366
+ assert.ok(text.includes('view-a.vue:10'));
367
+ });
@@ -104,41 +104,45 @@ export const runAudit = async (customDir, isCli, rawArgs, loadProjectConfig) =>
104
104
  const auditOptions = { outputFile, model, costPerMillion, fast: isFast, fileList, stage };
105
105
  const report = executeAstAudit(targetDir, auditOptions);
106
106
  saveAuditSnapshot(report);
107
- const syncRes = syncSearchIndex(targetDir, process.cwd());
108
-
109
- if (syncRes?.db) {
110
- syncViolationsIndex(syncRes.db, report.violations);
111
- recordAuditSnapshot(syncRes.db, report);
112
- if (rawArgs.includes('--triage')) {
113
- const createdTasks = autoGenerateTasksFromAudit(syncRes.db, { cwd: process.cwd(), targetDir });
114
- if (isCli && !isJson) {
115
- process.stdout.write(`\x1b[32m✔\x1b[0m Auto-triage generated ${createdTasks.length} team task(s) from audit violations.\n`);
107
+ try {
108
+ const syncRes = syncSearchIndex(targetDir, process.cwd());
109
+
110
+ if (syncRes?.db) {
111
+ syncViolationsIndex(syncRes.db, report.violations);
112
+ const shouldTriage = !rawArgs.includes('--no-triage');
113
+ if (shouldTriage) {
114
+ const createdTasks = autoGenerateTasksFromAudit(syncRes.db, { cwd: process.cwd(), targetDir });
115
+ if (isCli && !isJson && createdTasks.length > 0) {
116
+ process.stdout.write(`\x1b[32m✔\x1b[0m Auto-triage synchronized ${createdTasks.length} team task(s) in SQLite backlog.\n`);
117
+ }
116
118
  }
117
- }
118
- if (rawArgs.includes('--clones')) {
119
- const { detectSemanticClones } = await import('../audit/clone-detector.js');
120
- const { formatCloneReport } = await import('../audit/reporter-clones.js');
121
- const cloneThresholdArg = rawArgs.find((a) => a.startsWith('--clone-threshold='));
122
- const threshold = cloneThresholdArg ? parseFloat(cloneThresholdArg.split('=')[1]) : 0.85;
123
- const clones = detectSemanticClones(syncRes.db, { threshold });
124
- if (isJson) {
125
- report.clones = clones;
126
- } else {
127
- process.stdout.write(formatCloneReport(clones));
119
+ if (rawArgs.includes('--clones')) {
120
+ const { detectSemanticClones } = await import('../audit/clone-detector.js');
121
+ const { formatCloneReport } = await import('../audit/reporter-clones.js');
122
+ const cloneThresholdArg = rawArgs.find((a) => a.startsWith('--clone-threshold='));
123
+ const threshold = cloneThresholdArg ? parseFloat(cloneThresholdArg.split('=')[1]) : 0.85;
124
+ const clones = detectSemanticClones(syncRes.db, { threshold });
125
+ if (isJson) {
126
+ report.clones = clones;
127
+ } else {
128
+ process.stdout.write(formatCloneReport(clones));
129
+ }
128
130
  }
129
- }
130
- if (rawArgs.includes('--hotspot-graph')) {
131
- const { calculateCascadingHotspotGraph } = await import('../search-queries-hotspot-graph.js');
132
- const { formatHotspotGraphReport } = await import('../audit/reporter-hotspot-graph.js');
133
- const limitArg = rawArgs.find((a) => a.startsWith('--limit='));
134
- const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) : 10;
135
- const graph = calculateCascadingHotspotGraph(syncRes.db, { limit });
136
- if (isJson) {
137
- report.hotspotGraph = graph;
138
- } else {
139
- process.stdout.write(formatHotspotGraphReport(graph));
131
+ if (rawArgs.includes('--hotspot-graph')) {
132
+ const { calculateCascadingHotspotGraph } = await import('../search-queries-hotspot-graph.js');
133
+ const { formatHotspotGraphReport } = await import('../audit/reporter-hotspot-graph.js');
134
+ const limitArg = rawArgs.find((a) => a.startsWith('--limit='));
135
+ const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) : 10;
136
+ const graph = calculateCascadingHotspotGraph(syncRes.db, { limit });
137
+ if (isJson) {
138
+ report.hotspotGraph = graph;
139
+ } else {
140
+ process.stdout.write(formatHotspotGraphReport(graph));
141
+ }
140
142
  }
141
143
  }
144
+ } catch {
145
+ // Gracefully bypass indexing when database is readonly in sandboxed environment
142
146
  }
143
147
 
144
148
  // Stage 1: Atomic failure predicates
@@ -200,7 +200,24 @@ export const dispatchCommand = async (firstArg, rawArgs, runAudit, getPackageVer
200
200
  const { startUiServer } = await import('../ui-server.js');
201
201
  const portArg = rawArgs.find((a) => a.startsWith('--port='));
202
202
  const port = portArg ? parseInt(portArg.split('=')[1], 10) : 4173;
203
- await startUiServer({ port, isCli: true, cwd: process.cwd() });
203
+ const hostArg = rawArgs.find((a) => a.startsWith('--host='));
204
+ const host = hostArg ? hostArg.split('=')[1] : '0.0.0.0';
205
+ const { server } = await startUiServer({ port, host, isCli: true, cwd: process.cwd() });
206
+ await new Promise((resolve) => {
207
+ const shutdown = () => {
208
+ server.close(() => resolve());
209
+ };
210
+ process.on('SIGINT', shutdown);
211
+ process.on('SIGTERM', shutdown);
212
+ server.on('close', resolve);
213
+ });
214
+ break;
215
+ }
216
+ case 'tesseract':
217
+ case 'cube':
218
+ case 'matrix': {
219
+ const { runTesseract } = await import('../tesseract.js');
220
+ await runTesseract(rawArgs.slice(1), true);
204
221
  break;
205
222
  }
206
223
  case 'help':
@@ -208,5 +208,19 @@ export const COMMANDS_SCHEMA = [
208
208
  'npx chemx ui',
209
209
  'npx chemx dashboard --port=3000'
210
210
  ]
211
+ },
212
+ {
213
+ name: 'tesseract',
214
+ aliases: ['cube', 'matrix'],
215
+ usage: 'npx chemx tesseract [options]',
216
+ summary: 'Autonomous AI Cognitive Bridge and Human-AI Symbiosis Interface.',
217
+ description: 'Deploys the full Chemical X cognitive lattice, AST topology, swarm telemetry, and Jarvis operational directives for instant AI onboarding.',
218
+ flags: [
219
+ { flag: '--json', desc: 'Output machine-readable JSON cognitive payload' }
220
+ ],
221
+ examples: [
222
+ 'npx chemx tesseract',
223
+ 'npx chemx tesseract --json'
224
+ ]
211
225
  }
212
226
  ];
package/cli/index.js CHANGED
@@ -69,6 +69,7 @@ export const writeFile = async (...a) => (await import('./patcher.js')).wr
69
69
  export const runTeamCli = async (...a) => (await import('./team/index.js')).runTeamCli(...a);
70
70
  export const runTrend = async (...a) => (await import('./trend.js')).runTrend(...a);
71
71
  export const runPillarsWizard = async (...a) => (await import('./pillars-wizard.js')).runPillarsWizard(...a);
72
+ export const runTesseract = async (...a) => (await import('./tesseract.js')).runTesseract(...a);
72
73
  export { handleError, withErrorCatcher, publishIssue };
73
74
 
74
75
  // ---------------------------------------------------------------------------
@@ -100,6 +101,7 @@ export const ALLOWED_COMMANDS = new Set([
100
101
  'init',
101
102
  'hook', 'hooks', 'install-hooks', 'setup-ci',
102
103
  'add:prop', 'add:state', 'add:action', 'fix',
104
+ 'tesseract', 'cube', 'matrix',
103
105
  'help', '--help', '-h',
104
106
  'version', '--version', '-v'
105
107
  ]);
@@ -11,6 +11,8 @@ export const buildMcpInstructions = () => {
11
11
  'Provides all Chemical X operations through a single tool. This avoids requiring',
12
12
  'separate approval for each sub-operation in MCP clients that gate tool access per-tool.',
13
13
  'Pass the desired operation via the action parameter; see action: \'help\' for the full list.',
14
+ '0. AUTONOMOUS COGNITIVE BRIDGE (TESSERACT):',
15
+ ' - chemx({ action: "tesseract" }) or chemx({ command: "tesseract" }) -> Boot Jarvis cognitive lattice and mission protocol',
14
16
  '',
15
17
  '1. TESTING, BUILDS & VERIFICATION:',
16
18
  ' - chemx({ action: "test" }) or chemx({ command: "test" })',
@@ -33,7 +33,8 @@ export const MASTER_MCP_TOOL = {
33
33
  'generate',
34
34
  'patterns',
35
35
  'issue',
36
- 'project'
36
+ 'project',
37
+ 'tesseract'
37
38
  ],
38
39
  description: 'The Chemical X subsystem action to execute.'
39
40
  },
@@ -11,6 +11,7 @@ import {
11
11
  buildHotspotsPrompt
12
12
  } from '../audit/prompts.js';
13
13
  import { syncSearchIndex, syncViolationsIndex, recordAuditSnapshot } from '../search.js';
14
+ import { autoGenerateTasksFromAudit } from '../team/team-triage.js';
14
15
  import { resolveTargetCwd } from './tools-search.js';
15
16
 
16
17
  export const handleAudit = (args = {}, cwd = process.cwd()) => {
@@ -43,6 +44,7 @@ export const handleAudit = (args = {}, cwd = process.cwd()) => {
43
44
  if (syncRes?.db) {
44
45
  syncViolationsIndex(syncRes.db, report.violations);
45
46
  recordAuditSnapshot(syncRes.db, report);
47
+ autoGenerateTasksFromAudit(syncRes.db, { cwd: baseCwd, targetDir: resolvedTarget });
46
48
  }
47
49
  } catch (syncError) {
48
50
  process.stderr.write(`[chemx] Search index sync bypassed: ${syncError?.message || String(syncError)}\n`);
@@ -18,7 +18,8 @@ export const handleChemxTeamTask = async (args = {}, cwd = process.cwd()) => {
18
18
 
19
19
  if (action === 'list') {
20
20
  const parentId = args.parentId !== undefined ? args.parentId : args.parent;
21
- const tasks = listTasks(db, { status: args.status, assigned_agent_id: args.agentId, parentId });
21
+ const rule = args.rule || args.ruleId;
22
+ const tasks = listTasks(db, { status: args.status, assigned_agent_id: args.agentId, parentId, rule, priority: args.priority });
22
23
  const col = toColumnar(tasks, ['id', 'title', 'tier', 'status', 'priority', 'assigned_agent_id', 'target_path', 'parent_id']);
23
24
  return { ...col, total: tasks.length, card: formatTaskListCard(tasks) };
24
25
  }
package/cli/mcp/tools.js CHANGED
@@ -9,7 +9,7 @@ import { handleChemxProject } from './tools-project.js';
9
9
 
10
10
  export {
11
11
  MCP_TOOLS, ALL_MCP_TOOLS, handleChemxQ, handleChemxRead, handleChemxPatch, handleChemxCheck, handleChemxWrite,
12
- handleChemxTeamStatus, handleChemxTeamFeed, handleChemxTeamPost, handleChemxTeamTask, handleChemxTeamLock, handleChemxReportIssue, handleChemxProject
12
+ handleChemxTeamStatus, handleChemxTeamFeed, handleChemxTeamPost, handleChemxTeamTask, handleChemxTeamLock, handleChemxReportIssue, handleChemxProject, handleChemxTesseract
13
13
  };
14
14
 
15
15
  const parseCommand = (command, params) => {
@@ -40,9 +40,19 @@ const parseCommand = (command, params) => {
40
40
  }
41
41
  if (subCmd === 'autofix') return { action: 'autofix', params: { path: parts[1] || 'src', ...params } };
42
42
  if (subCmd === 'trend' || subCmd === 'trends') return { action: 'trend', params };
43
+ if (['tesseract', 'cube', 'matrix'].includes(subCmd)) return { action: 'tesseract', params };
43
44
  return { action: subCmd, params };
44
45
  };
45
46
 
47
+ const handleChemxTesseract = async (params = {}, cwd = process.cwd()) => {
48
+ const { runTesseract } = await import('../tesseract.js');
49
+ const result = await runTesseract(params.args || [], false, cwd);
50
+ const textOutput = result.text || JSON.stringify(result.payload, null, 2);
51
+ return {
52
+ content: [{ type: 'text', text: textOutput }]
53
+ };
54
+ };
55
+
46
56
  const DISPATCHER = {
47
57
  audit: handleAudit, trend: handleChemxTrend, build: handleAuditBuild, verify: handleChemxVerify,
48
58
  typecheck: handleChemxTypecheck, test: handleChemxTest, check: handleChemxCheck,
@@ -51,7 +61,8 @@ const DISPATCHER = {
51
61
  team_status: handleChemxTeamStatus, team_feed: handleChemxTeamFeed, team_post: handleChemxTeamPost,
52
62
  team_task: handleChemxTeamTask, team_lock: handleChemxTeamLock, q: handleChemxQ, search: handleChemxQ,
53
63
  autofix: handleAutofix, generate: handleGenerateCapsule, patterns: handleQueryPatterns,
54
- issue: handleChemxReportIssue, project: handleChemxProject, coordinator: handleChemxProject
64
+ issue: handleChemxReportIssue, project: handleChemxProject, coordinator: handleChemxProject,
65
+ tesseract: handleChemxTesseract, cube: handleChemxTesseract, matrix: handleChemxTesseract
55
66
  };
56
67
 
57
68
  export const handleChemx = async (args = {}, cwd = process.cwd()) => {
@@ -1,4 +1,5 @@
1
1
  import './silence-warnings.js';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { ensureChemxDir } from './audit/history.js';
4
5
  import { initTeamSchema } from './team/team-schema.js';
@@ -36,14 +37,61 @@ export const openIndexDb = (cwd = process.cwd(), options = {}) => {
36
37
  return DB_CACHE.get(dbPath);
37
38
  }
38
39
 
39
- const db = new DatabaseSync(dbPath);
40
-
40
+ let isReadOnly = false;
41
41
  try {
42
- db.exec('PRAGMA busy_timeout = 5000;');
43
- db.exec('PRAGMA journal_mode = WAL;');
44
- db.exec('PRAGMA foreign_keys = ON;');
42
+ fs.accessSync(path.dirname(dbPath), fs.constants.W_OK);
43
+ if (fs.existsSync(dbPath)) {
44
+ fs.accessSync(dbPath, fs.constants.W_OK);
45
+ }
45
46
  } catch {
46
- // Safe retry/fallback if concurrent worker holds active lock
47
+ isReadOnly = true;
48
+ }
49
+
50
+ let db = null;
51
+ try {
52
+ db = new DatabaseSync(dbPath, isReadOnly ? { readOnly: true } : {});
53
+ if (!isReadOnly) {
54
+ try {
55
+ db.exec('PRAGMA busy_timeout = 5000;');
56
+ db.exec('PRAGMA journal_mode = WAL;');
57
+ db.exec('PRAGMA foreign_keys = ON;');
58
+ } catch {
59
+ // Safe retry/fallback if concurrent worker holds active lock
60
+ }
61
+ }
62
+ } catch (err) {
63
+ if (!isReadOnly) {
64
+ try {
65
+ db = new DatabaseSync(dbPath, { readOnly: true });
66
+ isReadOnly = true;
67
+ } catch {
68
+ return null;
69
+ }
70
+ } else {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ if (isReadOnly) {
76
+ if (typeof db.function === 'function') {
77
+ try {
78
+ db.function('vec_cosine', (b1, b2) => {
79
+ if (!b1 || !b2) return 0;
80
+ const a = new Float32Array(b1.buffer, b1.byteOffset, b1.byteLength / 4);
81
+ const b = new Float32Array(b2.buffer, b2.byteOffset, b2.byteLength / 4);
82
+ let dot = 0;
83
+ const len = Math.min(a.length, b.length);
84
+ for (let i = 0; i < len; i++) {
85
+ dot += a[i] * b[i];
86
+ }
87
+ return Math.max(0, Math.min(1, dot));
88
+ });
89
+ } catch {
90
+ // Ignored if already registered
91
+ }
92
+ }
93
+ DB_CACHE.set(dbPath, db);
94
+ return db;
47
95
  }
48
96
 
49
97
  db.exec(`
package/cli/search.js CHANGED
@@ -105,6 +105,10 @@ export const syncSearchIndex = (targetDir = 'src', cwd = process.cwd(), options
105
105
  const db = openIndexDb(cwd);
106
106
  if (!db) return null;
107
107
 
108
+ const isWritable = () => { try { fs.accessSync(path.join(cwd, '.chemx'), fs.constants.W_OK); return true; } catch { return false; } };
109
+ const shouldSkipSync = !isWritable() && !options.reindex;
110
+ if (shouldSkipSync) return { db, updatedCount: 0 };
111
+
108
112
  const targetDirs = Array.isArray(targetDir) ? targetDir : [targetDir];
109
113
  if (targetDir === 'src' && fs.existsSync(path.resolve(cwd, 'cli'))) {
110
114
  targetDirs.push('cli');
@@ -25,7 +25,7 @@ import { getSwarmTokenBreakdown, formatTokenBreakdownCard } from './team-tokens.
25
25
  import { runAblationComparison, formatAblationCard } from './team-memory.js';
26
26
  import { parseFlags } from './team-flags.js';
27
27
 
28
- const ARG_VAL_FLAGS = ['--target', '--as', '--to', '--agent', '--since', '--limit', '--thread', '--task', '--parent'];
28
+ const ARG_VAL_FLAGS = ['--target', '--as', '--to', '--agent', '--since', '--limit', '--thread', '--task', '--parent', '--rule', '--priority', '--prio'];
29
29
 
30
30
  export const runTeamCli = (rawArgs = [], isCli = false, cwd = process.cwd()) => {
31
31
  const db = openIndexDb(cwd);
@@ -119,7 +119,9 @@ export const runTeamCli = (rawArgs = [], isCli = false, cwd = process.cwd()) =>
119
119
  const tasks = listTasks(db, {
120
120
  status: flags.status,
121
121
  assigned_agent_id: flags.agent,
122
- parentId: flags.parent
122
+ parentId: flags.parent,
123
+ rule: flags.rule,
124
+ priority: flags.priority
123
125
  });
124
126
  if (flags.isJson) {
125
127
  const col = toColumnar(tasks, ['id', 'title', 'tier', 'status', 'priority', 'assigned_agent_id', 'target_path', 'parent_id']);
@@ -76,16 +76,20 @@ export const cleanExpiredLeases = (db) => {
76
76
  if (!hasDb) return [];
77
77
 
78
78
  const now = Date.now();
79
- const expired = db.prepare('SELECT * FROM file_leases WHERE expires_at <= ?').all(now);
80
- for (const lease of expired) {
81
- db.prepare('DELETE FROM file_leases WHERE file_path = ?').run(lease.file_path);
82
- postFeedEvent(db, {
83
- author_id: '@system',
84
- event_type: 'lock_expired',
85
- file_path: lease.file_path,
86
- message: `Lease expired for ${lease.file_path} held by ${lease.locked_by}`
87
- });
88
- promoteNextWaiter(db, lease.file_path);
79
+ try {
80
+ const expired = db.prepare('SELECT * FROM file_leases WHERE expires_at <= ?').all(now);
81
+ for (const lease of expired) {
82
+ db.prepare('DELETE FROM file_leases WHERE file_path = ?').run(lease.file_path);
83
+ postFeedEvent(db, {
84
+ author_id: '@system',
85
+ event_type: 'lock_expired',
86
+ file_path: lease.file_path,
87
+ message: `Lease expired for ${lease.file_path} held by ${lease.locked_by}`
88
+ });
89
+ promoteNextWaiter(db, lease.file_path);
90
+ }
91
+ return expired;
92
+ } catch {
93
+ return [];
89
94
  }
90
- return expired;
91
95
  };
@@ -90,7 +90,57 @@ export const buildTaskListQuery = (filter = {}) => {
90
90
  conditions.push('parent_id = ?');
91
91
  params.push(Number(parentId));
92
92
  }
93
+ const ruleFilter = filter.rule || filter.rule_id;
94
+ if (ruleFilter) {
95
+ conditions.push('(rule_id = ? OR rule_id LIKE ?)');
96
+ params.push(ruleFilter, `%${ruleFilter}%`);
97
+ }
98
+ const hasPriority = filter.priority !== undefined && filter.priority !== null && filter.priority !== '';
99
+ if (hasPriority) {
100
+ conditions.push('priority = ?');
101
+ params.push(Number(filter.priority));
102
+ }
93
103
  if (conditions.length > 0) query += ` WHERE ${conditions.join(' AND ')}`;
94
104
  query += ' ORDER BY priority ASC, id ASC';
95
105
  return { query, params };
96
106
  };
107
+
108
+ export const queryUnassignedHazards = (db) => {
109
+ if (!db) return [];
110
+ const query = `
111
+ SELECT
112
+ COALESCE(v.file_path, f.path) as path,
113
+ COALESCE(f.tier, 'molecule') as tier,
114
+ COALESCE(f.lines, 0) as lines,
115
+ COALESCE(f.health_score, CASE WHEN COUNT(v.id) > 0 THEN MAX(20, 100 - COUNT(v.id) * 15) ELSE 100 END) as health_score,
116
+ COALESCE(MAX(f.hazard_count, COUNT(v.id)), COUNT(v.id)) as hazard_count,
117
+ COUNT(v.id) as violation_count,
118
+ GROUP_CONCAT(DISTINCT v.rule) as rules_summary
119
+ FROM violations v
120
+ LEFT JOIN files f ON f.path = v.file_path
121
+ LEFT JOIN agent_tasks t ON t.target_path = v.file_path AND t.status IN ('queued', 'in_progress', 'review')
122
+ WHERE t.id IS NULL
123
+ GROUP BY v.file_path
124
+ UNION
125
+ SELECT
126
+ f.path,
127
+ f.tier,
128
+ f.lines,
129
+ f.health_score,
130
+ f.hazard_count,
131
+ f.hazard_count as violation_count,
132
+ 'ARCHITECTURAL_HAZARD' as rules_summary
133
+ FROM files f
134
+ LEFT JOIN agent_tasks t ON t.target_path = f.path AND t.status IN ('queued', 'in_progress', 'review')
135
+ WHERE (f.health_score < 90 OR f.hazard_count > 0)
136
+ AND t.id IS NULL
137
+ AND f.path NOT IN (SELECT file_path FROM violations)
138
+ GROUP BY f.path
139
+ ORDER BY health_score ASC, hazard_count DESC
140
+ `;
141
+ try {
142
+ return db.prepare(query).all();
143
+ } catch {
144
+ return [];
145
+ }
146
+ };
@@ -48,7 +48,10 @@ export const parseFlags = (args = []) => {
48
48
  if (arg.startsWith('--target=')) flags.target = arg.split('=')[1];
49
49
  if (arg === '--target' && hasNext) flags.target = nextArg;
50
50
  if (arg.startsWith('--tier=')) flags.tier = arg.split('=')[1];
51
- if (arg.startsWith('--prio=')) flags.priority = parseInt(arg.split('=')[1], 10);
51
+ if (arg.startsWith('--rule=')) flags.rule = arg.split('=')[1];
52
+ if (arg === '--rule' && hasNext) flags.rule = nextArg;
53
+ if (arg.startsWith('--prio=') || arg.startsWith('--priority=')) flags.priority = parseInt(arg.split('=')[1], 10);
54
+ if ((arg === '--prio' || arg === '--priority') && hasNext) flags.priority = parseInt(nextArg, 10);
52
55
  if (arg.startsWith('--purpose=')) flags.purpose = arg.split('=')[1];
53
56
  if (arg.startsWith('--tokens=')) flags.tokens = parseInt(arg.split('=')[1], 10);
54
57
  if (arg.startsWith('--prompt-tokens=')) flags.promptTokens = parseInt(arg.split('=')[1], 10);