create-claude-workspace 1.1.39 → 1.1.41

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.
@@ -126,7 +126,39 @@ If no remote, skip this step.
126
126
  - `git log --oneline -10`
127
127
  - Scan current file structure
128
128
 
129
- ### 8. Retroactive git integration sync
129
+ ### 8. Detect PLAN.md changes
130
+ If `PLAN.md` exists in the project root, check whether it changed since the last session. This runs AFTER git sync (step 6) to avoid commit conflicts with remote changes.
131
+
132
+ 1. Read PLAN.md content
133
+ 2. Read MEMORY.md `PLAN_MD_Hash` value (if present)
134
+ 3. Compute a hash of PLAN.md content: take first 8 chars of hex SHA-256 (`node -e "const c=require('crypto');process.stdout.write(c.createHash('sha256').update(require('fs').readFileSync('PLAN.md','utf-8')).digest('hex').slice(0,8))"`)
135
+ 4. If hash matches → skip (no changes). If `PLAN_MD_Hash` is absent (first run) → treat as "changed" to seed the state.
136
+ 5. If changed — read PLAN.md and diff against current project state. Delegate updates based on WHICH sections changed:
137
+
138
+ | Changed section | Action |
139
+ |---|---|
140
+ | **Product Vision** (Target users, Core problem, MVP features, Non-goals) | Delegate to `product-owner`: "PLAN.md Product Vision was updated. Read the new PLAN.md and current PRODUCT.md. Update PRODUCT.md to reflect the new vision. Output what changed." |
141
+ | **Technical Decisions** (Framework, Backend, Database, Deployment, CI/CD, Testing) | Delegate to `technical-planner`: "PLAN.md Technical Decisions were updated. Read new PLAN.md, current CLAUDE.md, and TODO.md. Update TODO.md tasks if the tech stack changed (e.g., framework switch may invalidate existing tasks). Update CLAUDE.md Tech Stack section to match." If only Deployment/CI/CD changed, also delegate to `deployment-engineer`: "PLAN.md deployment config changed. Update CI/CD pipeline config to match new settings." |
142
+ | **Workflow** (Mode, Branch strategy, Auto-push, Auto-merge, Review required) | Update CLAUDE.md `Workflow:` field directly. If mode changed from solo→team or team→solo, log in MEMORY.md Notes. |
143
+ | **Constraints** (Max lines per file, Coverage threshold, Forbidden patterns) | Update CLAUDE.md constraints section directly (Code Quality & Linting, coverage thresholds). |
144
+ | **Project Info** (Name, Description, Framework, Package manager, Monorepo) | Delegate to `technical-planner`: "PLAN.md Project Info changed. Update CLAUDE.md header and Tech Stack to match." |
145
+ | **Access & Credentials** | Validate new credentials (run auth checks). Log status in MEMORY.md Session Config. Do NOT store credential values. |
146
+ | **Initial State / Runtime** | Read values but do NOT delegate — these are consumed by `autonomous.mjs`, not agents. |
147
+
148
+ 6. After all delegations complete, update MEMORY.md: set `PLAN_MD_Hash: [new-hash]`
149
+ 7. If any files were modified (CLAUDE.md, PRODUCT.md, TODO.md), commit: `git add CLAUDE.md PRODUCT.md TODO.md MEMORY.md && git commit -m "chore: propagate PLAN.md changes to project files"`
150
+ 8. Push if remote exists: `git push origin HEAD`
151
+ 9. Skip this entire step if PLAN.md does not exist.
152
+
153
+ **How to detect which sections changed** (without storing the old file):
154
+ - Compare PLAN.md values against current project files:
155
+ - `Product Vision` → compare MVP features list against PRODUCT.md features
156
+ - `Technical Decisions` → compare framework/backend/database against CLAUDE.md Tech Stack
157
+ - `Workflow` → compare mode against CLAUDE.md `Workflow:` field
158
+ - `Constraints` → compare coverage threshold against CLAUDE.md code quality section
159
+ - If a PLAN.md value differs from the corresponding project file value, that section "changed"
160
+
161
+ ### 9. Retroactive git integration sync
130
162
  - **Trigger condition**: remote exists (`git remote -v | grep origin`) AND TODO.md exists with tasks BUT no `<!-- #N -->` markers found in TODO.md AND CLI is available (`gh --version` or `glab --version`) AND auth is valid (`gh auth status` or `glab auth status`)
