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.
- package/dist/template/.claude/agents/orchestrator.md +4 -3
- package/dist/template/.claude/scripts/autonomous.mjs +49 -9
- package/dist/template/.claude/scripts/lib/claude-runner.mjs +2 -2
- package/dist/template/.claude/scripts/lib/errors.mjs +7 -1
- package/dist/template/.claude/scripts/lib/oauth-refresh.mjs +146 -0
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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
|
|
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.
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
//
|
|
301
|
-
if (
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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 >=
|
|
355
|
+
if (authErrorCount >= 3 && !killed) {
|
|
356
356
|
log.warn(`Auth error burst detected (${authErrorCount} errors) — killing process.`);
|
|
357
|
-
killChild('
|
|
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
|
+
}
|