ctx-cc 4.1.1 → 4.1.2
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 +5 -5
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/auto.js +0 -287
- package/src/commits.js +0 -94
- package/src/context.js +0 -241
- package/src/handoff.js +0 -156
- package/src/hooks.js +0 -218
- package/src/lifecycle.js +0 -194
- package/src/metrics.js +0 -198
- package/src/pipeline.js +0 -269
- package/src/review-gate.js +0 -338
- package/src/runner.js +0 -120
- package/src/state.js +0 -267
- package/src/worktree.js +0 -244
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/ctx-cc)
|
|
15
15
|
[](https://opensource.org/licenses/MIT)
|
|
16
|
-
[](#testing)
|
|
17
17
|
[](#)
|
|
18
18
|
|
|
19
19
|
```bash
|
|
@@ -493,7 +493,7 @@ Options:
|
|
|
493
493
|
```bash
|
|
494
494
|
git clone https://github.com/jufjuf/CTX.git
|
|
495
495
|
cd CTX
|
|
496
|
-
npm test #
|
|
496
|
+
npm test # 149 tests, node:test runner
|
|
497
497
|
```
|
|
498
498
|
|
|
499
499
|
**Project structure:**
|
|
@@ -504,8 +504,8 @@ ctx-cc/
|
|
|
504
504
|
├── skills/ 7 skill directories (each contains SKILL.md)
|
|
505
505
|
├── commands/ 26 slash command definitions (.md)
|
|
506
506
|
├── hooks/ 3 enforcement hook scripts (.js)
|
|
507
|
-
├── src/
|
|
508
|
-
├── test/
|
|
507
|
+
├── src/ 5 source modules (.js)
|
|
508
|
+
├── test/ 8 test files (.test.js)
|
|
509
509
|
├── templates/ config.json, PRD.json, state templates
|
|
510
510
|
├── bin/ctx.js CLI entry point (installer only)
|
|
511
511
|
├── plugin.json Marketplace manifest
|
|
@@ -518,7 +518,7 @@ ctx-cc/
|
|
|
518
518
|
|
|
519
519
|
```bash
|
|
520
520
|
npm test
|
|
521
|
-
#
|
|
521
|
+
# 149 tests, 0 failures, ~1s
|
|
522
522
|
```
|
|
523
523
|
|
|
524
524
|
**Coverage:**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ctx-cc",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "CTX 4.0 — Intelligent workflow orchestration for Claude Code. 26 subagents, 7 skills, deterministic hooks. Phase-based lifecycle with autonomous execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ctx",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "CTX — Intelligent workflow orchestration for Claude Code. Specialized agents, phase-based lifecycle, three-stage review gate with OpenAI Codex cross-model review, autonomous execution.",
|
|
5
5
|
"author": "jufjuf",
|
|
6
6
|
"license": "MIT",
|
package/src/auto.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { readState, writeState, initState, transitionPhase } from './state.js';
|
|
4
|
-
import { executePipeline } from './pipeline.js';
|
|
5
|
-
import { runReviewGate, isReviewGateEnabled } from './review-gate.js';
|
|
6
|
-
import { selectStory, listPendingStories } from './lifecycle.js';
|
|
7
|
-
import { commitTask } from './commits.js';
|
|
8
|
-
|
|
9
|
-
const STOP_FILE = 'STOP';
|
|
10
|
-
const AUTO_LOG = 'AUTO-LOG.md';
|
|
11
|
-
|
|
12
|
-
const DEFAULTS = {
|
|
13
|
-
maxIterationsPerStory: 5,
|
|
14
|
-
maxTotalTimeMs: 2 * 60 * 60 * 1000, // 2 hours
|
|
15
|
-
pipeline: ['plan', 'execute'],
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Run autonomous execution loop across all P1 stories (or a single story).
|
|
20
|
-
*
|
|
21
|
-
* Options:
|
|
22
|
-
* ctxDir, projectDir, agentsDir, config
|
|
23
|
-
* storyId — single story to process (null = all P1)
|
|
24
|
-
* retryFailed — only retry previously failed stories
|
|
25
|
-
* streaming — stream agent output
|
|
26
|
-
* timeout — per-agent timeout in ms
|
|
27
|
-
* onEvent — callback({ type, story, message, ... })
|
|
28
|
-
*/
|
|
29
|
-
export async function runAutoLoop({ ctxDir, projectDir, agentsDir, config = {}, storyId = null, retryFailed = false, streaming = true, timeout = 300000, onEvent = null }) {
|
|
30
|
-
const maxIterations = config.maxIterationsPerStory || DEFAULTS.maxIterationsPerStory;
|
|
31
|
-
const maxTime = config.maxTotalTimeMs || DEFAULTS.maxTotalTimeMs;
|
|
32
|
-
const startTime = Date.now();
|
|
33
|
-
|
|
34
|
-
// Initialize auto log
|
|
35
|
-
const logPath = path.join(ctxDir, AUTO_LOG);
|
|
36
|
-
appendLog(logPath, `# CTX Auto Loop — ${new Date().toISOString()}\n`);
|
|
37
|
-
|
|
38
|
-
// Determine stories to process
|
|
39
|
-
const stories = resolveStories(ctxDir, storyId, retryFailed);
|
|
40
|
-
if (stories.length === 0) {
|
|
41
|
-
emit(onEvent, { type: 'no_stories', message: 'No stories to process.' });
|
|
42
|
-
return { completed: [], failed: [], skipped: [], totalTime: 0 };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
emit(onEvent, { type: 'start', storyCount: stories.length, maxIterations, maxTime });
|
|
46
|
-
appendLog(logPath, `\nProcessing ${stories.length} stories. Max ${maxIterations} iterations each.\n`);
|
|
47
|
-
|
|
48
|
-
const completed = [];
|
|
49
|
-
const failed = [];
|
|
50
|
-
const skipped = [];
|
|
51
|
-
|
|
52
|
-
for (const story of stories) {
|
|
53
|
-
// Check stop file
|
|
54
|
-
if (shouldStop(ctxDir)) {
|
|
55
|
-
emit(onEvent, { type: 'stopped', message: 'STOP file detected. Halting after current story.' });
|
|
56
|
-
appendLog(logPath, `\n⏹ Stopped by STOP file at ${new Date().toISOString()}\n`);
|
|
57
|
-
skipped.push(...stories.slice(stories.indexOf(story)));
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check time limit
|
|
62
|
-
if (Date.now() - startTime > maxTime) {
|
|
63
|
-
emit(onEvent, { type: 'timeout', message: `Time limit (${maxTime / 1000 / 60}min) exceeded.` });
|
|
64
|
-
appendLog(logPath, `\n⏱ Time limit exceeded at ${new Date().toISOString()}\n`);
|
|
65
|
-
skipped.push(...stories.slice(stories.indexOf(story)));
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
emit(onEvent, { type: 'story_start', story: story.id, title: story.title });
|
|
70
|
-
appendLog(logPath, `\n## ${story.id} — ${story.title}\nStarted: ${new Date().toISOString()}\n`);
|
|
71
|
-
|
|
72
|
-
const result = await processStory({
|
|
73
|
-
story, ctxDir, projectDir, agentsDir, config,
|
|
74
|
-
maxIterations, streaming, timeout, onEvent, logPath,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (result.success) {
|
|
78
|
-
completed.push(story.id);
|
|
79
|
-
appendLog(logPath, `Result: ✓ COMPLETED (${result.iterations} iterations)\n`);
|
|
80
|
-
} else {
|
|
81
|
-
failed.push({ id: story.id, reason: result.reason });
|
|
82
|
-
appendLog(logPath, `Result: ✗ FAILED — ${result.reason}\n`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Write summary
|
|
87
|
-
const totalTime = Date.now() - startTime;
|
|
88
|
-
const summary = buildSummary(completed, failed, skipped, totalTime);
|
|
89
|
-
appendLog(logPath, `\n---\n${summary}`);
|
|
90
|
-
|
|
91
|
-
emit(onEvent, { type: 'complete', completed, failed, skipped, totalTime });
|
|
92
|
-
|
|
93
|
-
// Clean up stop file if it exists
|
|
94
|
-
cleanupStopFile(ctxDir);
|
|
95
|
-
|
|
96
|
-
return { completed, failed, skipped, totalTime };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Create a STOP file to gracefully halt the auto loop.
|
|
101
|
-
*/
|
|
102
|
-
export function createStopFile(ctxDir) {
|
|
103
|
-
const stopPath = path.join(ctxDir, STOP_FILE);
|
|
104
|
-
fs.writeFileSync(stopPath, `Stop requested at ${new Date().toISOString()}\n`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Format auto loop results for display.
|
|
109
|
-
*/
|
|
110
|
-
export function formatAutoResult({ completed, failed, skipped, totalTime }) {
|
|
111
|
-
const lines = [];
|
|
112
|
-
const mins = Math.round(totalTime / 1000 / 60);
|
|
113
|
-
|
|
114
|
-
lines.push(` Total time: ${mins} minutes`);
|
|
115
|
-
lines.push('');
|
|
116
|
-
|
|
117
|
-
if (completed.length > 0) {
|
|
118
|
-
lines.push(` ✓ Completed (${completed.length}):`);
|
|
119
|
-
for (const id of completed) lines.push(` ${id}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (failed.length > 0) {
|
|
123
|
-
lines.push(` ✗ Failed (${failed.length}):`);
|
|
124
|
-
for (const f of failed) lines.push(` ${f.id} — ${f.reason}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (skipped.length > 0) {
|
|
128
|
-
lines.push(` ○ Skipped (${skipped.length}):`);
|
|
129
|
-
for (const s of skipped) lines.push(` ${s.id || s}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (failed.length > 0) {
|
|
133
|
-
lines.push('');
|
|
134
|
-
lines.push(' Retry failed: ctx-cc auto --retry-failed');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return lines.join('\n');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --- internal ---
|
|
141
|
-
|
|
142
|
-
async function processStory({ story, ctxDir, projectDir, agentsDir, config, maxIterations, streaming, timeout, onEvent, logPath }) {
|
|
143
|
-
// Select story
|
|
144
|
-
selectStory(ctxDir, story.id);
|
|
145
|
-
|
|
146
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
147
|
-
emit(onEvent, { type: 'iteration', story: story.id, iteration, max: maxIterations });
|
|
148
|
-
appendLog(logPath, ` Iteration ${iteration}/${maxIterations}: `);
|
|
149
|
-
|
|
150
|
-
// Run pipeline: plan → execute
|
|
151
|
-
try {
|
|
152
|
-
transitionPhase(ctxDir, 'init'); // Reset to init for fresh pipeline
|
|
153
|
-
const pipeResult = await executePipeline({
|
|
154
|
-
steps: ['plan', 'execute'],
|
|
155
|
-
message: `Implement story ${story.id}: ${story.title}\n\n${story.description || ''}\n\nAcceptance criteria:\n${(story.acceptanceCriteria || []).map((c, i) => `${i + 1}. ${c}`).join('\n')}`,
|
|
156
|
-
ctxDir, projectDir, agentsDir,
|
|
157
|
-
streaming, timeout,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
if (pipeResult.failed) {
|
|
161
|
-
appendLog(logPath, `pipeline failed at ${pipeResult.failed}\n`);
|
|
162
|
-
if (iteration === maxIterations) {
|
|
163
|
-
return { success: false, iterations: iteration, reason: `Pipeline failed: ${pipeResult.error}` };
|
|
164
|
-
}
|
|
165
|
-
continue; // Retry
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
appendLog(logPath, `pipeline error: ${err.message}\n`);
|
|
169
|
-
if (iteration === maxIterations) {
|
|
170
|
-
return { success: false, iterations: iteration, reason: err.message };
|
|
171
|
-
}
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Run review gate (if enabled)
|
|
176
|
-
if (isReviewGateEnabled(config)) {
|
|
177
|
-
try {
|
|
178
|
-
const reviewResult = await runReviewGate({
|
|
179
|
-
ctxDir, projectDir, agentsDir, streaming, timeout, config,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
if (reviewResult.escalated) {
|
|
183
|
-
return { success: false, iterations: iteration, reason: 'Review loop exceeded — human review required.' };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!reviewResult.passed) {
|
|
187
|
-
appendLog(logPath, `review failed (cycle ${reviewResult.cycle})\n`);
|
|
188
|
-
if (iteration === maxIterations) {
|
|
189
|
-
return { success: false, iterations: iteration, reason: `Review failed: ${reviewResult.feedback}` };
|
|
190
|
-
}
|
|
191
|
-
continue; // Retry with feedback
|
|
192
|
-
}
|
|
193
|
-
} catch (err) {
|
|
194
|
-
appendLog(logPath, `review error: ${err.message}\n`);
|
|
195
|
-
// Review errors don't block — continue
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// If we get here, story passed
|
|
200
|
-
appendLog(logPath, `passed\n`);
|
|
201
|
-
|
|
202
|
-
// Commit
|
|
203
|
-
commitTask({
|
|
204
|
-
projectDir, ctxDir,
|
|
205
|
-
agentName: 'auto',
|
|
206
|
-
taskId: story.id,
|
|
207
|
-
taskTitle: story.title,
|
|
208
|
-
criteriaIds: story.acceptanceCriteria || [],
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
// Mark story as passed in PRD
|
|
212
|
-
markStoryPassed(ctxDir, story.id);
|
|
213
|
-
|
|
214
|
-
return { success: true, iterations: iteration, reason: null };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return { success: false, iterations: maxIterations, reason: `Max iterations (${maxIterations}) exceeded.` };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function resolveStories(ctxDir, storyId, retryFailed) {
|
|
221
|
-
if (storyId) {
|
|
222
|
-
// Single story mode
|
|
223
|
-
try {
|
|
224
|
-
const prd = JSON.parse(fs.readFileSync(path.join(ctxDir, 'PRD.json'), 'utf-8'));
|
|
225
|
-
const story = (prd.stories || []).find(s => s.id === storyId);
|
|
226
|
-
return story ? [story] : [];
|
|
227
|
-
} catch {
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const pending = listPendingStories(ctxDir);
|
|
233
|
-
|
|
234
|
-
if (retryFailed) {
|
|
235
|
-
// Read auto log for failed stories
|
|
236
|
-
const state = readState(ctxDir);
|
|
237
|
-
const failedIds = new Set((state?.autoFailedStories || []).map(f => f.id || f));
|
|
238
|
-
return pending.filter(s => failedIds.has(s.id));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// All P1 stories first, then P2, etc.
|
|
242
|
-
return pending.sort((a, b) => (a.priority || 99) - (b.priority || 99));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function markStoryPassed(ctxDir, storyId) {
|
|
246
|
-
try {
|
|
247
|
-
const prdPath = path.join(ctxDir, 'PRD.json');
|
|
248
|
-
const prd = JSON.parse(fs.readFileSync(prdPath, 'utf-8'));
|
|
249
|
-
const story = (prd.stories || []).find(s => s.id === storyId);
|
|
250
|
-
if (story) {
|
|
251
|
-
story.passes = true;
|
|
252
|
-
story.verifiedAt = new Date().toISOString();
|
|
253
|
-
prd.metadata = prd.metadata || {};
|
|
254
|
-
prd.metadata.passedStories = (prd.metadata.passedStories || 0) + 1;
|
|
255
|
-
fs.writeFileSync(prdPath, JSON.stringify(prd, null, 2) + '\n');
|
|
256
|
-
}
|
|
257
|
-
} catch {}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function shouldStop(ctxDir) {
|
|
261
|
-
return fs.existsSync(path.join(ctxDir, STOP_FILE));
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function cleanupStopFile(ctxDir) {
|
|
265
|
-
const stopPath = path.join(ctxDir, STOP_FILE);
|
|
266
|
-
try { fs.unlinkSync(stopPath); } catch {}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function appendLog(logPath, text) {
|
|
270
|
-
fs.appendFileSync(logPath, text);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function buildSummary(completed, failed, skipped, totalTime) {
|
|
274
|
-
const mins = Math.round(totalTime / 1000 / 60);
|
|
275
|
-
return [
|
|
276
|
-
`## Summary`,
|
|
277
|
-
`- Completed: ${completed.length}`,
|
|
278
|
-
`- Failed: ${failed.length}`,
|
|
279
|
-
`- Skipped: ${skipped.length}`,
|
|
280
|
-
`- Total time: ${mins} minutes`,
|
|
281
|
-
`- Finished: ${new Date().toISOString()}`,
|
|
282
|
-
].join('\n') + '\n';
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function emit(fn, event) {
|
|
286
|
-
if (fn) fn(event);
|
|
287
|
-
}
|
package/src/commits.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { readState, writeState, recordCompletedTask } from './state.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Create an atomic git commit for a completed task.
|
|
6
|
-
*
|
|
7
|
-
* Checks that there are staged or unstaged changes before committing.
|
|
8
|
-
* Commit message format: ctx(<agent>): <task-title>
|
|
9
|
-
* Body includes acceptance criteria satisfied.
|
|
10
|
-
*
|
|
11
|
-
* Returns { committed: boolean, hash: string|null, error: string|null }
|
|
12
|
-
*/
|
|
13
|
-
export function commitTask({ projectDir, ctxDir, agentName, taskId, taskTitle, criteriaIds = [] }) {
|
|
14
|
-
try {
|
|
15
|
-
// Check for changes
|
|
16
|
-
const status = execSync('git status --porcelain', {
|
|
17
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 5000,
|
|
18
|
-
}).trim();
|
|
19
|
-
|
|
20
|
-
if (!status) {
|
|
21
|
-
return { committed: false, hash: null, error: 'No changes to commit.' };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Stage all changes (excluding .ctx/ state files to avoid noise)
|
|
25
|
-
execSync('git add -A -- . ":!.ctx/STATE.json" ":!.ctx/STATE.lock" ":!.ctx/HANDOFF.json"', {
|
|
26
|
-
cwd: projectDir, timeout: 5000,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Build commit message
|
|
30
|
-
const subject = `ctx(${agentName}): ${taskTitle}`;
|
|
31
|
-
const body = buildCommitBody(taskId, criteriaIds);
|
|
32
|
-
const message = `${subject}\n\n${body}`;
|
|
33
|
-
|
|
34
|
-
// Commit
|
|
35
|
-
execSync(`git commit -m ${shellEscape(message)}`, {
|
|
36
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 10000,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Get commit hash
|
|
40
|
-
const hash = execSync('git rev-parse --short HEAD', {
|
|
41
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 5000,
|
|
42
|
-
}).trim();
|
|
43
|
-
|
|
44
|
-
// Record in state
|
|
45
|
-
recordCompletedTask(ctxDir, taskId, taskTitle, criteriaIds);
|
|
46
|
-
|
|
47
|
-
// Log commit in agent history
|
|
48
|
-
const state = readState(ctxDir);
|
|
49
|
-
if (state) {
|
|
50
|
-
state.lastCommit = { hash, taskId, taskTitle, committedAt: new Date().toISOString() };
|
|
51
|
-
writeState(ctxDir, state);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { committed: true, hash, error: null };
|
|
55
|
-
} catch (err) {
|
|
56
|
-
return { committed: false, hash: null, error: err.message };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Show CTX commit log for the current story.
|
|
62
|
-
*/
|
|
63
|
-
export function getCtxCommitLog(projectDir, limit = 20) {
|
|
64
|
-
try {
|
|
65
|
-
const log = execSync(`git log --oneline --grep="^ctx(" -${limit} --no-color`, {
|
|
66
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 5000,
|
|
67
|
-
}).trim();
|
|
68
|
-
return log || 'No CTX commits found.';
|
|
69
|
-
} catch {
|
|
70
|
-
return 'No CTX commits found.';
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// --- internal ---
|
|
75
|
-
|
|
76
|
-
function buildCommitBody(taskId, criteriaIds) {
|
|
77
|
-
const lines = [];
|
|
78
|
-
if (taskId) lines.push(`Task: ${taskId}`);
|
|
79
|
-
if (criteriaIds.length > 0) {
|
|
80
|
-
lines.push('');
|
|
81
|
-
lines.push('Acceptance criteria satisfied:');
|
|
82
|
-
for (const id of criteriaIds) {
|
|
83
|
-
lines.push(` - ${id}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
lines.push('');
|
|
87
|
-
lines.push('Co-Authored-By: Claude <noreply@anthropic.com>');
|
|
88
|
-
return lines.join('\n');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function shellEscape(str) {
|
|
92
|
-
// Use $'...' syntax for strings with newlines
|
|
93
|
-
return "$'" + str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n') + "'";
|
|
94
|
-
}
|
package/src/context.js
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { execSync } from 'child_process';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Default context profiles per agent category.
|
|
7
|
-
* Each profile lists which context sources the agent receives.
|
|
8
|
-
*
|
|
9
|
-
* Sources:
|
|
10
|
-
* state — .ctx/STATE.json
|
|
11
|
-
* prd — .ctx/PRD.json (active story only)
|
|
12
|
-
* plan — .ctx/phases/<story>/PLAN.md
|
|
13
|
-
* repoMap — .ctx/REPO-MAP.md
|
|
14
|
-
* gitDiff — git diff (staged + unstaged)
|
|
15
|
-
* gitLog — recent commits
|
|
16
|
-
* fileTree — directory listing
|
|
17
|
-
* readme — README.md first 100 lines
|
|
18
|
-
*/
|
|
19
|
-
const DEFAULT_PROFILES = {
|
|
20
|
-
// Planning agents — need requirements and architecture, not code diffs
|
|
21
|
-
plan: ['state', 'prd', 'repoMap', 'readme'],
|
|
22
|
-
predict: ['state', 'prd', 'repoMap', 'fileTree'],
|
|
23
|
-
criteria: ['state', 'prd'],
|
|
24
|
-
discuss: ['state', 'prd', 'repoMap'],
|
|
25
|
-
parallelize: ['state', 'plan'],
|
|
26
|
-
|
|
27
|
-
// Execution agents — need the plan and relevant source files
|
|
28
|
-
execute: ['state', 'plan', 'repoMap'],
|
|
29
|
-
debug: ['state', 'gitDiff', 'gitLog', 'repoMap'],
|
|
30
|
-
|
|
31
|
-
// Review agents — need the diff, not the full tree
|
|
32
|
-
review: ['state', 'gitDiff', 'prd'],
|
|
33
|
-
audit: ['state', 'gitDiff'],
|
|
34
|
-
verify: ['state', 'prd', 'gitDiff'],
|
|
35
|
-
|
|
36
|
-
// Mapper agents — need filesystem overview
|
|
37
|
-
map: ['fileTree', 'readme'],
|
|
38
|
-
'arch-map': ['fileTree', 'readme'],
|
|
39
|
-
'tech-map': ['fileTree', 'readme'],
|
|
40
|
-
'quality-map': ['fileTree'],
|
|
41
|
-
'concerns-map': ['fileTree', 'gitLog'],
|
|
42
|
-
|
|
43
|
-
// Knowledge agents
|
|
44
|
-
research: ['state', 'prd', 'repoMap'],
|
|
45
|
-
learn: ['state', 'repoMap'],
|
|
46
|
-
design: ['state', 'prd', 'readme'],
|
|
47
|
-
|
|
48
|
-
// Coordination agents
|
|
49
|
-
handoff: ['state'],
|
|
50
|
-
team: ['state'],
|
|
51
|
-
qa: ['state', 'prd', 'readme'],
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Estimated tokens per source (conservative)
|
|
55
|
-
const SOURCE_TOKEN_ESTIMATES = {
|
|
56
|
-
state: 500,
|
|
57
|
-
prd: 1000,
|
|
58
|
-
plan: 1500,
|
|
59
|
-
repoMap: 2000,
|
|
60
|
-
gitDiff: 3000,
|
|
61
|
-
gitLog: 500,
|
|
62
|
-
fileTree: 1000,
|
|
63
|
-
readme: 800,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Load context manifest from file, or return defaults.
|
|
68
|
-
*/
|
|
69
|
-
export function loadContextManifest(ctxDir) {
|
|
70
|
-
const manifestPath = path.join(ctxDir, 'context-manifest.json');
|
|
71
|
-
try {
|
|
72
|
-
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
73
|
-
} catch {
|
|
74
|
-
return DEFAULT_PROFILES;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get the context profile for an agent command.
|
|
80
|
-
* Checks: agent frontmatter 'context-profile' → manifest → defaults.
|
|
81
|
-
*/
|
|
82
|
-
export function getContextProfile(agentCommand, manifest = DEFAULT_PROFILES) {
|
|
83
|
-
return manifest[agentCommand] || ['state', 'repoMap']; // fallback: minimal
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Build context string for an agent based on its profile.
|
|
88
|
-
* Returns { context: string, estimatedTokens: number, warnings: string[] }.
|
|
89
|
-
*/
|
|
90
|
-
export function buildContext(agentCommand, projectDir, ctxDir, options = {}) {
|
|
91
|
-
const manifest = loadContextManifest(ctxDir);
|
|
92
|
-
const sources = getContextProfile(agentCommand, manifest);
|
|
93
|
-
const tokenLimit = options.tokenLimit || 100_000;
|
|
94
|
-
|
|
95
|
-
const sections = [];
|
|
96
|
-
let estimatedTokens = 0;
|
|
97
|
-
const warnings = [];
|
|
98
|
-
|
|
99
|
-
for (const source of sources) {
|
|
100
|
-
const { content, tokens } = loadSource(source, projectDir, ctxDir);
|
|
101
|
-
if (content) {
|
|
102
|
-
sections.push(content);
|
|
103
|
-
estimatedTokens += tokens;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (estimatedTokens > tokenLimit * 0.8) {
|
|
108
|
-
warnings.push(`Context payload ~${estimatedTokens} tokens (${Math.round(estimatedTokens / tokenLimit * 100)}% of limit). Consider reducing context sources.`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
context: sections.join('\n\n---\n\n'),
|
|
113
|
-
estimatedTokens,
|
|
114
|
-
warnings,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Estimate token count from a string (chars / 4 approximation).
|
|
120
|
-
*/
|
|
121
|
-
export function estimateTokens(text) {
|
|
122
|
-
return Math.ceil((text || '').length / 4);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// --- Source loaders ---
|
|
126
|
-
|
|
127
|
-
function loadSource(source, projectDir, ctxDir) {
|
|
128
|
-
const loaders = {
|
|
129
|
-
state: () => safeRead(path.join(ctxDir, 'STATE.json'), '## Project State\n```json\n', '\n```'),
|
|
130
|
-
prd: () => loadPrdStory(ctxDir),
|
|
131
|
-
plan: () => loadCurrentPlan(ctxDir),
|
|
132
|
-
repoMap: () => safeRead(path.join(ctxDir, 'REPO-MAP.md'), '## Repository Map\n'),
|
|
133
|
-
gitDiff: () => loadGitDiff(projectDir),
|
|
134
|
-
gitLog: () => loadGitLog(projectDir),
|
|
135
|
-
fileTree: () => loadFileTree(projectDir),
|
|
136
|
-
readme: () => loadReadme(projectDir),
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const loader = loaders[source];
|
|
140
|
-
if (!loader) return { content: null, tokens: 0 };
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const content = loader();
|
|
144
|
-
return { content, tokens: estimateTokens(content) };
|
|
145
|
-
} catch {
|
|
146
|
-
return { content: null, tokens: 0 };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function safeRead(filePath, prefix = '', suffix = '') {
|
|
151
|
-
try {
|
|
152
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
153
|
-
return `${prefix}${content}${suffix}`;
|
|
154
|
-
} catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function loadPrdStory(ctxDir) {
|
|
160
|
-
try {
|
|
161
|
-
const prd = JSON.parse(fs.readFileSync(path.join(ctxDir, 'PRD.json'), 'utf-8'));
|
|
162
|
-
const current = prd.metadata?.currentStory;
|
|
163
|
-
if (!current) return `## PRD\nProject: ${prd.project?.name || 'unknown'}\nNo active story.`;
|
|
164
|
-
|
|
165
|
-
const story = (prd.stories || []).find(s => s.id === current);
|
|
166
|
-
if (!story) return null;
|
|
167
|
-
|
|
168
|
-
return [
|
|
169
|
-
`## Active Story: ${story.id} — ${story.title}`,
|
|
170
|
-
story.description,
|
|
171
|
-
'',
|
|
172
|
-
'### Acceptance Criteria',
|
|
173
|
-
...(story.acceptanceCriteria || []).map((c, i) => `${i + 1}. ${c}`),
|
|
174
|
-
].join('\n');
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function loadCurrentPlan(ctxDir) {
|
|
181
|
-
try {
|
|
182
|
-
const state = JSON.parse(fs.readFileSync(path.join(ctxDir, 'STATE.json'), 'utf-8'));
|
|
183
|
-
const storyId = state.activeStory;
|
|
184
|
-
if (!storyId) return null;
|
|
185
|
-
|
|
186
|
-
const planPath = path.join(ctxDir, 'phases', storyId, 'PLAN.md');
|
|
187
|
-
return safeRead(planPath, '## Execution Plan\n');
|
|
188
|
-
} catch {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function loadGitDiff(projectDir) {
|
|
194
|
-
try {
|
|
195
|
-
const diff = execSync('git diff HEAD --stat --no-color 2>/dev/null && echo "---" && git diff HEAD --no-color 2>/dev/null', {
|
|
196
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 1024 * 1024,
|
|
197
|
-
}).trim();
|
|
198
|
-
if (!diff || diff === '---') return null;
|
|
199
|
-
// Truncate if too large
|
|
200
|
-
const maxChars = 12000; // ~3000 tokens
|
|
201
|
-
const truncated = diff.length > maxChars ? diff.slice(0, maxChars) + '\n\n... (diff truncated)' : diff;
|
|
202
|
-
return `## Git Diff\n\`\`\`diff\n${truncated}\n\`\`\``;
|
|
203
|
-
} catch {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function loadGitLog(projectDir) {
|
|
209
|
-
try {
|
|
210
|
-
const log = execSync('git log --oneline -10 --no-color 2>/dev/null', {
|
|
211
|
-
cwd: projectDir, encoding: 'utf-8', timeout: 5000,
|
|
212
|
-
}).trim();
|
|
213
|
-
return log ? `## Recent Commits\n\`\`\`\n${log}\n\`\`\`` : null;
|
|
214
|
-
} catch {
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function loadFileTree(projectDir) {
|
|
220
|
-
try {
|
|
221
|
-
const tree = execSync(
|
|
222
|
-
'find . -type f -not -path "./.git/*" -not -path "./node_modules/*" -not -path "./.ctx/*" -not -path "./.next/*" -not -path "./dist/*" -not -path "./.cache/*" | head -200 | sort',
|
|
223
|
-
{ cwd: projectDir, encoding: 'utf-8', timeout: 5000, maxBuffer: 512 * 1024 }
|
|
224
|
-
).trim();
|
|
225
|
-
return tree ? `## File Tree\n\`\`\`\n${tree}\n\`\`\`` : null;
|
|
226
|
-
} catch {
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function loadReadme(projectDir) {
|
|
232
|
-
try {
|
|
233
|
-
const readme = fs.readFileSync(path.join(projectDir, 'README.md'), 'utf-8');
|
|
234
|
-
const lines = readme.split('\n').slice(0, 100);
|
|
235
|
-
return `## README (first 100 lines)\n${lines.join('\n')}`;
|
|
236
|
-
} catch {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export { DEFAULT_PROFILES };
|