131
163
  - This means work was done locally without git integration (token was missing or platform wasn't set up), and now it's available
132
164
  - Delegate to `devops-integrator` agent:
@@ -142,13 +174,13 @@ If no remote, skip this step.
142
174
  - Log in MEMORY.md Notes: "Retroactive git sync completed — [N] issues created, [M] marked as done"
143
175
  - Skip if any trigger condition is not met (no remote, no TODO.md, markers already present, CLI missing, auth invalid)
144
176
 
145
- ### 9. Ingest external issues (if git integration active)
177
+ ### 10. Ingest external issues (if git integration active)
146
178
  - **Only run at phase transitions** — not every invocation
147
179
  - Delegate to `devops-integrator` agent: "Run external issue intake — check for new issues on GitHub/GitLab that are not yet in TODO.md. Ingest bugs and feature requests."
148
180
  - If new bugs are found with `critical` or `priority::high` label, they become the next task
149
181
  - Skip if no git integration or platform is unreachable
150
182
 
151
- ### 10. One task per invocation
183
+ ### 11. One task per invocation
152
184
  - Complete exactly ONE task per invocation, then end the session cleanly.
153
185
  - The external loop (`autonomous.mjs` or ralph-loop) handles iteration control — you do NOT need to manage capacity, iteration counters, or session bounds.
154
186
  - After committing the task (STEP 11) and post-merge (STEP 12), EXIT. Do NOT update MEMORY.md after merge — it was already committed in STEP 11 and arrived on main via merge. Do NOT ask whether to continue, do NOT wait for confirmation, do NOT start another task. Do NOT create any commits after merge.
@@ -333,7 +333,9 @@ async function main() {
333
333
  const task = getCurrentTask(memory);
334
334
  log.info(`Iteration ${i}/${opts.maxIterations} | Phase: ${phase} | Task: ${task}`);
335
335
  // Run Claude
336
+ log.debug(`Spawning claude process...`);
336
337
  const output = await runClaude(opts, log, { resumeSessionId, resumePrompt });
338
+ log.debug(`Claude process exited (code=${output.code}, category=${output.errorCategory}, duration=${formatDuration(output.durationMs)})`);
337
339
  resumeSessionId = null;
338
340
  resumePrompt = null;
339
341
  if (stopping)
@@ -441,8 +443,10 @@ async function main() {
441
443
  log.info(`Iteration ${i} done (no structured result, exit ${output.code}).`);
442
444
  checkpoint.lastTaskSeen = task;
443
445
  writeCheckpoint(opts.projectDir, checkpoint, log);
444
- if (i < opts.maxIterations)
446
+ if (i < opts.maxIterations) {
447
+ log.info(`Next iteration in ${formatDuration(opts.delay)}...`);
445
448
  await sleep(opts.delay, stoppingRef);
449
+ }
446
450
  continue;
447
451
  }
448
452
  log.info(`Status: ${result.status} | Action: ${result.action}`);
@@ -496,6 +500,7 @@ async function main() {
496
500
  }
497
501
  writeCheckpoint(opts.projectDir, checkpoint, log);
498
502
  if (i < opts.maxIterations && !stopping && result.status !== 'needs_input') {
503
+ log.info(`Next iteration in ${formatDuration(opts.delay)}...`);
499
504
  await sleep(opts.delay, stoppingRef);
500
505
  }
501
506
  }
@@ -19,6 +19,9 @@ const TC = {
19
19
  dim: '\x1b[2m', reset: '\x1b[0m',
20
20
  cyan: '\x1b[36m', yellow: '\x1b[33m', green: '\x1b[32m', red: '\x1b[31m',
21
21
  };
22
+ function ts() {
23
+ return `[${new Date().toLocaleTimeString('en-GB', { hour12: false })}] `;
24
+ }
22
25
  const MAX_BUFFER = 1024 * 1024; // 1MB stdout buffer cap
23
26
  const MAX_STDERR = 64 * 1024; // 64KB stderr cap
24
27
  // ─── Stream event formatting ───
@@ -30,11 +33,11 @@ export function resetStreamState() {
30
33
  const SILENT_EVENTS = new Set(['system', 'ping', 'result']);
31
34
  function formatRateLimit(event) {
32
35
  const resets = event.rate_limit_info?.resets_at ?? event.resets_at ?? '';
33
- return `${TC.yellow}⏳ Rate limited${resets ? ` (resets: ${resets})` : ''}${TC.reset}\n`;
36
+ return `${ts()}${TC.yellow}⏳ Rate limited${resets ? ` (resets: ${resets})` : ''}${TC.reset}\n`;
34
37
  }
35
38
  function formatError(event) {
36
39
  const msg = event.error?.message ?? (typeof event.message === 'string' ? event.message : '') ?? JSON.stringify(event);
37
- return `${TC.red}✗ Error: ${msg}${TC.reset}\n`;
40
+ return `${ts()}${TC.red}✗ Error: ${msg}${TC.reset}\n`;
38
41
  }
39
42
  function getContentBlocks(event) {
40
43
  const msg = event.message;
@@ -46,7 +49,7 @@ function formatToolUse(block) {
46
49
  const input = block.input ?? {};
47
50
  const detail = input.command ?? input.file_path ?? input.pattern ?? input.query ?? '';
48
51
  const short = detail.slice(0, 120);
49
- return `${TC.cyan}▶ ${block.name ?? 'tool'}${TC.reset}${short ? ` ${TC.dim}${short}${TC.reset}` : ''}`;
52
+ return `${ts()}${TC.cyan}▶ ${block.name ?? 'tool'}${TC.reset}${short ? ` ${TC.dim}${short}${TC.reset}` : ''}`;
50
53
  }
51
54
  function formatTextBlock(block) {
52
55
  if (!block.text)
@@ -58,14 +61,15 @@ function formatTextBlock(block) {
58
61
  if (!text)
59
62
  return null;
60
63
  _lastAssistantText = block.text;
61
- return text;
64
+ // Timestamp on first line only (multi-line text shouldn't get per-line stamps)
65
+ return `${ts()}${text}`;
62
66
  }
63
67
  function formatThinking(block) {
64
68
  if (!block.thinking)
65
69
  return null;
66
70
  const preview = block.thinking.slice(0, 200).replace(/\n/g, ' ');
67
71
  const ellipsis = preview.length < block.thinking.length ? '...' : '';
68
- return `${TC.dim}💭 ${preview}${ellipsis}${TC.reset}`;
72
+ return `${ts()}${TC.dim}💭 ${preview}${ellipsis}${TC.reset}`;
69
73
  }
70
74
  const BLOCK_FORMATTERS = {
71
75
  tool_use: formatToolUse,
@@ -96,7 +100,7 @@ function formatUser(event) {
96
100
  const icon = block.is_error ? `${TC.red}✗` : `${TC.green}✓`;
97
101
  const text = typeof block.content === 'string' ? block.content : '';
98
102
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
99
- return `${icon} ${TC.dim}${preview}${TC.reset}\n`;
103
+ return `${ts()}${icon} ${TC.dim}${preview}${TC.reset}\n`;
100
104
  }
101
105
  return null;
102
106
  }
@@ -214,6 +218,10 @@ export function runClaude(opts, log, runOpts = {}) {
214
218
  const pid = child.pid;
215
219
  currentChild = { kill: () => { if (pid)
216
220
  killProcessTree(pid, isWin); } };
221
+ // Prevent uncaught exception from stdin write errors (EPIPE on Windows)
222
+ if (child.stdin) {
223
+ child.stdin.on('error', (err) => { log.debug(`stdin error (expected if child exited): ${err.message}`); });
224
+ }
217
225
  // ─── State ───
218
226
  let stderr = '';
219
227
  let sessionId = null;
@@ -363,6 +371,18 @@ export function runClaude(opts, log, runOpts = {}) {
363
371
  // ─── Close ───
364
372
  child.on('close', (code, signal) => {
365
373
  cleanup();
374
+ // Windows: kill orphan child processes that may hold Claude locks.
375
+ // Only kill if the process tree is still alive (avoid PID reuse killing unrelated processes).
376
+ if (isWin && pid && !killed) {
377
+ try {
378
+ const taskInfo = execSync(`tasklist /FI "PID eq ${pid}" /NH`, { encoding: 'utf-8', stdio: 'pipe', timeout: 3000 });
379
+ // tasklist always exits 0; parse stdout — if PID is dead it prints "INFO: No tasks..."
380
+ if (taskInfo.includes(String(pid))) {
381
+ execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore', timeout: 5000 });
382
+ }
383
+ }
384
+ catch { /* already dead — expected */ }
385
+ }
366
386
  // Flush remaining buffer
367
387
  if (buffer.trim()) {
368
388
  const event = parseStreamEvent(buffer.replace(/\r/g, ''));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.39",
3
+ "version": "1.1.41",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {