ctx-cc 4.1.0 → 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 CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  [![npm version](https://img.shields.io/npm/v/ctx-cc.svg?style=flat-square)](https://www.npmjs.com/package/ctx-cc)
15
15
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
16
- [![Tests](https://img.shields.io/badge/tests-264%20passing-brightgreen.svg?style=flat-square)](#testing)
16
+ [![Tests](https://img.shields.io/badge/tests-149%20passing-brightgreen.svg?style=flat-square)](#testing)
17
17
  [![Zero deps](https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square)](#)
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 # 264 tests, node:test runner
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/ 17 source modules (.js)
508
- ├── test/ 19 test files (.test.js)
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
- # 264 tests, 0 failures, ~2s
521
+ # 149 tests, 0 failures, ~1s
522
522
  ```
523
523
 
524
524
  **Coverage:**
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: ctx:cross-review
3
+ description: On-demand cross-model code review via OpenAI Codex. Works in any project.
4
+ argument-hint: [commit-range] [--focus=area]
5
+ ---
6
+
7
+ <objective>
8
+ Dispatch the current diff to OpenAI Codex (GPT-5.x) via MCP for adversarial cross-model review. Catches bugs Claude's same-model review tends to miss — different training data, different blind spots.
9
+
10
+ Wraps the `ctx-codex-reviewer` agent with the same skip logic and threadId carryforward used by Stage 3 of the CTX review gate, but invocable on demand from any Claude Code project (not just CTX-managed ones).
11
+ </objective>
12
+
13
+ <usage>
14
+ ```
15
+ /ctx:cross-review # review staged + unstaged diff
16
+ /ctx:cross-review HEAD~3..HEAD # review last 3 commits
17
+ /ctx:cross-review --focus=security # review with focus area hint
18
+ /ctx:cross-review HEAD~1 --focus=concurrency
19
+ ```
20
+
21
+ Argument parsing (lenient — any order):
22
+ - A git revision range (e.g. `HEAD~3..HEAD`, `main..HEAD`, `<sha1>..<sha2>`) selects what to review. If absent, defaults to `git diff` (working tree).
23
+ - `--focus=<area>` hints Codex to weight specific concerns (security, perf, concurrency, error-handling, contract).
24
+ - Bare positional after a range is treated as commentary appended to the prompt.
25
+ </usage>
26
+
27
+ <prerequisites>
28
+ This command is a no-op without the Codex MCP. If `mcp__codex__codex` is not registered, the agent will return `VERDICT: SKIP` and tell the user how to set it up:
29
+
30
+ ```bash
31
+ # One-time setup:
32
+ codex login # ChatGPT Plus auth
33
+ claude mcp add codex -- codex mcp-server # register MCP
34
+ ```
35
+
36
+ Do NOT block the command on missing MCP — surface the SKIP reason and suggest the install commands.
37
+ </prerequisites>
38
+
39
+ <workflow>
40
+
41
+ ## Step 1: Resolve scope
42
+
43
+ Parse `$ARGUMENTS`:
44
+ - Extract any token matching `<rev>..<rev>` or `<sha>~N` as the range.
45
+ - Extract `--focus=<area>` as a hint string.
46
+ - Treat remaining text as freeform commentary.
47
+
48
+ If no range, default: `git diff` (staged + unstaged).
49
+ If range given, the agent will use `git diff <range>`.
50
+
51
+ ## Step 2: Detect CTX context (optional)
52
+
53
+ ```bash
54
+ [ -f .ctx/STATE.json ] && jq -r '.activeStory // "none"' .ctx/STATE.json
55
+ ```
56
+
57
+ If a CTX story is active, include it in the prompt so the agent can pull acceptance criteria from `.ctx/PRD.json`. If not, skip — review still works against raw diff.
58
+
59
+ ## Step 3: Spawn the cross-reviewer agent
60
+
61
+ ```
62
+ Task:
63
+ subagent_type: "ctx-codex-reviewer"
64
+ prompt: |
65
+ On-demand cross-model review (not part of an automated review gate).
66
+
67
+ Scope: {{range or "working tree (staged + unstaged)"}}
68
+ Focus: {{--focus value or "general — bugs, security, contracts"}}
69
+ Story: {{.activeStory or "n/a — not a CTX project"}}
70
+ Notes: {{freeform commentary or empty}}
71
+
72
+ Run your standard playbook:
73
+ 1. Gather the diff for the scope above.
74
+ 2. Apply skip short-circuits (docs-only, test-only, <20 LOC).
75
+ 3. Dispatch via mcp__codex__codex.
76
+ 4. Parse verdict.
77
+
78
+ Output format (final line MUST be one of):
79
+ VERDICT: PASS
80
+ VERDICT: FAIL
81
+ VERDICT: SKIP
82
+
83
+ Append `THREAD: <id>` if a Codex thread was opened.
84
+ description: "Cross-model review"
85
+ ```
86
+
87
+ ## Step 4: Render the verdict to the user
88
+
89
+ After the agent returns:
90
+
91
+ ```
92
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93
+ CTX ► CROSS-REVIEW
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
+ ```
96
+
97
+ For each verdict:
98
+
99
+ - `PASS`: green checkmark, "Codex found no issues in {{scope}}."
100
+ - `FAIL`: red X, list the issues by `file:line — description`. Group by severity if Codex provided it.
101
+ - `SKIP`: muted ○, surface the skip reason verbatim. If reason is "MCP unavailable" or "auth expired", include the install commands from `<prerequisites>`.
102
+
103
+ If the agent emitted `THREAD: <id>`, mention it for follow-ups:
104
+ ```
105
+ Follow up with: ask Codex to expand on issue 2 in thread <id>
106
+ ```
107
+
108
+ ## Step 5: Optionally persist (CTX projects only)
109
+
110
+ If `.ctx/` exists, write the result alongside Stage 3 reviews:
111
+
112
+ ```
113
+ .ctx/reviews/cross-review-{{ISO-timestamp}}.json
114
+ {
115
+ "command": "/ctx:cross-review",
116
+ "scope": "{{range or working-tree}}",
117
+ "focus": "{{focus or null}}",
118
+ "verdict": "PASS|FAIL|SKIP",
119
+ "issues": [...],
120
+ "threadId": "..."
121
+ }
122
+ ```
123
+
124
+ If not in a CTX project, just print and exit — no file output.
125
+
126
+ </workflow>
127
+
128
+ <guardrails>
129
+ - Never fail on infra problems. Codex MCP missing, auth expired, or rate-limited → SKIP with actionable message, not error.
130
+ - Never call Codex on docs-only, test-only, or sub-20-LOC diffs (the agent enforces this; do not override).
131
+ - Rate-limit aware: the user's ChatGPT Plus quota is shared with everything else they use Codex for. One invocation = one message against the 5h window. Do not retry on transient failures.
132
+ - Read-only sandbox: the agent uses `sandbox: read-only` when calling Codex. Do not pass arguments that would change this.
133
+ </guardrails>
134
+
135
+ <comparison>
136
+ | When to use what | Tool |
137
+ |---|---|
138
+ | On-demand cross-review of current changes (any project) | `/ctx:cross-review` (this command) |
139
+ | Story-scoped review inside CTX gate (auto, every story) | Stage 3 of `runReviewGate` (no command — fires on its own) |
140
+ | Direct same-model review without Codex | `/ctx:verify` or `/ctx:quick` |
141
+ | Just want to send a single prompt to Codex (not review) | `/codex` (if installed) |
142
+ </comparison>
package/commands/help.md CHANGED
@@ -115,6 +115,7 @@ Or use commands directly:
115
115
  | `/ctx plan [goal]` | Force research + planning |
116
116
  | `/ctx verify` | Force three-level verification |
117
117
  | `/ctx quick "task"` | Quick task bypass |
118
+ | `/ctx cross-review [range] [--focus=area]` | On-demand Codex cross-model review (any project) |
118
119
 
119
120
  ### Debug
120
121
  | Command | Purpose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctx-cc",
3
- "version": "4.1.0",
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.0.0",
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
- }