create-claude-workspace 1.1.35 → 1.1.37

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.
@@ -151,7 +151,7 @@ If no remote, skip this step.
151
151
  ### 10. One task per invocation
152
152
  - Complete exactly ONE task per invocation, then end the session cleanly.
153
153
  - The external loop (`autonomous.mjs` or ralph-loop) handles iteration control — you do NOT need to manage capacity, iteration counters, or session bounds.
154
- - After committing the task (STEP 11) and post-merge (STEP 12), update MEMORY.md and EXIT. Do NOT ask whether to continue, do NOT wait for confirmation, do NOT start another task.
154
+ - 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.
155
155
  - Track complexity in MEMORY.md `Complexity This Session` for informational purposes only (S=1, M=2, L=4).
156
156
 
157
157
  ## Task Type Detection
@@ -482,7 +482,8 @@ To determine if a task is frontend, backend, or fullstack, use this heuristic:
482
482
  ```
483
483
  Note the stacking in MEMORY.md. When the previous MR/PR is merged: `git -C .worktrees/feat/{next} rebase --onto main feat/{previous}`
484
484
  - **If no `[ ]` tasks remain in current phase AND at least one `[~]` exists** (all remaining were skipped): do NOT auto-advance. Delegate to `product-owner`: "All tasks in Phase [N] are blocked/skipped. The autonomous loop cannot proceed. Options: (1) ADD replacement tasks that unblock progress, (2) REPRIORITIZE to skip this phase entirely and move to next, (3) declare a BLOCKER requiring human intervention." If product-owner returns only CONFIRM with no actionable changes, STOP the autonomous loop and log: "Phase [N] fully blocked — requires human intervention. Run orchestrator manually after resolving blockers."
485
- - One task per invocation — after post-merge, end session. The external loop will start the next invocation. Do NOT update MEMORY.md here (already done in STEP 11).
485
+ - One task per invocation — after post-merge, end session. The external loop will start the next invocation.
486
+ - **CRITICAL: ZERO commits after merge.** MEMORY.md and TODO.md are already on main (they came from the merge). Do NOT `git add`, do NOT `git commit`, do NOT create `chore:` commits. If you see tracking files as "modified" after merge, that is IMPOSSIBLE if STEP 11 was done correctly — investigate, do NOT blindly commit.
486
487
  - **If no `[ ]` tasks remain in current phase AND zero `[~]` exist** (all completed) -> phase transition (handled at start of STEP 1)
487
488
  - **If no `[ ]` tasks remain in ALL phases AND zero `[~]` exist** (everything completed) -> final completion sequence:
488
489
  1. Delegate to `product-owner` agent:
@@ -598,7 +599,7 @@ Examples:
598
599
  - NEVER implement without architect plan (STEP 2) or skip code review (STEP 7)
599
600
  - Each commit = 1 logical unit
600
601
  - Conventional commits: feat/fix/refactor/chore/docs
601
- - **NEVER create commits that only change MEMORY.md or TODO.md** tracking file updates MUST be included in the same commit as the implementation they track (STEP 11). After merge (STEP 12), do NOT commit anything. No `chore: update progress tracking`, no `chore: update MEMORY.md`, no `chore: log hotfix`. If you feel the urge to commit tracking-only changes, STOPyou missed including them in STEP 11.
602
+ - **ABSOLUTE BAN: NEVER create commits that only change MEMORY.md or TODO.md.** Tracking files are committed WITH implementation code in STEP 11. After merge (STEP 12), ZERO commits tracking files arrive on main via the merge itself. Forbidden patterns: `chore: update progress tracking`, `chore: update MEMORY.md`, `chore: update MEMORY.md tracking after ...`, `chore: log hotfix`. If tracking files show as modified after merge, something went wrong in STEP 11 investigate and fix, do NOT create a band-aid commit.
602
603
  - All files in ENGLISH
603
604
  - TODO.md + MEMORY.md are your tracking system — keep them PRECISE and CURRENT
604
605
  - If deviating from plan, LOG the reason in MEMORY.md
@@ -9,6 +9,7 @@ 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
11
  import { sleep, formatDuration, acquireLock, releaseLock, readMemory, isProjectComplete, getCurrentTask, getCurrentPhase, checkClaudeInstalled, checkAuth, checkGitIdentity, checkFilesystemWritable, gitFetchAndPull, gitCheckState, notify, printSummary, promptUser, } from './lib/utils.mjs';
12
+ import { isTokenExpiringSoon, refreshOAuthToken } from './lib/oauth-refresh.mjs';
12
13
  // ─── Args ───
13
14
  function parseArgs(argv) {
14
15
  const opts = { ...DEFAULTS };
@@ -291,18 +292,34 @@ async function main() {
291
292
  if (timeSinceLastAuth > opts.processTimeout * 2) {
292
293
  log.warn('Large time gap detected (system sleep?). Re-checking auth...');
293
294
  if (!checkAuth()) {
294
- log.error('Auth expired after sleep. Re-authenticate and restart.');
295
- notify(opts.notifyCommand, 'stopped', 'Auth expired after system sleep', i);
296
- break;
295
+ log.warn('Auth expired after sleep. Attempting OAuth refresh...');
296
+ const refreshed = await refreshOAuthToken(log);
297
+ if (!refreshed || !checkAuth()) {
298
+ log.error('Auth expired after sleep and refresh failed. Run `claude /login`.');
299
+ notify(opts.notifyCommand, 'stopped', 'Auth expired after system sleep (refresh failed)', i);
300
+ break;
301
+ }
302
+ log.info('OAuth token refreshed after sleep. Continuing.');
297
303
  }
298
304
  checkpoint.lastAuthCheckTime = Date.now();
299
305
  }
300
- // Periodic auth re-check (every 30 min)
301
- if (timeSinceLastAuth > 30 * 60_000) {
306
+ // Proactive token refresh refresh before expiry to prevent mid-iteration failures
307
+ if (isTokenExpiringSoon()) {
308
+ log.info('OAuth token expiring soon. Refreshing proactively...');
309
+ await refreshOAuthToken(log);
310
+ }
311
+ // Periodic auth re-check (every 10 min)
312
+ if (timeSinceLastAuth > 10 * 60_000) {
302
313
  if (!checkAuth()) {
303
- log.error('Auth expired. Run `claude /login` or check ANTHROPIC_API_KEY.');
304
- notify(opts.notifyCommand, 'stopped', 'Auth expired', i);
305
- break;
314
+ // Try OAuth refresh before giving up
315
+ log.warn('Auth check failed. Attempting OAuth token refresh...');
316
+ const refreshed = await refreshOAuthToken(log);
317
+ if (!refreshed || !checkAuth()) {
318
+ log.error('Auth expired and refresh failed. Run `claude /login` or check ANTHROPIC_API_KEY.');
319
+ notify(opts.notifyCommand, 'stopped', 'Auth expired (refresh failed)', i);
320
+ break;
321
+ }
322
+ log.info('OAuth token refreshed successfully. Continuing.');
306
323
  }
307
324
  checkpoint.lastAuthCheckTime = Date.now();
308
325
  }
@@ -333,14 +350,37 @@ async function main() {
333
350
  stats.processTimeouts++;
334
351
  if (category === 'activity_timeout')
335
352
  stats.activityTimeouts++;
353
+ // Immediate auth check after any error — catches expired OAuth tokens that
354
+ // weren't classified as auth_expired (e.g., process_timeout with auth errors in stderr)
355
+ if (category !== 'auth_expired' && !checkAuth()) {
356
+ log.warn('Auth check failed after error. Attempting OAuth refresh...');
357
+ const refreshed = await refreshOAuthToken(log);
358
+ if (!refreshed || !checkAuth()) {
359
+ log.error('Auth expired and refresh failed. Run `claude /login` or check ANTHROPIC_API_KEY.');
360
+ notify(opts.notifyCommand, 'stopped', 'Auth expired (refresh failed after process error)', i);
361
+ writeCheckpoint(opts.projectDir, checkpoint, log);
362
+ break;
363
+ }
364
+ log.info('OAuth token refreshed after error. Continuing.');
365
+ }
336
366
  }
337
367
  // Get response action
338
368
  const action = getErrorAction(category, {
339
369
  consecutiveFailures: checkpoint.consecutiveFailures,
340
370
  authRetries: checkpoint.authRetries,
341
371
  });
342
- // Handle stop
372
+ // Handle stop — try OAuth refresh for auth errors before giving up
343
373
  if (action.type === 'stop') {
374
+ if (category === 'auth_expired') {
375
+ log.warn('Auth expired. Attempting OAuth token refresh...');
376
+ const refreshed = await refreshOAuthToken(log);
377
+ if (refreshed && checkAuth()) {
378
+ log.info('OAuth token refreshed. Retrying iteration.');
379
+ checkpoint.lastAuthCheckTime = Date.now();
380
+ i--; // Retry this iteration
381
+ continue;
382
+ }
383
+ }
344
384
  log.error(action.reason);
345
385
  notify(opts.notifyCommand, 'stopped', action.reason, i);
346
386
  writeCheckpoint(opts.projectDir, checkpoint, log);
@@ -352,9 +352,9 @@ export function runClaude(opts, log, runOpts = {}) {
352
352
  // Kill on auth error burst (e.g. expired token causing infinite retry loop)
353
353
  if (AUTH_ERROR_RE.test(lower)) {
354
354
  authErrorCount++;
355
- if (authErrorCount >= 5 && !killed) {
355
+ if (authErrorCount >= 3 && !killed) {
356
356
  log.warn(`Auth error burst detected (${authErrorCount} errors) — killing process.`);
357
- killChild('activity_timeout');
357
+ killChild('process_timeout');
358
358
  return;
359
359
  }
360
360
  }
@@ -15,14 +15,20 @@ export function classifyError(signals) {
15
15
  if (signals.code === 0 && signals.hasResult)
16
16
  return 'none';
17
17
  // Flag-based (set by stream events, not just stderr)
18
+ // Auth errors take priority over everything — prevents 30-min timeout loops on expired tokens
18
19
  if (signals.isAuthError)
19
20
  return 'auth_expired';
20
21
  if (signals.isAuthServerError)
21
22
  return 'auth_server_error';
22
23
  if (signals.isRateLimit)
23
24
  return 'rate_limited';
24
- if (signals.timedOut)
25
+ if (signals.timedOut) {
26
+ // Double-check stderr for auth errors (may not have been caught by stream events)
27
+ if (/authentication_error|failed to authenticate|api error: 401|oauth token has expired/i.test(signals.stderr)) {
28
+ return 'auth_expired';
29
+ }
25
30
  return 'process_timeout';
31
+ }
26
32
  // Activity timeout after result = success (process just hung after finishing)
27
33
  if (signals.activityTimedOut)
28
34
  return signals.hasResult ? 'none' : 'activity_timeout';
@@ -0,0 +1,146 @@
1
+ // ─── OAuth token refresh for Claude Code ───
2
+ // Cross-platform: reads/writes ~/.claude/.credentials.json (Windows/Linux)
3
+ // or macOS Keychain via `security` CLI.
4
+ // Refresh endpoint: https://console.anthropic.com/v1/oauth/token
5
+ import { readFileSync, writeFileSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ import { execSync } from 'node:child_process';
9
+ const TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
10
+ const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
11
+ const CREDENTIALS_FILE = '.claude/.credentials.json';
12
+ const KEYCHAIN_SERVICE = 'Claude Code-credentials';
13
+ // Buffer: refresh if token expires within this many ms
14
+ const REFRESH_BUFFER_MS = 30 * 60_000; // 30 minutes
15
+ // ─── Credential storage (cross-platform) ───
16
+ function credentialsPath() {
17
+ return resolve(homedir(), CREDENTIALS_FILE);
18
+ }
19
+ function readCredentialsFile() {
20
+ try {
21
+ const raw = readFileSync(credentialsPath(), 'utf-8');
22
+ const parsed = JSON.parse(raw);
23
+ if (parsed?.claudeAiOauth?.refreshToken)
24
+ return parsed;
25
+ return null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function writeCredentialsFile(creds) {
32
+ writeFileSync(credentialsPath(), JSON.stringify(creds));
33
+ }
34
+ function readKeychainCredentials() {
35
+ try {
36
+ // Try common account names
37
+ const accounts = ['claude', 'Claude Code', 'default'];
38
+ for (const account of accounts) {
39
+ try {
40
+ const raw = execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${account}" -w`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
41
+ const parsed = JSON.parse(raw);
42
+ if (parsed?.claudeAiOauth?.refreshToken)
43
+ return parsed;
44
+ }
45
+ catch { /* try next account */ }
46
+ }
47
+ return null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ function writeKeychainCredentials(creds, account) {
54
+ const data = JSON.stringify(creds);
55
+ try {
56
+ execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${account}"`, { stdio: 'ignore', timeout: 5000 });
57
+ }
58
+ catch { /* may not exist */ }
59
+ execSync(`security add-generic-password -s "${KEYCHAIN_SERVICE}" -a "${account}" -w '${data.replace(/'/g, "'\\''")}' -U`, { stdio: 'ignore', timeout: 5000 });
60
+ }
61
+ function readCredentials() {
62
+ // File-based (Windows/Linux) takes priority — always present
63
+ const fileCreds = readCredentialsFile();
64
+ if (fileCreds)
65
+ return fileCreds;
66
+ // macOS Keychain fallback
67
+ if (process.platform === 'darwin')
68
+ return readKeychainCredentials();
69
+ return null;
70
+ }
71
+ function writeCredentials(creds) {
72
+ // Always write to file
73
+ writeCredentialsFile(creds);
74
+ // Also update Keychain on macOS
75
+ if (process.platform === 'darwin') {
76
+ try {
77
+ writeKeychainCredentials(creds, 'claude');
78
+ }
79
+ catch { /* non-fatal */ }
80
+ }
81
+ }
82
+ // ─── Token refresh ───
83
+ async function postRefresh(refreshToken) {
84
+ const body = JSON.stringify({
85
+ grant_type: 'refresh_token',
86
+ refresh_token: refreshToken,
87
+ client_id: CLIENT_ID,
88
+ });
89
+ const response = await fetch(TOKEN_URL, {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body,
93
+ signal: AbortSignal.timeout(30_000),
94
+ });
95
+ if (!response.ok) {
96
+ const text = await response.text().catch(() => '');
97
+ throw new Error(`OAuth refresh failed: ${response.status} ${text.slice(0, 200)}`);
98
+ }
99
+ return await response.json();
100
+ }
101
+ // ─── Public API ───
102
+ /** Check if the current OAuth token is expired or about to expire. */
103
+ export function isTokenExpiringSoon() {
104
+ const creds = readCredentials();
105
+ if (!creds)
106
+ return false; // No OAuth credentials (API key user) — not our problem
107
+ return Date.now() > (creds.claudeAiOauth.expiresAt - REFRESH_BUFFER_MS);
108
+ }
109
+ /** Attempt to refresh the OAuth token. Returns true on success. */
110
+ export async function refreshOAuthToken(log) {
111
+ const creds = readCredentials();
112
+ if (!creds) {
113
+ log.warn('No OAuth credentials found — cannot refresh.');
114
+ return false;
115
+ }
116
+ const oauth = creds.claudeAiOauth;
117
+ if (!oauth.refreshToken) {
118
+ log.warn('No refresh token available.');
119
+ return false;
120
+ }
121
+ const remaining = oauth.expiresAt - Date.now();
122
+ log.info(`Token expires in ${Math.round(remaining / 60_000)}m. Attempting refresh...`);
123
+ try {
124
+ const result = await postRefresh(oauth.refreshToken);
125
+ if (!result.access_token || !result.refresh_token) {
126
+ log.error('Refresh response missing tokens.');
127
+ return false;
128
+ }
129
+ // Update credentials
130
+ const updated = {
131
+ claudeAiOauth: {
132
+ ...oauth,
133
+ accessToken: result.access_token,
134
+ refreshToken: result.refresh_token,
135
+ expiresAt: Date.now() + (result.expires_in * 1000),
136
+ },
137
+ };
138
+ writeCredentials(updated);
139
+ log.info(`OAuth token refreshed. New expiry: ${new Date(updated.claudeAiOauth.expiresAt).toISOString()}`);
140
+ return true;
141
+ }
142
+ catch (err) {
143
+ log.error(`OAuth refresh failed: ${err.message}`);
144
+ return false;
145
+ }
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "1.1.35",
3
+ "version": "1.1.37",
4
4
  "description": "Scaffold a project with Claude Code agents for autonomous AI-driven development",
5
5
  "type": "module",
6
6
  "bin": {