create-claude-workspace 1.1.101 → 1.1.103
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 +49 -3
- package/dist/scripts/autonomous.spec.js +10 -0
- package/dist/scripts/lib/idle-poll.mjs +196 -0
- package/dist/scripts/lib/idle-poll.spec.js +92 -0
- package/dist/scripts/lib/state.mjs +1 -0
- package/dist/scripts/lib/types.mjs +2 -0
- package/dist/scripts/lib/utils.mjs +17 -2
- package/dist/scripts/lib/utils.spec.js +50 -2
- package/dist/template/.claude/agents/orchestrator.md +22 -1
- package/dist/template/.claude/agents/project-initializer.md +30 -2
- package/dist/template/.claude/agents/ui-engineer.md +1 -1
- package/dist/template/.claude/templates/claude-md.md +3 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import { emptyCheckpoint, readCheckpoint, writeCheckpoint } from './lib/state.mj
|
|
|
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, parseUsageLimitResetMs, } from './lib/utils.mjs';
|
|
12
12
|
import { isTokenExpiringSoon, refreshOAuthToken } from './lib/oauth-refresh.mjs';
|
|
13
|
+
import { pollForNewWork } from './lib/idle-poll.mjs';
|
|
13
14
|
// ─── Args ───
|
|
14
15
|
function parseArgs(argv) {
|
|
15
16
|
const opts = { ...DEFAULTS };
|
|
@@ -119,6 +120,16 @@ function parseArgs(argv) {
|
|
|
119
120
|
i++;
|
|
120
121
|
continue;
|
|
121
122
|
}
|
|
123
|
+
if (arg === '--idle-poll') {
|
|
124
|
+
opts.idlePollInterval = numFlag(arg, i);
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg === '--max-idle') {
|
|
129
|
+
opts.maxIdleTime = numFlag(arg, i);
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
122
133
|
if (arg.startsWith('--')) {
|
|
123
134
|
console.error(`Unknown option: ${arg}`);
|
|
124
135
|
process.exit(1);
|
|
@@ -148,6 +159,8 @@ Options:
|
|
|
148
159
|
--log-file <path> Log file (default: .claude/autonomous.log)
|
|
149
160
|
--no-lock Disable lock file
|
|
150
161
|
--no-pull Skip auto git pull
|
|
162
|
+
--idle-poll <ms> Poll interval when idle (default: 300000 = 5min)
|
|
163
|
+
--max-idle <ms> Max idle time before exit, 0=unlimited (default: 0)
|
|
151
164
|
--dry-run Validate prerequisites only
|
|
152
165
|
--help Show this message
|
|
153
166
|
`);
|
|
@@ -330,9 +343,41 @@ async function main() {
|
|
|
330
343
|
}
|
|
331
344
|
// Read project state
|
|
332
345
|
const memory = readMemory(opts.projectDir);
|
|
333
|
-
if (isProjectComplete(memory)) {
|
|
334
|
-
|
|
335
|
-
|
|
346
|
+
if (isProjectComplete(memory, opts.projectDir)) {
|
|
347
|
+
if (!checkpoint.completionVerified) {
|
|
348
|
+
// First time seeing "complete" — let orchestrator check for external issues once
|
|
349
|
+
log.info('Project marked complete. Running orchestrator to verify (checking for external issues)...');
|
|
350
|
+
checkpoint.completionVerified = true;
|
|
351
|
+
writeCheckpoint(opts.projectDir, checkpoint, log);
|
|
352
|
+
// Fall through to run Claude (orchestrator step 1b will check external issues)
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Orchestrator confirmed complete — enter idle polling mode
|
|
356
|
+
log.info('Project complete. Entering idle polling mode...');
|
|
357
|
+
const idleStart = Date.now();
|
|
358
|
+
let foundWork = false;
|
|
359
|
+
while (!stopping) {
|
|
360
|
+
const poll = await pollForNewWork(opts.projectDir, log);
|
|
361
|
+
if (poll.hasWork) {
|
|
362
|
+
log.info(`New work detected (source: ${poll.source}${poll.issueCount ? `, ${poll.issueCount} issue(s)` : ''}). Resuming development...`);
|
|
363
|
+
notify(opts.notifyCommand, 'info', `New work detected from ${poll.source}`, i);
|
|
364
|
+
checkpoint.completionVerified = false;
|
|
365
|
+
writeCheckpoint(opts.projectDir, checkpoint, log);
|
|
366
|
+
foundWork = true;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (opts.maxIdleTime > 0 && Date.now() - idleStart >= opts.maxIdleTime) {
|
|
370
|
+
log.info(`Max idle time (${formatDuration(opts.maxIdleTime)}) reached. Exiting.`);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
log.debug(`No new work. Polling again in ${formatDuration(opts.idlePollInterval)}...`);
|
|
374
|
+
await sleep(opts.idlePollInterval, stoppingRef);
|
|
375
|
+
}
|
|
376
|
+
if (!foundWork)
|
|
377
|
+
break;
|
|
378
|
+
// Re-read state after idle — memory may have changed
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
336
381
|
}
|
|
337
382
|
const phase = getCurrentPhase(memory);
|
|
338
383
|
const task = getCurrentTask(memory);
|
|
@@ -484,6 +529,7 @@ async function main() {
|
|
|
484
529
|
if (result.task_completed) {
|
|
485
530
|
stats.tasksCompleted.push(result.task_completed);
|
|
486
531
|
log.info(`Completed: ${result.task_completed}`);
|
|
532
|
+
checkpoint.completionVerified = false; // Reset so completion is re-verified if all tasks finish
|
|
487
533
|
}
|
|
488
534
|
if (result.next_task)
|
|
489
535
|
log.info(`Next: ${result.next_task}`);
|
|
@@ -33,4 +33,14 @@ describe('parseArgs', () => {
|
|
|
33
33
|
expect(parseArgs(['--help']).help).toBe(true);
|
|
34
34
|
expect(parseArgs(['-h']).help).toBe(true);
|
|
35
35
|
});
|
|
36
|
+
it('parses idle poll flags', () => {
|
|
37
|
+
const opts = parseArgs(['--idle-poll', '60000', '--max-idle', '3600000']);
|
|
38
|
+
expect(opts.idlePollInterval).toBe(60000);
|
|
39
|
+
expect(opts.maxIdleTime).toBe(3600000);
|
|
40
|
+
});
|
|
41
|
+
it('uses defaults for idle poll flags when not provided', () => {
|
|
42
|
+
const opts = parseArgs([]);
|
|
43
|
+
expect(opts.idlePollInterval).toBe(5 * 60_000);
|
|
44
|
+
expect(opts.maxIdleTime).toBe(0);
|
|
45
|
+
});
|
|
36
46
|
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// ─── Idle polling: check for new work without AI ───
|
|
2
|
+
// Checks TODO.md (filesystem) and GitHub/GitLab/ClickUp API for new issues.
|
|
3
|
+
// Zero AI tokens consumed — pure Node.js HTTP + filesystem checks.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
// ─── TODO.md check (filesystem, free) ───
|
|
8
|
+
export function checkTodoForPendingTasks(projectDir) {
|
|
9
|
+
try {
|
|
10
|
+
const todo = readFileSync(resolve(projectDir, 'TODO.md'), 'utf-8');
|
|
11
|
+
return /^- \[ \] /m.test(todo);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// ─── Platform detection ───
|
|
18
|
+
export function detectPlatform(projectDir) {
|
|
19
|
+
const config = {
|
|
20
|
+
git: null,
|
|
21
|
+
gitOwner: null,
|
|
22
|
+
gitToken: null,
|
|
23
|
+
taskPlatform: null,
|
|
24
|
+
clickupListIds: [],
|
|
25
|
+
clickupToken: null,
|
|
26
|
+
};
|
|
27
|
+
// 1. Detect git platform from remote URL
|
|
28
|
+
try {
|
|
29
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
30
|
+
cwd: projectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
}).trim();
|
|
32
|
+
if (remoteUrl.includes('github.com')) {
|
|
33
|
+
config.git = 'github';
|
|
34
|
+
config.gitOwner = extractGitHubOwnerRepo(remoteUrl);
|
|
35
|
+
config.gitToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || null;
|
|
36
|
+
config.taskPlatform = 'github'; // default, may be overridden by ClickUp
|
|
37
|
+
}
|
|
38
|
+
else if (remoteUrl.includes('gitlab')) {
|
|
39
|
+
config.git = 'gitlab';
|
|
40
|
+
config.gitOwner = extractGitLabProject(remoteUrl);
|
|
41
|
+
config.gitToken = process.env.GITLAB_TOKEN || null;
|
|
42
|
+
config.taskPlatform = 'gitlab'; // default, may be overridden by ClickUp
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { /* no remote */ }
|
|
46
|
+
// 2. Check for ClickUp override in CLAUDE.md
|
|
47
|
+
try {
|
|
48
|
+
const claudeMd = readFileSync(resolve(projectDir, 'CLAUDE.md'), 'utf-8');
|
|
49
|
+
if (/TaskPlatform:\s*clickup/i.test(claudeMd)) {
|
|
50
|
+
config.taskPlatform = 'clickup';
|
|
51
|
+
config.clickupToken = process.env.CLICKUP_API_TOKEN || null;
|
|
52
|
+
// Extract ClickUp Space ID for list discovery
|
|
53
|
+
const spaceMatch = claudeMd.match(/ClickUp Space ID:\s*(\S+)/i);
|
|
54
|
+
if (spaceMatch) {
|
|
55
|
+
config.clickupListIds.push(spaceMatch[1]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* no CLAUDE.md */ }
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
/** Extract "owner/repo" from GitHub URL (HTTPS or SSH) */
|
|
63
|
+
export function extractGitHubOwnerRepo(url) {
|
|
64
|
+
// https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
65
|
+
const match = url.match(/github\.com[/:]([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
66
|
+
return match ? match[1] : null;
|
|
67
|
+
}
|
|
68
|
+
/** Extract project path from GitLab URL */
|
|
69
|
+
export function extractGitLabProject(url) {
|
|
70
|
+
// https://gitlab.com/group/project.git or git@gitlab.com:group/project.git
|
|
71
|
+
const cleaned = url.replace(/\.git$/, '');
|
|
72
|
+
// Match after the host portion: "gitlab.com/" or "gitlab.com:"
|
|
73
|
+
const match = cleaned.match(/gitlab[^/:]*[/:]([\w./-]+)$/);
|
|
74
|
+
return match ? match[1] : null;
|
|
75
|
+
}
|
|
76
|
+
// ─── API polling (no AI, just HTTP) ───
|
|
77
|
+
async function fetchJson(url, headers) {
|
|
78
|
+
const response = await fetch(url, { headers, signal: AbortSignal.timeout(10_000) });
|
|
79
|
+
if (!response.ok)
|
|
80
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
/** Check GitHub for open issues not yet in TODO.md */
|
|
84
|
+
async function pollGitHub(ownerRepo, token, projectDir) {
|
|
85
|
+
const headers = {
|
|
86
|
+
'Accept': 'application/vnd.github+json',
|
|
87
|
+
'Authorization': `Bearer ${token}`,
|
|
88
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
89
|
+
};
|
|
90
|
+
const url = `https://api.github.com/repos/${ownerRepo}/issues?state=open&per_page=100&sort=created&direction=desc`;
|
|
91
|
+
const issues = await fetchJson(url, headers);
|
|
92
|
+
// Filter out PRs (GitHub API returns PRs as issues)
|
|
93
|
+
const realIssues = issues.filter(i => !i.pull_request);
|
|
94
|
+
// Check which issues are already tracked in TODO.md
|
|
95
|
+
const todoContent = readTodoSafe(projectDir);
|
|
96
|
+
let newCount = 0;
|
|
97
|
+
for (const issue of realIssues) {
|
|
98
|
+
if (!todoContent.includes(`<!-- #${issue.number} -->`)) {
|
|
99
|
+
newCount++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return newCount;
|
|
103
|
+
}
|
|
104
|
+
/** Check GitLab for open issues not yet in TODO.md */
|
|
105
|
+
async function pollGitLab(projectPath, token, projectDir) {
|
|
106
|
+
const headers = {
|
|
107
|
+
'PRIVATE-TOKEN': token,
|
|
108
|
+
};
|
|
109
|
+
// URL-encode the project path for the API
|
|
110
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
111
|
+
const url = `https://gitlab.com/api/v4/projects/${encodedPath}/issues?state=opened&per_page=100&order_by=created_at&sort=desc`;
|
|
112
|
+
const issues = await fetchJson(url, headers);
|
|
113
|
+
const todoContent = readTodoSafe(projectDir);
|
|
114
|
+
let newCount = 0;
|
|
115
|
+
for (const issue of issues) {
|
|
116
|
+
if (!todoContent.includes(`<!-- #${issue.iid} -->`)) {
|
|
117
|
+
newCount++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return newCount;
|
|
121
|
+
}
|
|
122
|
+
/** Check ClickUp for tasks with "to do" status */
|
|
123
|
+
async function pollClickUp(spaceId, token, projectDir) {
|
|
124
|
+
const headers = {
|
|
125
|
+
'Authorization': token,
|
|
126
|
+
};
|
|
127
|
+
// First get lists in the space (ClickUp: Space → Lists → Tasks)
|
|
128
|
+
const listsUrl = `https://api.clickup.com/api/v2/space/${spaceId}/list`;
|
|
129
|
+
const listsData = await fetchJson(listsUrl, headers);
|
|
130
|
+
const todoContent = readTodoSafe(projectDir);
|
|
131
|
+
let newCount = 0;
|
|
132
|
+
for (const list of listsData.lists) {
|
|
133
|
+
const tasksUrl = `https://api.clickup.com/api/v2/list/${list.id}/task?statuses[]=to%20do`;
|
|
134
|
+
const tasksData = await fetchJson(tasksUrl, headers);
|
|
135
|
+
for (const task of tasksData.tasks) {
|
|
136
|
+
if (!todoContent.includes(`<!-- cu:${task.id} -->`)) {
|
|
137
|
+
newCount++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return newCount;
|
|
142
|
+
}
|
|
143
|
+
function readTodoSafe(projectDir) {
|
|
144
|
+
try {
|
|
145
|
+
return readFileSync(resolve(projectDir, 'TODO.md'), 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ─── Main poll function ───
|
|
152
|
+
export async function pollForNewWork(projectDir, log) {
|
|
153
|
+
// 1. Check TODO.md first (free, instant)
|
|
154
|
+
if (checkTodoForPendingTasks(projectDir)) {
|
|
155
|
+
return { hasWork: true, source: 'todo', issueCount: 0 };
|
|
156
|
+
}
|
|
157
|
+
// 2. Detect platform and check for external issues
|
|
158
|
+
const platform = detectPlatform(projectDir);
|
|
159
|
+
if (platform.taskPlatform === 'clickup' && platform.clickupToken && platform.clickupListIds.length > 0) {
|
|
160
|
+
try {
|
|
161
|
+
const count = await pollClickUp(platform.clickupListIds[0], platform.clickupToken, projectDir);
|
|
162
|
+
if (count > 0) {
|
|
163
|
+
log.info(`Found ${count} new ClickUp task(s).`);
|
|
164
|
+
return { hasWork: true, source: 'clickup', issueCount: count };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
log.debug(`ClickUp poll failed: ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (platform.git === 'github' && platform.gitToken && platform.gitOwner) {
|
|
172
|
+
try {
|
|
173
|
+
const count = await pollGitHub(platform.gitOwner, platform.gitToken, projectDir);
|
|
174
|
+
if (count > 0) {
|
|
175
|
+
log.info(`Found ${count} new GitHub issue(s).`);
|
|
176
|
+
return { hasWork: true, source: 'github', issueCount: count };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
log.debug(`GitHub poll failed: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (platform.git === 'gitlab' && platform.gitToken && platform.gitOwner) {
|
|
184
|
+
try {
|
|
185
|
+
const count = await pollGitLab(platform.gitOwner, platform.gitToken, projectDir);
|
|
186
|
+
if (count > 0) {
|
|
187
|
+
log.info(`Found ${count} new GitLab issue(s).`);
|
|
188
|
+
return { hasWork: true, source: 'gitlab', issueCount: count };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
log.debug(`GitLab poll failed: ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return { hasWork: false, source: null, issueCount: 0 };
|
|
196
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { checkTodoForPendingTasks, extractGitHubOwnerRepo, extractGitLabProject, detectPlatform, pollForNewWork, } from './idle-poll.mjs';
|
|
6
|
+
function makeTmpDir() {
|
|
7
|
+
return mkdtempSync(join(tmpdir(), 'idle-poll-test-'));
|
|
8
|
+
}
|
|
9
|
+
function makeLogger() {
|
|
10
|
+
return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
11
|
+
}
|
|
12
|
+
describe('checkTodoForPendingTasks', () => {
|
|
13
|
+
it('returns true when TODO.md has unchecked tasks', () => {
|
|
14
|
+
const dir = makeTmpDir();
|
|
15
|
+
writeFileSync(join(dir, 'TODO.md'), '## Phase 9\n- [ ] New task\n- [x] Done\n');
|
|
16
|
+
expect(checkTodoForPendingTasks(dir)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it('returns false when all tasks are done', () => {
|
|
19
|
+
const dir = makeTmpDir();
|
|
20
|
+
writeFileSync(join(dir, 'TODO.md'), '- [x] Done\n- [~] Skipped\n');
|
|
21
|
+
expect(checkTodoForPendingTasks(dir)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
it('returns false when no TODO.md exists', () => {
|
|
24
|
+
const dir = makeTmpDir();
|
|
25
|
+
expect(checkTodoForPendingTasks(dir)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('extractGitHubOwnerRepo', () => {
|
|
29
|
+
it('extracts from HTTPS URL', () => {
|
|
30
|
+
expect(extractGitHubOwnerRepo('https://github.com/owner/repo.git')).toBe('owner/repo');
|
|
31
|
+
});
|
|
32
|
+
it('extracts from HTTPS URL without .git', () => {
|
|
33
|
+
expect(extractGitHubOwnerRepo('https://github.com/owner/repo')).toBe('owner/repo');
|
|
34
|
+
});
|
|
35
|
+
it('extracts from SSH URL', () => {
|
|
36
|
+
expect(extractGitHubOwnerRepo('git@github.com:owner/repo.git')).toBe('owner/repo');
|
|
37
|
+
});
|
|
38
|
+
it('returns null for non-GitHub URL', () => {
|
|
39
|
+
expect(extractGitHubOwnerRepo('https://gitlab.com/group/project.git')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('extractGitLabProject', () => {
|
|
43
|
+
it('extracts from HTTPS URL', () => {
|
|
44
|
+
expect(extractGitLabProject('https://gitlab.com/group/project.git')).toBe('group/project');
|
|
45
|
+
});
|
|
46
|
+
it('extracts nested group', () => {
|
|
47
|
+
expect(extractGitLabProject('https://gitlab.com/group/sub/project.git')).toBe('group/sub/project');
|
|
48
|
+
});
|
|
49
|
+
it('extracts from SSH URL', () => {
|
|
50
|
+
expect(extractGitLabProject('git@gitlab.com:group/project.git')).toBe('group/project');
|
|
51
|
+
});
|
|
52
|
+
it('extracts from self-hosted GitLab', () => {
|
|
53
|
+
expect(extractGitLabProject('https://gitlab.example.com/team/repo.git')).toBe('team/repo');
|
|
54
|
+
});
|
|
55
|
+
it('returns null for non-GitLab URL', () => {
|
|
56
|
+
expect(extractGitLabProject('https://github.com/owner/repo.git')).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('detectPlatform', () => {
|
|
60
|
+
it('detects ClickUp override from CLAUDE.md', () => {
|
|
61
|
+
const dir = makeTmpDir();
|
|
62
|
+
// No git repo, but CLAUDE.md has ClickUp config
|
|
63
|
+
writeFileSync(join(dir, 'CLAUDE.md'), '## Tech Stack\n- TaskPlatform: clickup\n- ClickUp Space ID: 12345\n');
|
|
64
|
+
const config = detectPlatform(dir);
|
|
65
|
+
expect(config.taskPlatform).toBe('clickup');
|
|
66
|
+
expect(config.clickupListIds).toContain('12345');
|
|
67
|
+
});
|
|
68
|
+
it('defaults to null when no remote and no CLAUDE.md', () => {
|
|
69
|
+
const dir = makeTmpDir();
|
|
70
|
+
const config = detectPlatform(dir);
|
|
71
|
+
expect(config.git).toBeNull();
|
|
72
|
+
expect(config.taskPlatform).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('pollForNewWork', () => {
|
|
76
|
+
it('returns hasWork=true from TODO.md without hitting API', async () => {
|
|
77
|
+
const dir = makeTmpDir();
|
|
78
|
+
writeFileSync(join(dir, 'TODO.md'), '- [ ] New task\n');
|
|
79
|
+
const log = makeLogger();
|
|
80
|
+
const result = await pollForNewWork(dir, log);
|
|
81
|
+
expect(result.hasWork).toBe(true);
|
|
82
|
+
expect(result.source).toBe('todo');
|
|
83
|
+
});
|
|
84
|
+
it('returns hasWork=false when no work anywhere', async () => {
|
|
85
|
+
const dir = makeTmpDir();
|
|
86
|
+
writeFileSync(join(dir, 'TODO.md'), '- [x] All done\n');
|
|
87
|
+
const log = makeLogger();
|
|
88
|
+
const result = await pollForNewWork(dir, log);
|
|
89
|
+
expect(result.hasWork).toBe(false);
|
|
90
|
+
expect(result.source).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -166,11 +166,26 @@ export function readMemory(projectDir) {
|
|
|
166
166
|
return null;
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
-
export function
|
|
169
|
+
export function hasPendingTasks(projectDir) {
|
|
170
|
+
try {
|
|
171
|
+
const todo = readFileSync(resolve(projectDir, 'TODO.md'), 'utf-8');
|
|
172
|
+
return /^- \[ \] /m.test(todo);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function isProjectComplete(memory, projectDir) {
|
|
170
179
|
if (!memory)
|
|
171
180
|
return false;
|
|
172
181
|
const lower = memory.toLowerCase();
|
|
173
|
-
|
|
182
|
+
const markedComplete = lower.includes('project complete') || lower.includes('status: complete');
|
|
183
|
+
if (!markedComplete)
|
|
184
|
+
return false;
|
|
185
|
+
// If TODO.md has unchecked tasks, project is NOT complete — user added new work
|
|
186
|
+
if (projectDir && hasPendingTasks(projectDir))
|
|
187
|
+
return false;
|
|
188
|
+
return true;
|
|
174
189
|
}
|
|
175
190
|
export function getMemoryField(memory, field) {
|
|
176
191
|
if (!memory)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { formatDuration, sleep, getMemoryField, getCurrentTask, getCurrentPhase, isProjectComplete, printSummary, parseUsageLimitResetMs } from './utils.mjs';
|
|
2
|
+
import { formatDuration, sleep, getMemoryField, getCurrentTask, getCurrentPhase, isProjectComplete, hasPendingTasks, printSummary, parseUsageLimitResetMs } from './utils.mjs';
|
|
3
3
|
describe('formatDuration', () => {
|
|
4
4
|
it('formats seconds', () => {
|
|
5
5
|
expect(formatDuration(5000)).toBe('5s');
|
|
@@ -82,7 +82,7 @@ describe('getCurrentPhase', () => {
|
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
describe('isProjectComplete', () => {
|
|
85
|
-
it('detects project complete', () => {
|
|
85
|
+
it('detects project complete (no projectDir)', () => {
|
|
86
86
|
expect(isProjectComplete('## Status\nProject Complete')).toBe(true);
|
|
87
87
|
expect(isProjectComplete('status: complete')).toBe(true);
|
|
88
88
|
});
|
|
@@ -92,6 +92,54 @@ describe('isProjectComplete', () => {
|
|
|
92
92
|
it('returns false for active project', () => {
|
|
93
93
|
expect(isProjectComplete('## Status\nIn progress')).toBe(false);
|
|
94
94
|
});
|
|
95
|
+
it('returns false when marked complete but TODO.md has pending tasks', () => {
|
|
96
|
+
const { mkdtempSync, writeFileSync } = require('node:fs');
|
|
97
|
+
const { join } = require('node:path');
|
|
98
|
+
const { tmpdir } = require('node:os');
|
|
99
|
+
const dir = mkdtempSync(join(tmpdir(), 'claude-test-'));
|
|
100
|
+
writeFileSync(join(dir, 'TODO.md'), '## Phase 9\n- [ ] New task\n- [x] Done task\n');
|
|
101
|
+
expect(isProjectComplete('## Status\nProject Complete', dir)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it('returns true when marked complete and TODO.md has no pending tasks', () => {
|
|
104
|
+
const { mkdtempSync, writeFileSync } = require('node:fs');
|
|
105
|
+
const { join } = require('node:path');
|
|
106
|
+
const { tmpdir } = require('node:os');
|
|
107
|
+
const dir = mkdtempSync(join(tmpdir(), 'claude-test-'));
|
|
108
|
+
writeFileSync(join(dir, 'TODO.md'), '## Phase 8\n- [x] Done task\n- [~] Skipped task\n');
|
|
109
|
+
expect(isProjectComplete('## Status\nProject Complete', dir)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it('returns true when marked complete and TODO.md does not exist', () => {
|
|
112
|
+
const { mkdtempSync } = require('node:fs');
|
|
113
|
+
const { join } = require('node:path');
|
|
114
|
+
const { tmpdir } = require('node:os');
|
|
115
|
+
const dir = mkdtempSync(join(tmpdir(), 'claude-test-'));
|
|
116
|
+
expect(isProjectComplete('## Status\nProject Complete', dir)).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('hasPendingTasks', () => {
|
|
120
|
+
it('detects unchecked tasks', () => {
|
|
121
|
+
const { mkdtempSync, writeFileSync } = require('node:fs');
|
|
122
|
+
const { join } = require('node:path');
|
|
123
|
+
const { tmpdir } = require('node:os');
|
|
124
|
+
const dir = mkdtempSync(join(tmpdir(), 'claude-test-'));
|
|
125
|
+
writeFileSync(join(dir, 'TODO.md'), '- [ ] Pending\n- [x] Done\n');
|
|
126
|
+
expect(hasPendingTasks(dir)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
it('returns false when all tasks done', () => {
|
|
129
|
+
const { mkdtempSync, writeFileSync } = require('node:fs');
|
|
130
|
+
const { join } = require('node:path');
|
|
131
|
+
const { tmpdir } = require('node:os');
|
|
132
|
+
const dir = mkdtempSync(join(tmpdir(), 'claude-test-'));
|
|
133
|
+
writeFileSync(join(dir, 'TODO.md'), '- [x] Done\n- [~] Skipped\n');
|
|
134
|
+
expect(hasPendingTasks(dir)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
it('returns false when no TODO.md', () => {
|
|
137
|
+
const { mkdtempSync } = require('node:fs');
|
|
138
|
+
const { join } = require('node:path');
|
|
139
|
+
const { tmpdir } = require('node:os');
|
|
140
|
+
const dir = mkdtempSync(join(tmpdir(), 'claude-test-'));
|
|
141
|
+
expect(hasPendingTasks(dir)).toBe(false);
|
|
142
|
+
});
|
|
95
143
|
});
|
|
96
144
|
describe('parseUsageLimitResetMs', () => {
|
|
97
145
|
it('parses "resets 6pm (UTC)"', () => {
|
|
@@ -79,6 +79,27 @@ At the beginning of EVERY session (including every Ralph Loop iteration):
|
|
|
79
79
|
- Read MEMORY.md — find where you left off, what is done, what is next, what phase you're in
|
|
80
80
|
- **Load `.env`**: If `.env` exists in the project root, source it to load tokens (`GITLAB_TOKEN`, `GH_TOKEN`, `CLICKUP_API_TOKEN`, etc.): `set -a && [ -f .env ] && . ./.env && set +a`
|
|
81
81
|
|
|
82
|
+
### 1b. Detect resumed development on completed project
|
|
83
|
+
If MEMORY.md indicates "Project complete" (Current Phase contains "PROJECT COMPLETE" or Next says "project complete"):
|
|
84
|
+
|
|
85
|
+
**Check for new work from two sources:**
|
|
86
|
+
|
|
87
|
+
1. **TODO.md** — check for unchecked tasks (`- [ ]`). User may have added new phases/tasks directly.
|
|
88
|
+
2. **External issues** (if git integration active) — delegate to `devops-integrator`: "Run external issue intake — check for new issues on GitHub/GitLab/ClickUp that are not yet in TODO.md. Ingest bugs and feature requests." This catches issues created by the user or team members on the platform while the project was "complete".
|
|
89
|
+
|
|
90
|
+
**If new tasks are found** (from either source):
|
|
91
|
+
- Reset MEMORY.md for the new phase:
|
|
92
|
+
1. Find the first unchecked task's phase heading in TODO.md (e.g., `## Phase 9 — ...`)
|
|
93
|
+
2. Update MEMORY.md `Current Phase` to that phase name
|
|
94
|
+
3. Update `Next` to the first unchecked task
|
|
95
|
+
4. Clear `Current Task`, `Current Step`, `Current Worktree` (set to `(none)`)
|
|
96
|
+
5. Reset `Iterations This Session` to `0` and `Complexity This Session` to `0`
|
|
97
|
+
6. Add a note: "Resumed development — new tasks detected after previous completion (v1.x.x)"
|
|
98
|
+
7. **Do NOT delete** the `Done` section or `Release History` — previous work is preserved
|
|
99
|
+
- Then continue with the normal health check (step 2+). The new tasks will be picked up in STEP 1.
|
|
100
|
+
|
|
101
|
+
**If no new tasks found** (TODO.md has no `[ ]` and no external issues ingested) → project is truly complete, end session.
|
|
102
|
+
|
|
82
103
|
### 2. Resolve app names and package manager
|
|
83
104
|
- Read CLAUDE.md — find app name(s) and package manager from the Architecture/Tech Stack section
|
|
84
105
|
- Store app names for use in build/test/lint commands:
|
|
@@ -423,7 +444,7 @@ To determine if a task is frontend, backend, or fullstack, use this heuristic:
|
|
|
423
444
|
- The FULL architect plan from STEP 2 (copy it verbatim into the prompt)
|
|
424
445
|
- Any additional context: design system (UX.md), Figma references, existing code patterns
|
|
425
446
|
- Explicit instruction: "Implement EXACTLY per this plan. Production code, not prototypes. Stay within SCOPE — do not gold-plate."
|
|
426
|
-
- For UI: if Figma MCP is available and
|
|
447
|
+
- For UI: if Figma MCP is available and CLAUDE.md contains a `<!-- FIGMA_URL: ... -->`, include: "Figma designs are available at [URL]. Use `/figma:implement-design` for components with Figma designs — find the relevant frames in the Figma file. Otherwise use UX.md spec or `/frontend-design` skill."
|
|
427
448
|
- **Include these rules in the implementation prompt:**
|
|
428
449
|
- **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:
|
|
429
450
|
- **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`. After workspace creation, run `nx g @cibule/devkit:preset {orgName}` to initialize onion architecture layers.
|
|
@@ -26,7 +26,8 @@ Have a natural conversation with the user about their project. The user does NOT
|
|
|
26
26
|
If yes, follow up immediately:
|
|
27
27
|
- **Which registry?** (npmjs.com / GitHub Packages / GitLab Packages / private)
|
|
28
28
|
- **Public or private?** (`--access public` vs `--access restricted`)
|
|
29
|
-
7. **
|
|
29
|
+
7. **Do you have designs in Figma?** (If yes: "Paste the Figma file URL — I'll set it up so agents can implement directly from the designs.")
|
|
30
|
+
8. **Anything else important?** Special constraints, integrations, existing work, design files, etc.
|
|
30
31
|
|
|
31
32
|
Do NOT ask technical questions (runtime, database, package manager, deployment target, architecture). Those decisions belong to architect agents during development planning.
|
|
32
33
|
|
|
@@ -47,9 +48,35 @@ After the conversation, scan what already exists. Do NOT create or scaffold anyt
|
|
|
47
48
|
- List `apps/` and `libs/` structure if they exist
|
|
48
49
|
- Check for existing plan/design/UX files (TODO.md, PLAN.md, PRODUCT.md, UX.md)
|
|
49
50
|
- Check for existing `.claude/profiles/frontend.md` (may already exist if user set it up manually)
|
|
50
|
-
- Check if Figma MCP server is available
|
|
51
|
+
- Check if Figma MCP server is available (try listing MCP tools — if figma tools exist, it's configured)
|
|
51
52
|
- `git log --oneline -10` — recent history
|
|
52
53
|
|
|
54
|
+
### Step 2a: Figma Setup (if question 7 = yes)
|
|
55
|
+
|
|
56
|
+
If the user provided a Figma URL:
|
|
57
|
+
|
|
58
|
+
1. **Check if Figma MCP is already configured** — try using a Figma MCP tool. If it works, skip to step 3.
|
|
59
|
+
2. **If not configured**, guide the user:
|
|
60
|
+
- "To connect Figma, I need a Figma API token. Create one at: https://www.figma.com/developers/api#access-tokens (Settings → Security → Personal access tokens → Generate new token)."
|
|
61
|
+
- Ask the user to paste the token directly as a plain text reply.
|
|
62
|
+
- Configure the MCP server by writing to `.claude/settings.json` in the project:
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"figma": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "figma-developer-mcp", "--stdio"],
|
|
69
|
+
"env": { "FIGMA_API_KEY": "<pasted-token>" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
- **IMPORTANT**: Add `.claude/settings.json` to `.gitignore` — it contains the API token and MUST NOT be committed.
|
|
75
|
+
- Verify the MCP server works by attempting to use a Figma tool.
|
|
76
|
+
3. **Store the Figma URL** — save it for Step 4 (CLAUDE.md generation). The URL will be written into CLAUDE.md so agents know where to find designs.
|
|
77
|
+
|
|
78
|
+
**For Docker / autonomous mode**: The Figma API token should be passed as `FIGMA_API_KEY` environment variable. Add to `docker-compose.yml` or `.env` (Docker reads `.env` automatically). The MCP server config in `.claude/settings.json` can reference `${FIGMA_API_KEY}` env var instead of hardcoding the token.
|
|
79
|
+
|
|
53
80
|
**Validate agent availability:**
|
|
54
81
|
- Verify `.claude/agents/` directory exists and contains all required agents:
|
|
55
82
|
`orchestrator.md`, `ui-engineer.md`, `backend-ts-architect.md`, `senior-code-reviewer.md`,
|
|
@@ -145,6 +172,7 @@ Read the template from `.claude/templates/claude-md.md`.
|
|
|
145
172
|
Create a **minimal** CLAUDE.md in the project root by filling what is KNOWN from the conversation and codebase:
|
|
146
173
|
- The template content starts after the `---` separator line — copy everything below it
|
|
147
174
|
- Fill in: project name, description, workflow (solo/team)
|
|
175
|
+
- If the user provided a Figma URL (question 7), fill in the `<!-- FIGMA_URL: -->` placeholder in the Figma MCP section: `<!-- FIGMA_URL: https://www.figma.com/design/... -->`
|
|
148
176
|
- 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
177
|
- Fill in Tech Stack ONLY for what is already detected in the codebase (existing package.json, nx.json, etc.)
|
|
150
178
|
- **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.
|
|
@@ -91,7 +91,7 @@ Write clean, elegant code that is easy to test and reason about:
|
|
|
91
91
|
|
|
92
92
|
## Design Implementation
|
|
93
93
|
|
|
94
|
-
- If Figma MCP is available and design exists: implement from Figma with 1:1 fidelity
|
|
94
|
+
- If Figma MCP is available and design exists: implement from Figma with 1:1 fidelity. Find the Figma project URL in CLAUDE.md (`<!-- FIGMA_URL: ... -->`) and use `/figma:implement-design` with the specific frame/component URL.
|
|
95
95
|
- If UX.md exists: follow its design tokens, typography, colors, breakpoints
|
|
96
96
|
- If neither: use `/frontend-design` skill for high-quality UI generation
|
|
97
97
|
- Always use @themecraft generated SCSS variables (`colors.$token`, `sizes.$token`, `@include typography.level-name`) — no hardcoded color/size values, no direct `color-var()`/`size-var()` calls
|
|
@@ -391,8 +391,10 @@ Install these before starting autonomous development:
|
|
|
391
391
|
|
|
392
392
|
- **Figma MCP** (optional) — for implementing designs from Figma files
|
|
393
393
|
- Enables: `/figma:implement-design`, `/figma:code-connect-components`
|
|
394
|
-
- Install:
|
|
394
|
+
- Install: `claude mcp add figma -- npx -y figma-developer-mcp --stdio`, then set `FIGMA_API_KEY` in MCP server env (see setup below)
|
|
395
395
|
- If not available, use `/frontend-design` skill or UX.md spec for UI implementation
|
|
396
|
+
- **Figma Project URL**: <!-- FIGMA_URL: --> (set during project initialization or manually)
|
|
397
|
+
- When implementing UI tasks, agents use this URL to find the relevant design frames
|
|
396
398
|
|
|
397
399
|
- **Playwright MCP** (`@anthropic/plugin-playwright`) — browser automation for visual verification
|
|
398
400
|
- Enables: navigating to dev server, taking screenshots, clicking, filling forms
|