create-claude-workspace 1.1.50 → 1.1.52
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/dist/scripts/autonomous.mjs +2 -1
- package/dist/scripts/lib/claude-runner.mjs +1 -1
- package/dist/scripts/lib/claude-runner.spec.js +109 -1
- package/dist/template/.claude/agents/deployment-engineer.md +1 -1
- package/dist/template/.claude/agents/orchestrator.md +19 -11
- package/dist/template/.claude/agents/project-initializer.md +1 -0
- package/dist/template/.claude/agents/test-engineer.md +1 -1
- package/dist/template/.claude/templates/PLAN.md +1 -1
- package/dist/template/.claude/templates/claude-md.md +1 -1
- package/package.json +1 -1
|
@@ -216,9 +216,10 @@ function setupSignals(opts, log, checkpoint) {
|
|
|
216
216
|
finalCleanup(opts, checkpoint, log);
|
|
217
217
|
process.exit(1);
|
|
218
218
|
}
|
|
219
|
-
log.info(`${label}.
|
|
219
|
+
log.info(`${label}. Killing child process and stopping (Ctrl+C again to force exit)...`);
|
|
220
220
|
stopping = true;
|
|
221
221
|
stoppingRef.value = true;
|
|
222
|
+
currentChild?.kill();
|
|
222
223
|
};
|
|
223
224
|
process.on('SIGINT', () => gracefulStop('SIGINT'));
|
|
224
225
|
process.on('SIGTERM', () => gracefulStop('SIGTERM'));
|
|
@@ -144,7 +144,7 @@ function parseStreamEvent(json) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
// ─── Process tree kill (cross-platform) ───
|
|
147
|
-
function killProcessTree(pid, isWin) {
|
|
147
|
+
export function killProcessTree(pid, isWin) {
|
|
148
148
|
if (isWin) {
|
|
149
149
|
try {
|
|
150
150
|
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { resetStreamState, validateResult, formatStreamEvent } from './claude-runner.mjs';
|
|
2
|
+
import { resetStreamState, validateResult, formatStreamEvent, killProcessTree } from './claude-runner.mjs';
|
|
3
|
+
import { spawn, execSync } from 'node:child_process';
|
|
3
4
|
describe('validateResult', () => {
|
|
4
5
|
it('returns valid result for complete object', () => {
|
|
5
6
|
const input = { status: 'completed', action: 'commit', message: 'Done task #1' };
|
|
@@ -236,3 +237,110 @@ describe('resetStreamState', () => {
|
|
|
236
237
|
resetStreamState(); // should not throw
|
|
237
238
|
});
|
|
238
239
|
});
|
|
240
|
+
// ─── killProcessTree integration tests ───
|
|
241
|
+
// Verifies that Ctrl+C (killProcessTree) actually kills child processes.
|
|
242
|
+
const isWin = process.platform === 'win32';
|
|
243
|
+
function isProcessAlive(pid) {
|
|
244
|
+
try {
|
|
245
|
+
process.kill(pid, 0); // signal 0 = existence check
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function spawnLongRunning() {
|
|
253
|
+
const script = 'setTimeout(()=>{},60000)';
|
|
254
|
+
if (isWin) {
|
|
255
|
+
return spawn(process.env.ComSpec ?? 'cmd.exe', ['/c', 'node', '-e', script], {
|
|
256
|
+
stdio: 'ignore',
|
|
257
|
+
detached: true,
|
|
258
|
+
windowsHide: true,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return spawn('node', ['-e', script], {
|
|
262
|
+
stdio: 'ignore',
|
|
263
|
+
detached: true,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/** Spawn a nested process tree: parent node → child node (simulates cmd.exe → claude) */
|
|
267
|
+
function spawnNestedTree() {
|
|
268
|
+
// Parent spawns a child that also sleeps — simulates cmd.exe → claude process tree
|
|
269
|
+
const innerScript = `
|
|
270
|
+
const { spawn } = require('child_process');
|
|
271
|
+
const child = spawn('node', ['-e', 'setTimeout(()=>{},60000)'], { stdio: 'ignore' });
|
|
272
|
+
child.unref();
|
|
273
|
+
setTimeout(()=>{},60000);
|
|
274
|
+
`.replace(/\n/g, ' ');
|
|
275
|
+
if (isWin) {
|
|
276
|
+
return spawn(process.env.ComSpec ?? 'cmd.exe', ['/c', 'node', '-e', innerScript], {
|
|
277
|
+
stdio: 'ignore',
|
|
278
|
+
detached: true,
|
|
279
|
+
windowsHide: true,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return spawn('node', ['-e', innerScript], {
|
|
283
|
+
stdio: 'ignore',
|
|
284
|
+
detached: true,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
describe('killProcessTree', () => {
|
|
288
|
+
it('kills a simple child process', async () => {
|
|
289
|
+
const child = spawnLongRunning();
|
|
290
|
+
const pid = child.pid;
|
|
291
|
+
expect(pid).toBeGreaterThan(0);
|
|
292
|
+
// Give process time to start
|
|
293
|
+
await new Promise(r => setTimeout(r, 500));
|
|
294
|
+
expect(isProcessAlive(pid)).toBe(true);
|
|
295
|
+
// Kill it
|
|
296
|
+
killProcessTree(pid, isWin);
|
|
297
|
+
// Wait for process to die
|
|
298
|
+
await new Promise(r => setTimeout(r, isWin ? 2000 : 1000));
|
|
299
|
+
expect(isProcessAlive(pid)).toBe(false);
|
|
300
|
+
}, 10_000);
|
|
301
|
+
it('kills a nested process tree (simulates cmd.exe → claude)', async () => {
|
|
302
|
+
const parent = spawnNestedTree();
|
|
303
|
+
const parentPid = parent.pid;
|
|
304
|
+
expect(parentPid).toBeGreaterThan(0);
|
|
305
|
+
// Give tree time to start
|
|
306
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
307
|
+
expect(isProcessAlive(parentPid)).toBe(true);
|
|
308
|
+
// Kill the tree
|
|
309
|
+
killProcessTree(parentPid, isWin);
|
|
310
|
+
// Wait for tree to die
|
|
311
|
+
await new Promise(r => setTimeout(r, isWin ? 3000 : 2000));
|
|
312
|
+
expect(isProcessAlive(parentPid)).toBe(false);
|
|
313
|
+
// On Windows, verify no orphan node.exe children from our tree
|
|
314
|
+
if (isWin) {
|
|
315
|
+
// tasklist can't reliably check by parent PID after parent is dead,
|
|
316
|
+
// but the /T flag in taskkill should have killed all children
|
|
317
|
+
try {
|
|
318
|
+
const taskInfo = execSync(`tasklist /FI "PID eq ${parentPid}" /NH`, {
|
|
319
|
+
encoding: 'utf-8', stdio: 'pipe', timeout: 3000,
|
|
320
|
+
});
|
|
321
|
+
expect(taskInfo).not.toContain(String(parentPid));
|
|
322
|
+
}
|
|
323
|
+
catch { /* process dead, expected */ }
|
|
324
|
+
}
|
|
325
|
+
}, 15_000);
|
|
326
|
+
it('does not throw when killing already-dead process', () => {
|
|
327
|
+
// PID 999999 is almost certainly not a real process
|
|
328
|
+
expect(() => killProcessTree(999999, isWin)).not.toThrow();
|
|
329
|
+
});
|
|
330
|
+
it('kills process fast enough for interactive use (< 3s)', async () => {
|
|
331
|
+
const child = spawnLongRunning();
|
|
332
|
+
const pid = child.pid;
|
|
333
|
+
await new Promise(r => setTimeout(r, 500));
|
|
334
|
+
const start = Date.now();
|
|
335
|
+
killProcessTree(pid, isWin);
|
|
336
|
+
// Poll until dead or timeout
|
|
337
|
+
let alive = true;
|
|
338
|
+
while (alive && Date.now() - start < 3000) {
|
|
339
|
+
await new Promise(r => setTimeout(r, 100));
|
|
340
|
+
alive = isProcessAlive(pid);
|
|
341
|
+
}
|
|
342
|
+
const elapsed = Date.now() - start;
|
|
343
|
+
expect(alive).toBe(false);
|
|
344
|
+
expect(elapsed).toBeLessThan(3000);
|
|
345
|
+
}, 10_000);
|
|
346
|
+
});
|
|
@@ -380,7 +380,7 @@ vercel rollback
|
|
|
380
380
|
**VPS (SSH):**
|
|
381
381
|
```bash
|
|
382
382
|
# If using PM2 + git:
|
|
383
|
-
ssh deploy@host "cd /var/www/myapp && git checkout <previous-release-tag-or-hash> &&
|
|
383
|
+
ssh deploy@host "cd /var/www/myapp && git checkout <previous-release-tag-or-hash> && [PKG] install && pm2 restart all"
|
|
384
384
|
# If using Docker:
|
|
385
385
|
ssh deploy@host "docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d"
|
|
386
386
|
# (redeploy previous image tag — update docker-compose.prod.yml or .env with previous tag first)
|
|
@@ -72,11 +72,20 @@ At the beginning of EVERY session (including every Ralph Loop iteration):
|
|
|
72
72
|
### 1. Read state
|
|
73
73
|
- Read MEMORY.md — find where you left off, what is done, what is next, what phase you're in
|
|
74
74
|
|
|
75
|
-
### 2. Resolve app names
|
|
76
|
-
- Read CLAUDE.md — find app name(s) from the Architecture/Tech Stack section
|
|
75
|
+
### 2. Resolve app names and package manager
|
|
76
|
+
- Read CLAUDE.md — find app name(s) and package manager from the Architecture/Tech Stack section
|
|
77
77
|
- Store app names for use in build/test/lint commands:
|
|
78
78
|
- `FRONTEND_APP` = the frontend app name (e.g., `my-app`)
|
|
79
79
|
- `BACKEND_APP` = the API app name (e.g., `api`) — may not exist
|
|
80
|
+
- **Resolve package manager** — use the FIRST match:
|
|
81
|
+
1. CLAUDE.md Tech Stack `[PKG] as package manager` field (authoritative if set)
|
|
82
|
+
2. Lockfile detection: `bun.lock` or `bun.lockb` → bun, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm
|
|
83
|
+
3. Default: npm
|
|
84
|
+
- Store as `PKG` in MEMORY.md Session Config (e.g., `PKG: bun`). **All commands in all steps MUST use this value** — never hardcode npm/pnpm/yarn/bun.
|
|
85
|
+
- Install: `{PKG} install` (bun: `bun install`, npm: `npm ci`, pnpm: `pnpm install --frozen-lockfile`, yarn: `yarn install --frozen-lockfile`)
|
|
86
|
+
- Run script: `{PKG} run [script]` (bun: `bun run`, npm: `npm run`)
|
|
87
|
+
- Add package: `{PKG} add [pkg]` (npm: `npm install [pkg]`)
|
|
88
|
+
- Execute binary: `bunx` / `npx` / `pnpm exec` / `yarn dlx`
|
|
80
89
|
- Also note: workflow mode from CLAUDE.md — `Workflow: solo` or `Workflow: team` (default: solo)
|
|
81
90
|
- Store these in MEMORY.md under a `## Session Config` section if not already present
|
|
82
91
|
- **If Session Config has "TBD" values** (new project before Phase 0): scan `apps/` directory. If apps now exist (Phase 0 completed), update the values. If still empty, keep "TBD" — Phase 0 scaffolding will create them.
|
|
@@ -236,14 +245,13 @@ To determine if a task is frontend, backend, or fullstack, use this heuristic:
|
|
|
236
245
|
- **Phase transition check**: If this is the FIRST task of a new phase (all previous phase tasks done):
|
|
237
246
|
- **CI/CD generation (Phase 0 → Phase 1 only):** If Phase 0 just completed AND CLAUDE.md has a `## Deployment` section (deployment interview was done during setup) AND no CI/CD config files exist yet (`.gitlab-ci.yml`, `.github/workflows/`): delegate to `deployment-engineer`: "Phase 0 (workspace scaffolding) is complete — the project now has package.json and source code. Read CLAUDE.md ## Deployment section for deployment config collected during setup. Generate CI/CD config files now. [If CLAUDE.md has Distribution: npm:] Also generate CI publish jobs — read MEMORY.md NPM_REGISTRY/NPM_CI_AUTH for registry config."
|
|
238
247
|
- **Dependency freshness check** (skip at Phase 0 → Phase 1 — deps were just installed during scaffolding):
|
|
239
|
-
1.
|
|
248
|
+
1. Use `PKG` from MEMORY.md Session Config (resolved in health check step 2).
|
|
240
249
|
2. Run outdated check — capture output, then validate. Note: `npm outdated` exits with code 1 when outdated packages exist (this is normal, not an error):
|
|
241
250
|
```bash
|
|
242
|
-
# npm
|
|
243
|
-
OUTDATED=$(
|
|
244
|
-
# yarn: yarn outdated --json 2>&1 || true
|
|
245
|
-
#
|
|
246
|
-
# bun: bun outdated 2>&1 || true
|
|
251
|
+
# npm: OUTDATED=$(npm outdated --json 2>&1) || true
|
|
252
|
+
# pnpm: OUTDATED=$(pnpm outdated --format json 2>&1) || true
|
|
253
|
+
# yarn: OUTDATED=$(yarn outdated --json 2>&1) || true
|
|
254
|
+
# bun: OUTDATED=$(bun outdated 2>&1) || true
|
|
247
255
|
```
|
|
248
256
|
If the output is not valid JSON (network failure, corrupt lockfile), log warning in MEMORY.md Notes and skip.
|
|
249
257
|
3. Skip if no outdated dependencies found (empty JSON object `{}`).
|
|
@@ -251,12 +259,12 @@ To determine if a task is frontend, backend, or fullstack, use this heuristic:
|
|
|
251
259
|
- **Patch** (e.g., `1.2.3` → `1.2.5`): safe, auto-upgrade
|
|
252
260
|
- **Minor** (e.g., `1.2.3` → `1.3.0`): likely safe, auto-upgrade
|
|
253
261
|
- **Major** (e.g., `1.2.3` → `2.0.0`): breaking changes — do NOT auto-upgrade
|
|
254
|
-
5. **Auto-upgrade patch + minor:** Run `
|
|
262
|
+
5. **Auto-upgrade patch + minor:** Run `{PKG} update` (e.g., `bun update`, `npm update`, `pnpm update`, `yarn upgrade`). If package.json pins exact versions or ranges exclude the available update, use `{PKG} add [pkg]@[wanted]` (npm: `npm install [pkg]@[wanted]`) for each outdated package where wanted ≤ latest within the semver range.
|
|
255
263
|
6. **Verify:** Run full workspace build, lint, and test from the project root on main (not scoped to a specific project — there is no active task or worktree at this point). If ANY fail:
|
|
256
264
|
- Revert files AND restore node_modules to match:
|
|
257
265
|
```bash
|
|
258
266
|
git restore package.json [lockfile]
|
|
259
|
-
|
|
267
|
+
{PKG} install # bun install / npm ci / pnpm install --frozen-lockfile / yarn install --frozen-lockfile
|
|
260
268
|
```
|
|
261
269
|
- Log failed upgrade in MEMORY.md Notes: "Dependency upgrade failed at phase transition — [error summary]. Skipped."
|
|
262
270
|
- Do NOT block phase transition — continue with current versions.
|
|
@@ -314,7 +322,7 @@ To determine if a task is frontend, backend, or fullstack, use this heuristic:
|
|
|
314
322
|
- Update MEMORY.md (on main): set `Current Step: 3 — IMPLEMENT`
|
|
315
323
|
- **All file operations in the worktree** (`Current Worktree` from MEMORY.md). Use absolute paths for Read/Edit/Write and `cd {worktree}` for Bash commands.
|
|
316
324
|
- **CLI commands and Nx Generators FIRST**: Always prioritize using CLI commands and `nx generate` over manual file creation. NEVER manually create `project.json`, `tsconfig.*`, or configure build/test/lint targets — generators and CLI tools handle this correctly. NEVER manually install or configure build tools (tsup, esbuild, rollup, webpack) — Nx generators handle build configuration. Internal libs don't need a build step; publishable libs get `--bundler` from the generator. This includes:
|
|
317
|
-
- **Workspace scaffolding** (Phase 0): `
|
|
325
|
+
- **Workspace scaffolding** (Phase 0): `create-nx-workspace` via the detected package manager runner (`bunx` / `npx` / `pnpm dlx` / `yarn dlx`) — architect decides preset and flags. If PKG is bun, use `bunx create-nx-workspace` and pass `--packageManager=bun`.
|
|
318
326
|
- **Libraries/components/apps**: `nx generate` — see CLAUDE.md "Nx Generators" section
|
|
319
327
|
- **Plugins**: `nx add` (e.g., `nx add @nx/playwright`, `nx add @nx/eslint`)
|
|
320
328
|
- **Code quality setup**: install via package manager, configure via CLI where possible
|
|
@@ -147,6 +147,7 @@ Create a **minimal** CLAUDE.md in the project root by filling what is KNOWN from
|
|
|
147
147
|
- Fill in: project name, description, workflow (solo/team)
|
|
148
148
|
- If user answered "yes" to npm publishing (question 6), add `- **Distribution**: npm ([registry], [public/restricted])` to Tech Stack section (details collected in Step 2b)
|
|
149
149
|
- Fill in Tech Stack ONLY for what is already detected in the codebase (existing package.json, nx.json, etc.)
|
|
150
|
+
- **Detect package manager** from lockfile: `bun.lock` or `bun.lockb` → bun, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm. If no lockfile, check `package.json` `packageManager` field. Fill `[PKG]` in the template with the detected value. This is critical — all agents use this value for install/run/add commands.
|
|
150
151
|
- For NEW projects (empty codebase): leave Tech Stack minimal — just project type and package manager if known. Architecture decisions will be made by architect agents in TODO.md Phase 0.
|
|
151
152
|
- **Remove all backend sections** if no backend is mentioned
|
|
152
153
|
- Remove placeholder comments and unresolved `[PLACEHOLDER]` values
|
|
@@ -179,7 +179,7 @@ Before writing E2E tests, detect existing config:
|
|
|
179
179
|
```bash
|
|
180
180
|
nx add @nx/playwright
|
|
181
181
|
```
|
|
182
|
-
If not using Nx, install Playwright directly: `npm init playwright@latest`
|
|
182
|
+
If not using Nx, install Playwright directly: `{PKG} init playwright@latest` (bun: `bun create playwright`, npm: `npm init playwright@latest`). Read CLAUDE.md Tech Stack for the project's package manager.
|
|
183
183
|
NEVER manually create `project.json` for E2E projects — the generator configures targets, config files, and directory structure correctly.
|
|
184
184
|
|
|
185
185
|
4. E2E tests live in `apps/[APP]-e2e/src/` (Nx) or `e2e/` (standalone) — NOT next to unit tests
|
|
@@ -34,7 +34,7 @@ When running in autonomous/unattended mode (via `autonomous.mjs` or non-interact
|
|
|
34
34
|
|
|
35
35
|
[Generate based on detected project type. Include only lines relevant to this project:]
|
|
36
36
|
- **Project type**: [Angular app / TS library monorepo / Fullstack app / Backend only]
|
|
37
|
-
- **Build**: Nx monorepo, [PKG] as package manager
|
|
37
|
+
- **Build**: Nx monorepo, [PKG] as package manager (CRITICAL: detect from lockfile — `bun.lock`/`bun.lockb` → bun, `pnpm-lock.yaml` → pnpm, `yarn.lock` → yarn, `package-lock.json` → npm)
|
|
38
38
|
- **Testing**: Vitest
|
|
39
39
|
- **Deployment**: Cloudflare (Pages + Workers) — default
|
|
40
40
|
- **Workflow**: solo / team (solo = direct merge to main, team = MR/PR review required)
|