ctx-cc 3.4.4 → 4.0.0

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.
Files changed (72) hide show
  1. package/README.md +34 -289
  2. package/agents/ctx-arch-mapper.md +5 -3
  3. package/agents/ctx-auditor.md +5 -3
  4. package/agents/ctx-concerns-mapper.md +5 -3
  5. package/agents/ctx-criteria-suggester.md +6 -4
  6. package/agents/ctx-debugger.md +5 -3
  7. package/agents/ctx-designer.md +488 -114
  8. package/agents/ctx-discusser.md +5 -3
  9. package/agents/ctx-executor.md +5 -3
  10. package/agents/ctx-handoff.md +6 -4
  11. package/agents/ctx-learner.md +5 -3
  12. package/agents/ctx-mapper.md +4 -3
  13. package/agents/ctx-ml-analyst.md +600 -0
  14. package/agents/ctx-ml-engineer.md +933 -0
  15. package/agents/ctx-ml-reviewer.md +485 -0
  16. package/agents/ctx-ml-scientist.md +626 -0
  17. package/agents/ctx-parallelizer.md +4 -3
  18. package/agents/ctx-planner.md +5 -3
  19. package/agents/ctx-predictor.md +4 -3
  20. package/agents/ctx-qa.md +5 -3
  21. package/agents/ctx-quality-mapper.md +5 -3
  22. package/agents/ctx-researcher.md +5 -3
  23. package/agents/ctx-reviewer.md +6 -4
  24. package/agents/ctx-team-coordinator.md +5 -3
  25. package/agents/ctx-tech-mapper.md +5 -3
  26. package/agents/ctx-verifier.md +5 -3
  27. package/bin/ctx.js +168 -27
  28. package/commands/brand.md +309 -0
  29. package/commands/ctx.md +234 -114
  30. package/commands/design.md +304 -0
  31. package/commands/experiment.md +251 -0
  32. package/commands/help.md +57 -7
  33. package/commands/metrics.md +1 -1
  34. package/commands/milestone.md +1 -1
  35. package/commands/ml-status.md +197 -0
  36. package/commands/monitor.md +1 -1
  37. package/commands/train.md +266 -0
  38. package/commands/visual-qa.md +559 -0
  39. package/commands/voice.md +1 -1
  40. package/hooks/post-tool-use.js +39 -0
  41. package/hooks/pre-tool-use.js +93 -0
  42. package/hooks/subagent-stop.js +32 -0
  43. package/package.json +9 -3
  44. package/plugin.json +45 -0
  45. package/skills/ctx-design-system/SKILL.md +572 -0
  46. package/skills/ctx-ml-experiment/SKILL.md +334 -0
  47. package/skills/ctx-ml-pipeline/SKILL.md +437 -0
  48. package/skills/ctx-orchestrator/SKILL.md +91 -0
  49. package/skills/ctx-review-gate/SKILL.md +111 -0
  50. package/skills/ctx-state/SKILL.md +100 -0
  51. package/skills/ctx-visual-qa/SKILL.md +587 -0
  52. package/src/agents.js +109 -0
  53. package/src/auto.js +287 -0
  54. package/src/capabilities.js +171 -0
  55. package/src/commits.js +94 -0
  56. package/src/config.js +112 -0
  57. package/src/context.js +241 -0
  58. package/src/handoff.js +156 -0
  59. package/src/hooks.js +218 -0
  60. package/src/install.js +119 -51
  61. package/src/lifecycle.js +194 -0
  62. package/src/metrics.js +198 -0
  63. package/src/pipeline.js +269 -0
  64. package/src/review-gate.js +244 -0
  65. package/src/runner.js +120 -0
  66. package/src/skills.js +143 -0
  67. package/src/state.js +267 -0
  68. package/src/worktree.js +244 -0
  69. package/templates/PRD.json +1 -1
  70. package/templates/config.json +1 -237
  71. package/workflows/ctx-router.md +0 -485
  72. package/workflows/map-codebase.md +0 -329
