create-claude-workspace 1.1.41 → 1.1.44
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/template/.claude/agents/orchestrator.md +31 -0
- package/dist/template/.claude/scripts/autonomous.mjs +23 -1
- package/dist/template/.claude/scripts/lib/claude-runner.mjs +9 -2
- package/dist/template/.claude/scripts/lib/errors.mjs +12 -2
- package/dist/template/.claude/scripts/lib/utils.mjs +29 -0
- package/package.json +1 -1
|
@@ -220,6 +220,37 @@ To determine if a task is frontend, backend, or fullstack, use this heuristic:
|
|
|
220
220
|
After planner updates TODO.md -> if git integration active, delegate to `devops-integrator` to sync new tasks as issues. Then restart STEP 1 with the first sub-task.
|
|
221
221
|
- **Phase transition check**: If this is the FIRST task of a new phase (all previous phase tasks done):
|
|
222
222
|
- **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."
|
|
223
|
+
- **Dependency freshness check** (skip at Phase 0 → Phase 1 — deps were just installed during scaffolding):
|
|
224
|
+
1. Detect package manager from lockfile (`package-lock.json` → npm, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, `bun.lock` → bun). Default to npm.
|
|
225
|
+
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):
|
|
226
|
+
```bash
|
|
227
|
+
# npm
|
|
228
|
+
OUTDATED=$(npm outdated --json 2>&1) || true
|
|
229
|
+
# yarn: yarn outdated --json 2>&1 || true
|
|
230
|
+
# pnpm: pnpm outdated --format json 2>&1 || true
|
|
231
|
+
# bun: bun outdated 2>&1 || true
|
|
232
|
+
```
|
|
233
|
+
If the output is not valid JSON (network failure, corrupt lockfile), log warning in MEMORY.md Notes and skip.
|
|
234
|
+
3. Skip if no outdated dependencies found (empty JSON object `{}`).
|
|
235
|
+
4. **Categorize updates:**
|
|
236
|
+
- **Patch** (e.g., `1.2.3` → `1.2.5`): safe, auto-upgrade
|
|
237
|
+
- **Minor** (e.g., `1.2.3` → `1.3.0`): likely safe, auto-upgrade
|
|
238
|
+
- **Major** (e.g., `1.2.3` → `2.0.0`): breaking changes — do NOT auto-upgrade
|
|
239
|
+
5. **Auto-upgrade patch + minor:** Run `npm update` (respects semver ranges in package.json). If package.json pins exact versions or ranges exclude the available update, use `npm install [pkg]@[wanted]` for each outdated package where wanted ≤ latest within the semver range. For other package managers: `yarn upgrade`, `pnpm update`, `bun update`.
|
|
240
|
+
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:
|
|
241
|
+
- Revert files AND restore node_modules to match:
|
|
242
|
+
```bash
|
|
243
|
+
git restore package.json [lockfile]
|
|
244
|
+
npm ci # or: yarn install --frozen-lockfile / pnpm install --frozen-lockfile / bun install --frozen-lockfile
|
|
245
|
+
```
|
|
246
|
+
- Log failed upgrade in MEMORY.md Notes: "Dependency upgrade failed at phase transition — [error summary]. Skipped."
|
|
247
|
+
- Do NOT block phase transition — continue with current versions.
|
|
248
|
+
7. **If verification passes:** Commit on main: `git add package.json [lockfile] && git commit -m "chore: upgrade dependencies"`. Push if remote exists.
|
|
249
|
+
8. **Major version upgrades:** For each package with a major version available, add a maintenance task to the CURRENT phase in TODO.md. Infer task type from the package's nature — devDependencies (eslint, vitest, typescript) use `Type: backend`, runtime dependencies use `Type: fullstack` or the appropriate frontend/backend type:
|
|
250
|
+
- `- [ ] **Upgrade [package]@[current] → [latest]** — major version, review changelog for breaking changes (Complexity: S, Type: [inferred])`
|
|
251
|
+
- If git integration active, delegate to `devops-integrator` to create issues for these tasks.
|
|
252
|
+
- Log in MEMORY.md Notes: "Major upgrades available: [list packages with current → latest]"
|
|
253
|
+
9. **Monorepo / Nx workspace:** If `nx.json` exists, run outdated check from workspace root (covers all hoisted deps — do NOT run per-project). Use `nx report` to verify plugin compatibility after upgrades. For Nx plugin major upgrades, prefer `nx migrate [package]@latest` over manual update — it generates migrations that handle config changes.
|
|
223
254
|
- Delegate to `product-owner` agent: "This is a PHASE RE-EVALUATION (not initial creation). Phase [N-1] is complete. Read PRODUCT.md, TODO.md, MEMORY.md, and scan the current codebase. Evaluate: are Phase [N] priorities still correct? Output a structured diff: ADD / REMOVE / REPRIORITIZE / CONFIRM. If everything looks good, just CONFIRM."
|
|
224
255
|
- If product-owner recommends changes -> delegate to `technical-planner` to update TODO.md
|
|
225
256
|
- CLAUDE.md refresh: attempt `/revise-claude-md` skill. If the skill is not available, manually read CLAUDE.md, append any new patterns/conventions discovered during the completed phase under `## Project-Specific Details`, and commit the update.
|
|
@@ -8,7 +8,7 @@ import { getErrorAction } from './lib/errors.mjs';
|
|
|
8
8
|
import { createLogger } from './lib/logger.mjs';
|
|
9
9
|
import { emptyCheckpoint, readCheckpoint, writeCheckpoint } from './lib/state.mjs';
|
|
10
10
|
import { runClaude, currentChild } from './lib/claude-runner.mjs';
|
|
11
|
-
import { sleep, formatDuration, acquireLock, releaseLock, readMemory, isProjectComplete, getCurrentTask, getCurrentPhase, checkClaudeInstalled, checkAuth, checkGitIdentity, checkFilesystemWritable, gitFetchAndPull, gitCheckState, notify, printSummary, promptUser, } from './lib/utils.mjs';
|
|
11
|
+
import { sleep, formatDuration, acquireLock, releaseLock, readMemory, isProjectComplete, getCurrentTask, getCurrentPhase, checkClaudeInstalled, checkAuth, checkGitIdentity, checkFilesystemWritable, gitFetchAndPull, gitCheckState, notify, printSummary, promptUser, parseUsageLimitResetMs, } from './lib/utils.mjs';
|
|
12
12
|
import { isTokenExpiringSoon, refreshOAuthToken } from './lib/oauth-refresh.mjs';
|
|
13
13
|
// ─── Args ───
|
|
14
14
|
function parseArgs(argv) {
|
|
@@ -371,6 +371,28 @@ async function main() {
|
|
|
371
371
|
consecutiveFailures: checkpoint.consecutiveFailures,
|
|
372
372
|
authRetries: checkpoint.authRetries,
|
|
373
373
|
});
|
|
374
|
+
// Handle usage limit — wait until reset time if parseable, otherwise stop
|
|
375
|
+
if (category === 'usage_limit') {
|
|
376
|
+
const waitMs = parseUsageLimitResetMs(output.stderr);
|
|
377
|
+
if (waitMs) {
|
|
378
|
+
log.warn(`Account usage limit reached. Waiting ${formatDuration(waitMs + 60_000)} until reset...`);
|
|
379
|
+
notify(opts.notifyCommand, 'error', `Usage limit — waiting ${formatDuration(waitMs + 60_000)}`, i);
|
|
380
|
+
writeCheckpoint(opts.projectDir, checkpoint, log);
|
|
381
|
+
await sleep(waitMs + 60_000, stoppingRef); // +1min buffer
|
|
382
|
+
if (stopping)
|
|
383
|
+
break;
|
|
384
|
+
checkpoint.consecutiveFailures = 0;
|
|
385
|
+
i--; // Retry this iteration
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// Can't parse reset time — stop
|
|
389
|
+
checkpoint.consecutiveFailures++;
|
|
390
|
+
log.error('Account usage limit reached. Could not parse reset time from output.');
|
|
391
|
+
log.error('Restart the loop after your limit resets.');
|
|
392
|
+
notify(opts.notifyCommand, 'stopped', 'Account usage limit (unknown reset time)', i);
|
|
393
|
+
writeCheckpoint(opts.projectDir, checkpoint, log);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
374
396
|
// Handle stop — try OAuth refresh for auth errors before giving up
|
|
375
397
|
if (action.type === 'stop') {
|
|
376
398
|
if (category === 'auth_expired') {
|
|
@@ -165,11 +165,12 @@ function killProcessTree(pid, isWin) {
|
|
|
165
165
|
}, 5000).unref();
|
|
166
166
|
}
|
|
167
167
|
// ─── Error signal patterns ───
|
|
168
|
-
const RATE_LIMIT_RE = /rate.?limit|429|overloaded|529/;
|
|
169
|
-
const AUTH_ERROR_RE = /authentication_error|failed to authenticate|api error: 401|invalid authentication|403 forbidden/;
|
|
168
|
+
const RATE_LIMIT_RE = /rate.?limit|429|overloaded|529|at capacity|capacity reached|503 service unavailable|502 bad gateway/;
|
|
169
|
+
const AUTH_ERROR_RE = /authentication_error|failed to authenticate|api error: 401|invalid authentication|403 forbidden|account.{0,10}(?:suspended|disabled|deactivated)/;
|
|
170
170
|
const AUTH_TYPE_RE = /authentication/;
|
|
171
171
|
const AUTH_MSG_RE = /authentication|401|403/;
|
|
172
172
|
const AUTH_SERVER_RE = /\b5\d\d\b/;
|
|
173
|
+
const USAGE_LIMIT_RE = /you.ve hit your limit|usage.?limit|plan.?limit|quota.?exceeded|daily.?limit|monthly.?limit/;
|
|
173
174
|
export let currentChild = null;
|
|
174
175
|
export function runClaude(opts, log, runOpts = {}) {
|
|
175
176
|
const startTime = process.hrtime.bigint();
|
|
@@ -229,6 +230,7 @@ export function runClaude(opts, log, runOpts = {}) {
|
|
|
229
230
|
let isRateLimit = false;
|
|
230
231
|
let isAuthError = false;
|
|
231
232
|
let isAuthServerError = false;
|
|
233
|
+
let isUsageLimit = false;
|
|
232
234
|
let resultReceived = false;
|
|
233
235
|
let buffer = '';
|
|
234
236
|
let killed = false;
|
|
@@ -240,6 +242,8 @@ export function runClaude(opts, log, runOpts = {}) {
|
|
|
240
242
|
return;
|
|
241
243
|
const msg = (event.error?.message ?? (typeof event.message === 'string' ? event.message : '') ?? '').toLowerCase();
|
|
242
244
|
const errType = (event.error?.type ?? '').toLowerCase();
|
|
245
|
+
if (USAGE_LIMIT_RE.test(msg))
|
|
246
|
+
isUsageLimit = true;
|
|
243
247
|
if (RATE_LIMIT_RE.test(msg))
|
|
244
248
|
isRateLimit = true;
|
|
245
249
|
if (AUTH_TYPE_RE.test(errType) || AUTH_MSG_RE.test(msg))
|
|
@@ -248,6 +252,8 @@ export function runClaude(opts, log, runOpts = {}) {
|
|
|
248
252
|
isAuthServerError = true;
|
|
249
253
|
}
|
|
250
254
|
function detectTextSignals(lower) {
|
|
255
|
+
if (USAGE_LIMIT_RE.test(lower))
|
|
256
|
+
isUsageLimit = true;
|
|
251
257
|
if (RATE_LIMIT_RE.test(lower))
|
|
252
258
|
isRateLimit = true;
|
|
253
259
|
if (AUTH_ERROR_RE.test(lower))
|
|
@@ -405,6 +411,7 @@ export function runClaude(opts, log, runOpts = {}) {
|
|
|
405
411
|
isRateLimit,
|
|
406
412
|
isAuthError,
|
|
407
413
|
isAuthServerError,
|
|
414
|
+
isUsageLimit,
|
|
408
415
|
timedOut: killReason === 'process_timeout',
|
|
409
416
|
activityTimedOut: killReason === 'activity_timeout',
|
|
410
417
|
hasResult: resultReceived,
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Pure functions, no side effects. Lookup maps instead of nested conditions.
|
|
3
3
|
/** Pattern table: checked in order, first match wins. */
|
|
4
4
|
const STDERR_PATTERNS = [
|
|
5
|
-
['auth_expired', /authentication_error|invalid authentication|api error: 401|failed to authenticate|403 forbidden/i],
|
|
6
|
-
['
|
|
5
|
+
['auth_expired', /authentication_error|invalid authentication|api error: 401|failed to authenticate|403 forbidden|account.{0,10}(?:suspended|disabled|deactivated)/i],
|
|
6
|
+
['usage_limit', /you.ve hit your limit|usage.?limit|plan.?limit|quota.?exceeded|daily.?limit|monthly.?limit/i],
|
|
7
|
+
['rate_limited', /rate.?limit|too many requests|429|overloaded|529|at capacity|capacity reached|503 service unavailable|502 bad gateway/i],
|
|
7
8
|
['disk_full', /ENOSPC|no space left/i],
|
|
8
9
|
['oom_killed', /out of memory|heap|allocation failed/i],
|
|
9
10
|
['network_error', /ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|network|CERT_|SSL_|proxy/i],
|
|
@@ -20,6 +21,9 @@ export function classifyError(signals) {
|
|
|
20
21
|
return 'auth_expired';
|
|
21
22
|
if (signals.isAuthServerError)
|
|
22
23
|
return 'auth_server_error';
|
|
24
|
+
// Usage limit (account quota) takes priority over rate limit — it's hours, not seconds
|
|
25
|
+
if (signals.isUsageLimit)
|
|
26
|
+
return 'usage_limit';
|
|
23
27
|
if (signals.isRateLimit)
|
|
24
28
|
return 'rate_limited';
|
|
25
29
|
if (signals.timedOut) {
|
|
@@ -69,6 +73,12 @@ const STRATEGIES = {
|
|
|
69
73
|
auth_server_error: (ctx) => ctx.authRetries >= MAX_AUTH_RETRIES
|
|
70
74
|
? { type: 'stop', reason: `Auth server failed ${MAX_AUTH_RETRIES}x. Try again later.` }
|
|
71
75
|
: { type: 'backoff', ms: 30_000 * (ctx.authRetries + 1) },
|
|
76
|
+
// Note: usage_limit has custom handling in autonomous.mts (wait until reset time).
|
|
77
|
+
// The 'stop' here is a fallback if that custom handler can't parse the reset time.
|
|
78
|
+
usage_limit: () => ({
|
|
79
|
+
type: 'stop',
|
|
80
|
+
reason: 'Account usage limit reached. Loop will resume automatically if reset time is detected.',
|
|
81
|
+
}),
|
|
72
82
|
rate_limited: () => ({ type: 'backoff', ms: 0 }), // ms overridden by checkpoint.rateLimitBackoff
|
|
73
83
|
network_error: (ctx) => ctx.consecutiveFailures >= MAX_CONSECUTIVE
|
|
74
84
|
? { type: 'stop', reason: `${MAX_CONSECUTIVE} consecutive network errors.` }
|
|
@@ -6,6 +6,35 @@ import { hostname } from 'node:os';
|
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
// Node.js runtime accepts `true` for shell but @types expects string
|
|
8
8
|
const SHELL = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
9
|
+
// ─── Usage limit reset time parser ───
|
|
10
|
+
/** Parse "resets Xpm (UTC)" / "resets X:XX AM (UTC)" / "resets HH:MM (UTC)" from stderr text.
|
|
11
|
+
* Returns milliseconds to wait, or null if unparseable. */
|
|
12
|
+
export function parseUsageLimitResetMs(text) {
|
|
13
|
+
// Match patterns: "resets 6pm (UTC)", "resets 6:30pm (UTC)", "resets 18:00 (UTC)", "resets 6 PM (UTC)"
|
|
14
|
+
const match = text.match(/resets\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(UTC\)/i);
|
|
15
|
+
if (!match)
|
|
16
|
+
return null;
|
|
17
|
+
let hours = parseInt(match[1], 10);
|
|
18
|
+
const minutes = match[2] ? parseInt(match[2], 10) : 0;
|
|
19
|
+
const ampm = match[3]?.toLowerCase();
|
|
20
|
+
if (ampm === 'pm' && hours < 12)
|
|
21
|
+
hours += 12;
|
|
22
|
+
if (ampm === 'am' && hours === 12)
|
|
23
|
+
hours = 0;
|
|
24
|
+
if (hours > 23 || minutes > 59)
|
|
25
|
+
return null;
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const reset = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hours, minutes, 0, 0));
|
|
28
|
+
// If reset time is in the past, it's tomorrow
|
|
29
|
+
if (reset.getTime() <= now.getTime()) {
|
|
30
|
+
reset.setUTCDate(reset.getUTCDate() + 1);
|
|
31
|
+
}
|
|
32
|
+
const waitMs = reset.getTime() - now.getTime();
|
|
33
|
+
// Sanity: max 24 hours
|
|
34
|
+
if (waitMs > 24 * 60 * 60_000)
|
|
35
|
+
return null;
|
|
36
|
+
return waitMs;
|
|
37
|
+
}
|
|
9
38
|
// ─── Sleep ───
|
|
10
39
|
export function sleep(ms, stoppingRef) {
|
|
11
40
|
if (!stoppingRef)
|