@@ -0,0 +1,244 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { readState, writeState } from './state.js';
5
+ import { buildContext } from './context.js';
6
+ import { runAgent } from './runner.js';
7
+
8
+ const WORKTREES_DIR = '.ctx/worktrees';
9
+
10
+ /**
11
+ * Parse a dependency graph from the parallelizer agent output.
12
+ * Input format: array of { taskId, title, dependsOn: [taskId] }
13
+ * Returns waves: [ [task, task], [task], ... ]
14
+ */
15
+ export function buildWaves(tasks) {
16
+ if (!tasks || tasks.length === 0) return [];
17
+
18
+ const completed = new Set();
19
+ const waves = [];
20
+ const remaining = [...tasks];
21
+ let safetyCounter = 0;
22
+
23
+ while (remaining.length > 0 && safetyCounter < 100) {
24
+ safetyCounter++;
25
+ const wave = [];
26
+
27
+ for (let i = remaining.length - 1; i >= 0; i--) {
28
+ const task = remaining[i];
29
+ const deps = task.dependsOn || [];
30
+ const allDepsComplete = deps.every(d => completed.has(d));
31
+
32
+ if (allDepsComplete) {
33
+ wave.push(task);
34
+ remaining.splice(i, 1);
35
+ }
36
+ }
37
+
38
+ if (wave.length === 0) {
39
+ // Circular dependency — force remaining into a single wave
40
+ waves.push(remaining.splice(0));
41
+ break;
42
+ }
43
+
44
+ waves.push(wave);
45
+ for (const t of wave) completed.add(t.taskId);
46
+ }
47
+
48
+ return waves;
49
+ }
50
+
51
+ /**
52
+ * Create a git worktree for a task.
53
+ * Returns the worktree path.
54
+ */
55
+ export function createWorktree(projectDir, taskId) {
56
+ const worktreeBase = path.join(projectDir, WORKTREES_DIR);
57
+ if (!fs.existsSync(worktreeBase)) fs.mkdirSync(worktreeBase, { recursive: true });
58
+
59
+ const worktreePath = path.join(worktreeBase, taskId);
60
+ const branchName = `ctx-task-${taskId}`;
61
+
62
+ try {
63
+ // Create branch and worktree
64
+ execSync(`git worktree add "${worktreePath}" -b "${branchName}" HEAD`, {
65
+ cwd: projectDir, encoding: 'utf-8', timeout: 10000,
66
+ });
67
+ return worktreePath;
68
+ } catch (err) {
69
+ throw new Error(`Failed to create worktree for ${taskId}: ${err.message}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Merge a worktree branch back to the current branch.
75
+ * Returns { merged: boolean, conflicts: boolean, error: string|null }
76
+ */
77
+ export function mergeWorktree(projectDir, taskId) {
78
+ const branchName = `ctx-task-${taskId}`;
79
+
80
+ try {
81
+ const result = execSync(`git merge --no-ff "${branchName}" -m "ctx(merge): task ${taskId}"`, {
82
+ cwd: projectDir, encoding: 'utf-8', timeout: 10000,
83
+ });
84
+ return { merged: true, conflicts: false, error: null };
85
+ } catch (err) {
86
+ if (err.message.includes('CONFLICT') || err.message.includes('Merge conflict')) {
87
+ // Abort the merge
88
+ try { execSync('git merge --abort', { cwd: projectDir, timeout: 5000 }); } catch {}
89
+ return { merged: false, conflicts: true, error: `Merge conflicts in task ${taskId}` };
90
+ }
91
+ return { merged: false, conflicts: false, error: err.message };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Clean up a worktree and its branch.
97
+ */
98
+ export function cleanupWorktree(projectDir, taskId) {
99
+ const worktreePath = path.join(projectDir, WORKTREES_DIR, taskId);
100
+ const branchName = `ctx-task-${taskId}`;
101
+
102
+ try {
103
+ execSync(`git worktree remove "${worktreePath}" --force`, {
104
+ cwd: projectDir, timeout: 10000,
105
+ });
106
+ } catch {
107
+ // Manual cleanup
108
+ if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true });
109
+ try { execSync('git worktree prune', { cwd: projectDir, timeout: 5000 }); } catch {}
110
+ }
111
+
112
+ // Delete branch
113
+ try {
114
+ execSync(`git branch -D "${branchName}"`, { cwd: projectDir, timeout: 5000 });
115
+ } catch {}
116
+ }
117
+
118
+ /**
119
+ * Execute a wave of tasks in parallel using git worktrees.
120
+ * Each task runs in its own worktree with a separate claude process.
121
+ *
122
+ * Returns { results: [{ taskId, success, error }], conflicts: [taskId] }
123
+ */
124
+ export async function executeWave({ wave, agentsDir, agentFile, projectDir, ctxDir, streaming = false, timeout = 300000 }) {
125
+ const agentPath = path.join(agentsDir, agentFile);
126
+
127
+ // Create worktrees for all tasks
128
+ const worktrees = [];
129
+ for (const task of wave) {
130
+ try {
131
+ const wtPath = createWorktree(projectDir, task.taskId);
132
+ worktrees.push({ task, worktreePath: wtPath });
133
+ } catch (err) {
134
+ worktrees.push({ task, worktreePath: null, error: err.message });
135
+ }
136
+ }
137
+
138
+ // Run agents in parallel (non-streaming for parallel execution)
139
+ const promises = worktrees.map(async ({ task, worktreePath, error }) => {
140
+ if (error) return { taskId: task.taskId, success: false, error };
141
+
142
+ try {
143
+ const { context } = buildContext('execute', worktreePath, ctxDir);
144
+ await runAgent({
145
+ agentPath,
146
+ message: `Execute task: ${task.title}\n\n${task.description || ''}`,
147
+ streaming: false, // Must be non-streaming for parallel
148
+ timeout,
149
+ context,
150
+ });
151
+ return { taskId: task.taskId, success: true, error: null };
152
+ } catch (err) {
153
+ return { taskId: task.taskId, success: false, error: err.message };
154
+ }
155
+ });
156
+
157
+ const results = await Promise.all(promises);
158
+
159
+ // Merge successful worktrees back
160
+ const conflicts = [];
161
+ for (const result of results) {
162
+ if (result.success) {
163
+ const merge = mergeWorktree(projectDir, result.taskId);
164
+ if (merge.conflicts) {
165
+ conflicts.push(result.taskId);
166
+ result.success = false;
167
+ result.error = merge.error;
168
+ }
169
+ }
170
+ // Cleanup worktree
171
+ cleanupWorktree(projectDir, result.taskId);
172
+ }
173
+
174
+ return { results, conflicts };
175
+ }
176
+
177
+ /**
178
+ * Execute all waves sequentially (wave N+1 waits for wave N).
179
+ *
180
+ * Options:
181
+ * waves, agentsDir, agentFile, projectDir, ctxDir, streaming, timeout, onWave
182
+ *
183
+ * Returns { waveResults: [...], conflicts: [...] }
184
+ */
185
+ export async function executeAllWaves({ waves, agentsDir, agentFile, projectDir, ctxDir, streaming = false, timeout = 300000, onWave = null }) {
186
+ const allResults = [];
187
+ const allConflicts = [];
188
+
189
+ for (let i = 0; i < waves.length; i++) {
190
+ const wave = waves[i];
191
+ if (onWave) onWave({ waveIndex: i, total: waves.length, taskCount: wave.length, status: 'starting' });
192
+
193
+ const { results, conflicts } = await executeWave({
194
+ wave, agentsDir, agentFile, projectDir, ctxDir, streaming, timeout,
195
+ });
196
+
197
+ allResults.push({ waveIndex: i, results });
198
+ allConflicts.push(...conflicts);
199
+
200
+ if (onWave) onWave({ waveIndex: i, total: waves.length, taskCount: wave.length, status: 'completed', results });
201
+
202
+ // Update state with wave progress
203
+ const state = readState(ctxDir);
204
+ if (state) {
205
+ state.waveProgress = state.waveProgress || [];
206
+ state.waveProgress.push({
207
+ waveIndex: i,
208
+ completedAt: new Date().toISOString(),
209
+ taskResults: results.map(r => ({ taskId: r.taskId, success: r.success })),
210
+ });
211
+ writeState(ctxDir, state);
212
+ }
213
+ }
214
+
215
+ // Re-queue conflicting tasks as sequential
216
+ if (allConflicts.length > 0) {
217
+ const state = readState(ctxDir);
218
+ if (state) {
219
+ state.conflictTasks = allConflicts;
220
+ writeState(ctxDir, state);
221
+ }
222
+ }
223
+
224
+ return { waveResults: allResults, conflicts: allConflicts };
225
+ }
226
+
227
+ /**
228
+ * Format wave execution results for display.
229
+ */
230
+ export function formatWaveResults(waveResults, conflicts) {
231
+ const lines = [];
232
+ for (const wave of waveResults) {
233
+ lines.push(` Wave ${wave.waveIndex + 1}:`);
234
+ for (const r of wave.results) {
235
+ const icon = r.success ? '✓' : '✗';
236
+ lines.push(` ${icon} ${r.taskId}${r.error ? ` — ${r.error}` : ''}`);
237
+ }
238
+ }
239
+ if (conflicts.length > 0) {
240
+ lines.push('');
241
+ lines.push(` ⚠ Merge conflicts (re-queued as sequential): ${conflicts.join(', ')}`);
242
+ }
243
+ return lines.join('\n');
244
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://ctx.dev/schemas/prd.json",
3
- "version": "2.1",
3
+ "version": "3.5",
4
4
  "project": {
5
5
  "name": "{{project_name}}",
6
6
  "description": "{{project_description}}",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://ctx.dev/schemas/config.json",
3
- "version": "3.3",
3
+ "version": "3.5",
4
4
 
5
5
  "models": {
6
6
  "architect": {
@@ -109,38 +109,6 @@
109
109
  "forceCheckpointThreshold": 0.7
110
110
  },
111
111
 
112
- "parallelization": {
113
- "enabled": true,
114
- "maxParallelTasks": 3,
115
- "requireDependencyAnalysis": true,
116
- "fileLocking": true
117
- },
118
-
119
- "review": {
120
- "enabled": true,
121
- "beforeCommit": true,
122
- "blockOnCritical": true,
123
- "blockOnHigh": true,
124
- "warnOnMedium": true,
125
- "autoFix": false,
126
- "checks": {
127
- "typeErrors": true,
128
- "importResolution": true,
129
- "circularDeps": true,
130
- "security": true,
131
- "bestPractices": true,
132
- "consoleLogs": true,
133
- "emptyBlocks": true
134
- }
135
- },
136
-
137
- "criteria": {
138
- "autoSuggest": true,
139
- "minCriteria": 5,
140
- "maxCriteria": 15,
141
- "categories": ["happy_path", "validation", "error_states", "security", "performance"]
142
- },
143
-
144
112
  "debug": {
145
113
  "maxAttempts": 10,
146
114
  "persistSessions": true,
@@ -148,214 +116,10 @@
148
116
  "autoResume": true
149
117
  },
150
118
 
151
- "integrations": {
152
- "linear": {
153
- "enabled": false,
154
- "apiKey": "env:LINEAR_API_KEY",
155
- "teamId": null,
156
- "projectId": null,
157
- "syncOnCreate": true,
158
- "syncOnVerify": true,
159
- "closeOnComplete": true,
160
- "statusMapping": {
161
- "pending": "Todo",
162
- "executing": "In Progress",
163
- "verifying": "In Review",
164
- "complete": "Done"
165
- }
166
- },
167
- "jira": {
168
- "enabled": false,
169
- "baseUrl": null,
170
- "email": "env:JIRA_EMAIL",
171
- "apiToken": "env:JIRA_API_TOKEN",
172
- "projectKey": null,
173
- "issueType": "Story",
174
- "syncOnCreate": true,
175
- "syncOnVerify": true,
176
- "transitionMapping": {
177
- "executing": "In Progress",
178
- "verifying": "In Review",
179
- "complete": "Done"
180
- }
181
- },
182
- "github": {
183
- "enabled": false,
184
- "repository": null,
185
- "token": "env:GITHUB_TOKEN",
186
- "syncIssues": true,
187
- "closeOnComplete": true,
188
- "createPROnComplete": false,
189
- "labels": {
190
- "feature": "enhancement",
191
- "bug": "bug",
192
- "design": "design"
193
- }
194
- },
195
- "slack": {
196
- "enabled": false,
197
- "webhookUrl": "env:SLACK_WEBHOOK_URL",
198
- "notifyOnPhaseComplete": false,
199
- "notifyOnVerifyFail": true,
200
- "notifyOnHandoff": false
201
- }
202
- },
203
-
204
119
  "ui": {
205
120
  "showTokenUsage": true,
206
121
  "showModelInfo": true,
207
122
  "showWaveProgress": true,
208
123
  "verboseOutput": false
209
- },
210
-
211
- "team": {
212
- "enabled": false,
213
- "members": [],
214
- "fileLocking": {
215
- "enabled": true,
216
- "expiryMinutes": 60,
217
- "autoRelease": true
218
- },
219
- "notifications": {
220
- "onPhaseStart": false,
221
- "onPhaseComplete": true,
222
- "onVerifyFail": true,
223
- "onConflict": true,
224
- "onMilestoneComplete": true
225
- },
226
- "branching": {
227
- "strategy": "feature",
228
- "prefix": "ctx"
229
- }
230
- },
231
-
232
- "audit": {
233
- "enabled": true,
234
- "retention": {
235
- "daily": "90d",
236
- "weekly": "1y",
237
- "monthly": "3y"
238
- },
239
- "logging": {
240
- "tokens": true,
241
- "costs": true,
242
- "decisions": true,
243
- "fileAccess": true,
244
- "externalCalls": true
245
- },
246
- "compliance": {
247
- "soc2": false,
248
- "hipaa": false,
249
- "gdpr": false
250
- },
251
- "reports": {
252
- "dailySummary": true,
253
- "weeklySummary": true,
254
- "exportFormat": "json"
255
- }
256
- },
257
-
258
- "metrics": {
259
- "enabled": true,
260
- "collection": {
261
- "tokens": true,
262
- "costs": true,
263
- "timing": true,
264
- "quality": true
265
- },
266
- "aggregation": {
267
- "daily": true,
268
- "weekly": true,
269
- "monthly": true
270
- },
271
- "costRates": {
272
- "opus": {"input": 0.015, "output": 0.075},
273
- "sonnet": {"input": 0.003, "output": 0.015},
274
- "haiku": {"input": 0.00025, "output": 0.00125}
275
- },
276
- "roi": {
277
- "developerHourlyRate": 150,
278
- "hoursPerStoryManual": 4
279
- },
280
- "alerts": {
281
- "dailyCostThreshold": 10,
282
- "weeklyStoriesTarget": 10
283
- }
284
- },
285
-
286
- "milestone": {
287
- "autoTag": true,
288
- "tagFormat": "v{version}",
289
- "archiveOnComplete": true,
290
- "requireAudit": true,
291
- "gapPhasesEnabled": true
292
- },
293
-
294
- "learning": {
295
- "enabled": true,
296
- "observePatterns": true,
297
- "observeDecisions": true,
298
- "observeFailures": true,
299
- "minConfidence": 0.7,
300
- "applyPatterns": true,
301
- "enforceConventions": true,
302
- "memoryPath": ".ctx/memory/"
303
- },
304
-
305
- "predict": {
306
- "enabled": true,
307
- "autoSuggest": {
308
- "onMilestoneComplete": true,
309
- "onAllStoriesPass": true,
310
- "weeklyDigest": false
311
- },
312
- "categories": ["revenue", "engagement", "retention", "operations", "performance", "security"],
313
- "maxSuggestions": 10,
314
- "minROI": 5,
315
- "includeQuickWins": true,
316
- "researchDepth": "balanced"
317
- },
318
-
319
- "monitor": {
320
- "enabled": false,
321
- "pollInterval": "5m",
322
- "autoFix": {
323
- "enabled": false,
324
- "safePatterns": ["null-check", "import-missing", "type-mismatch"],
325
- "requireReview": true,
326
- "createPR": true
327
- },
328
- "sentry": {
329
- "enabled": false,
330
- "token": "env:SENTRY_AUTH_TOKEN",
331
- "organization": null,
332
- "project": null,
333
- "environment": "production"
334
- },
335
- "logrocket": {
336
- "enabled": false,
337
- "appId": null,
338
- "apiKey": "env:LOGROCKET_API_KEY"
339
- },
340
- "alerts": {
341
- "errorSpike": {
342
- "threshold": 50,
343
- "window": "5m"
344
- },
345
- "slack": true,
346
- "pagerduty": false
347
- }
348
- },
349
-
350
- "voice": {
351
- "enabled": false,
352
- "engine": "auto",
353
- "language": "en-US",
354
- "wakeWord": "Hey CTX",
355
- "confirmCommands": true,
356
- "dictationEndPhrase": "done",
357
- "dictationPauseSeconds": 3,
358
- "noiseReduction": true,
359
- "minConfidence": 0.7
360
124
  }
361
125
  }