create-claude-workspace 1.1.152 → 2.1.0
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/README.md +33 -1
- package/dist/index.js +29 -56
- package/dist/scheduler/agents/health-checker.mjs +98 -0
- package/dist/scheduler/agents/health-checker.spec.js +143 -0
- package/dist/scheduler/agents/orchestrator.mjs +149 -0
- package/dist/scheduler/agents/orchestrator.spec.js +87 -0
- package/dist/scheduler/agents/prompt-builder.mjs +204 -0
- package/dist/scheduler/agents/prompt-builder.spec.js +240 -0
- package/dist/scheduler/agents/worker-pool.mjs +137 -0
- package/dist/scheduler/agents/worker-pool.spec.js +45 -0
- package/dist/scheduler/git/ci-watcher.mjs +93 -0
- package/dist/scheduler/git/ci-watcher.spec.js +35 -0
- package/dist/scheduler/git/manager.mjs +228 -0
- package/dist/scheduler/git/manager.spec.js +198 -0
- package/dist/scheduler/git/release.mjs +117 -0
- package/dist/scheduler/git/release.spec.js +175 -0
- package/dist/scheduler/index.mjs +309 -0
- package/dist/scheduler/index.spec.js +72 -0
- package/dist/scheduler/integration.spec.js +289 -0
- package/dist/scheduler/loop.mjs +435 -0
- package/dist/scheduler/loop.spec.js +139 -0
- package/dist/scheduler/state/session.mjs +14 -0
- package/dist/scheduler/state/session.spec.js +36 -0
- package/dist/scheduler/state/state.mjs +102 -0
- package/dist/scheduler/state/state.spec.js +175 -0
- package/dist/scheduler/tasks/inbox.mjs +98 -0
- package/dist/scheduler/tasks/inbox.spec.js +168 -0
- package/dist/scheduler/tasks/parser.mjs +228 -0
- package/dist/scheduler/tasks/parser.spec.js +303 -0
- package/dist/scheduler/tasks/queue.mjs +152 -0
- package/dist/scheduler/tasks/queue.spec.js +223 -0
- package/dist/scheduler/types.mjs +20 -0
- package/dist/scheduler/util/memory.mjs +126 -0
- package/dist/scheduler/util/memory.spec.js +165 -0
- package/dist/template/.claude/{profiles/angular.md → agents/angular-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/react.md → agents/react-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/svelte.md → agents/svelte-engineer.md} +9 -4
- package/dist/template/.claude/{profiles/vue.md → agents/vue-engineer.md} +9 -4
- package/package.json +3 -4
- package/dist/scripts/autonomous.mjs +0 -493
- package/dist/scripts/autonomous.spec.js +0 -46
- package/dist/scripts/docker-run.mjs +0 -462
- package/dist/scripts/integration.spec.js +0 -108
- package/dist/scripts/lib/formatter.mjs +0 -309
- package/dist/scripts/lib/formatter.spec.js +0 -262
- package/dist/scripts/lib/state.mjs +0 -44
- package/dist/scripts/lib/state.spec.js +0 -59
- package/dist/template/.claude/docker/.dockerignore +0 -8
- package/dist/template/.claude/docker/Dockerfile +0 -54
- package/dist/template/.claude/docker/docker-compose.yml +0 -22
- package/dist/template/.claude/docker/docker-entrypoint.sh +0 -101
- /package/dist/{scripts/lib/types.mjs → scheduler/shared-types.mjs} +0 -0
- /package/dist/{scripts/lib → scheduler/ui}/tui.mjs +0 -0
- /package/dist/{scripts/lib → scheduler/ui}/tui.spec.js +0 -0
- /package/dist/{scripts/lib → scheduler/util}/idle-poll.mjs +0 -0
- /package/dist/{scripts/lib → scheduler/util}/idle-poll.spec.js +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildGraph, getParallelBatches, isPhaseComplete, getNextPhase, isProjectComplete, getBlockedTasks } from './queue.mjs';
|
|
3
|
+
import { parseTodoMd } from './parser.mjs';
|
|
4
|
+
// ─── Fixtures ───
|
|
5
|
+
const MULTI_PHASE_TODO = `# TODO.md
|
|
6
|
+
|
|
7
|
+
## Phase 0: Foundation
|
|
8
|
+
|
|
9
|
+
- [x] **Init workspace** — scaffolding
|
|
10
|
+
- Type: fullstack
|
|
11
|
+
- Complexity: S
|
|
12
|
+
- Depends on: nothing
|
|
13
|
+
|
|
14
|
+
- [ ] **Configure ESLint** — code quality
|
|
15
|
+
- Type: fullstack
|
|
16
|
+
- Complexity: S
|
|
17
|
+
- Depends on: nothing
|
|
18
|
+
|
|
19
|
+
## Phase 1: Auth
|
|
20
|
+
|
|
21
|
+
- [ ] **Auth types** — interfaces
|
|
22
|
+
- Type: backend
|
|
23
|
+
- Complexity: S
|
|
24
|
+
- Depends on: Phase 0
|
|
25
|
+
|
|
26
|
+
- [ ] **Login API** — endpoint
|
|
27
|
+
- Type: backend
|
|
28
|
+
- Complexity: M
|
|
29
|
+
- Depends on: Auth types
|
|
30
|
+
|
|
31
|
+
- [ ] **Login form** — UI
|
|
32
|
+
- Type: frontend
|
|
33
|
+
- Complexity: S
|
|
34
|
+
- Depends on: Auth types
|
|
35
|
+
|
|
36
|
+
## Phase 2: Dashboard
|
|
37
|
+
|
|
38
|
+
- [ ] **Dashboard layout** — grid
|
|
39
|
+
- Type: frontend
|
|
40
|
+
- Complexity: L
|
|
41
|
+
- Depends on: Phase 1
|
|
42
|
+
`;
|
|
43
|
+
const CYCLIC_TODO = `## Phase 0: Cycle
|
|
44
|
+
|
|
45
|
+
- [ ] **Task A** — first
|
|
46
|
+
- Type: backend
|
|
47
|
+
- Complexity: S
|
|
48
|
+
- Depends on: Task C
|
|
49
|
+
|
|
50
|
+
- [ ] **Task B** — second
|
|
51
|
+
- Type: backend
|
|
52
|
+
- Complexity: S
|
|
53
|
+
- Depends on: Task A
|
|
54
|
+
|
|
55
|
+
- [ ] **Task C** — third
|
|
56
|
+
- Type: backend
|
|
57
|
+
- Complexity: S
|
|
58
|
+
- Depends on: Task B
|
|
59
|
+
`;
|
|
60
|
+
const KIT_UPGRADE_TODO = `## Phase 1: Feature
|
|
61
|
+
|
|
62
|
+
- [ ] **Upgrade sharp** — major version
|
|
63
|
+
- Type: backend
|
|
64
|
+
- Complexity: S
|
|
65
|
+
- Kit-Upgrade: v2.0
|
|
66
|
+
|
|
67
|
+
- [ ] **Feature A** — new feature
|
|
68
|
+
- Type: backend
|
|
69
|
+
- Complexity: M
|
|
70
|
+
- Depends on: nothing
|
|
71
|
+
`;
|
|
72
|
+
// ─── buildGraph ───
|
|
73
|
+
describe('buildGraph', () => {
|
|
74
|
+
it('builds graph from parsed tasks', () => {
|
|
75
|
+
const tasks = parseTodoMd(MULTI_PHASE_TODO);
|
|
76
|
+
const graph = buildGraph(tasks);
|
|
77
|
+
expect(graph.tasks.size).toBe(6);
|
|
78
|
+
});
|
|
79
|
+
it('returns runnable tasks (deps satisfied)', () => {
|
|
80
|
+
const tasks = parseTodoMd(MULTI_PHASE_TODO);
|
|
81
|
+
const graph = buildGraph(tasks);
|
|
82
|
+
const runnable = graph.runnable();
|
|
83
|
+
// "Init workspace" is done, "Configure ESLint" has no deps → runnable
|
|
84
|
+
expect(runnable.map(t => t.title)).toContain('Configure ESLint');
|
|
85
|
+
// "Auth types" depends on Phase 0 (Init workspace is done, ESLint is not) → blocked
|
|
86
|
+
expect(runnable.map(t => t.title)).not.toContain('Auth types');
|
|
87
|
+
});
|
|
88
|
+
it('detects no cycle in valid graph', () => {
|
|
89
|
+
const tasks = parseTodoMd(MULTI_PHASE_TODO);
|
|
90
|
+
const graph = buildGraph(tasks);
|
|
91
|
+
expect(graph.findCycle()).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('cycle detection', () => {
|
|
95
|
+
it('detects cycle in dependency graph', () => {
|
|
96
|
+
const tasks = parseTodoMd(CYCLIC_TODO);
|
|
97
|
+
const graph = buildGraph(tasks);
|
|
98
|
+
const cycle = graph.findCycle();
|
|
99
|
+
expect(cycle).not.toBeNull();
|
|
100
|
+
expect(cycle.length).toBeGreaterThan(1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('kit-upgrade priority', () => {
|
|
104
|
+
it('sorts kit-upgrade tasks before regular tasks', () => {
|
|
105
|
+
const tasks = parseTodoMd(KIT_UPGRADE_TODO);
|
|
106
|
+
const graph = buildGraph(tasks);
|
|
107
|
+
const runnable = graph.runnable();
|
|
108
|
+
expect(runnable.length).toBe(2);
|
|
109
|
+
expect(runnable[0].kitUpgrade).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('complexity sorting', () => {
|
|
113
|
+
it('sorts S before M before L within same phase', () => {
|
|
114
|
+
const todo = `## Phase 0: X
|
|
115
|
+
- [ ] **Large task** — big
|
|
116
|
+
- Type: backend
|
|
117
|
+
- Complexity: L
|
|
118
|
+
- Depends on: nothing
|
|
119
|
+
- [ ] **Small task** — tiny
|
|
120
|
+
- Type: backend
|
|
121
|
+
- Complexity: S
|
|
122
|
+
- Depends on: nothing
|
|
123
|
+
- [ ] **Medium task** — mid
|
|
124
|
+
- Type: backend
|
|
125
|
+
- Complexity: M
|
|
126
|
+
- Depends on: nothing
|
|
127
|
+
`;
|
|
128
|
+
const tasks = parseTodoMd(todo);
|
|
129
|
+
const graph = buildGraph(tasks);
|
|
130
|
+
const runnable = graph.runnable();
|
|
131
|
+
expect(runnable[0].complexity).toBe('S');
|
|
132
|
+
expect(runnable[1].complexity).toBe('M');
|
|
133
|
+
expect(runnable[2].complexity).toBe('L');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
// ─── getParallelBatches ───
|
|
137
|
+
describe('getParallelBatches', () => {
|
|
138
|
+
it('returns empty for empty input', () => {
|
|
139
|
+
expect(getParallelBatches([])).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
it('returns single batch for single task', () => {
|
|
142
|
+
const tasks = parseTodoMd(`## Phase 0: X\n- [ ] **Solo** — one\n - Type: backend\n - Complexity: S\n`);
|
|
143
|
+
const graph = buildGraph(tasks);
|
|
144
|
+
const batches = getParallelBatches(graph.runnable());
|
|
145
|
+
expect(batches).toHaveLength(1);
|
|
146
|
+
expect(batches[0]).toHaveLength(1);
|
|
147
|
+
});
|
|
148
|
+
it('batches independent tasks from same phase', () => {
|
|
149
|
+
const todo = `## Phase 0: Setup
|
|
150
|
+
- [ ] **Task A** — first
|
|
151
|
+
- Type: backend
|
|
152
|
+
- Complexity: S
|
|
153
|
+
- Depends on: nothing
|
|
154
|
+
- [ ] **Task B** — second
|
|
155
|
+
- Type: frontend
|
|
156
|
+
- Complexity: S
|
|
157
|
+
- Depends on: nothing
|
|
158
|
+
`;
|
|
159
|
+
const tasks = parseTodoMd(todo);
|
|
160
|
+
const graph = buildGraph(tasks);
|
|
161
|
+
const batches = getParallelBatches(graph.runnable());
|
|
162
|
+
expect(batches).toHaveLength(1);
|
|
163
|
+
expect(batches[0]).toHaveLength(2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
// ─── Phase helpers ───
|
|
167
|
+
describe('isPhaseComplete', () => {
|
|
168
|
+
it('returns true when all tasks in phase are done/skipped', () => {
|
|
169
|
+
const tasks = [
|
|
170
|
+
{ id: 'p0-1', title: 'A', phase: 0, type: 'backend', complexity: 'S', status: 'done', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 1, changelog: 'added' },
|
|
171
|
+
{ id: 'p0-2', title: 'B', phase: 0, type: 'backend', complexity: 'S', status: 'skipped', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 2, changelog: 'none' },
|
|
172
|
+
];
|
|
173
|
+
expect(isPhaseComplete(tasks, 0)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
it('returns false when a task is still todo', () => {
|
|
176
|
+
const tasks = [
|
|
177
|
+
{ id: 'p0-1', title: 'A', phase: 0, type: 'backend', complexity: 'S', status: 'done', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 1, changelog: 'added' },
|
|
178
|
+
{ id: 'p0-2', title: 'B', phase: 0, type: 'backend', complexity: 'S', status: 'todo', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 2, changelog: 'added' },
|
|
179
|
+
];
|
|
180
|
+
expect(isPhaseComplete(tasks, 0)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
it('returns false for nonexistent phase', () => {
|
|
183
|
+
expect(isPhaseComplete([], 99)).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe('getNextPhase', () => {
|
|
187
|
+
it('returns lowest phase with todo tasks', () => {
|
|
188
|
+
const tasks = parseTodoMd(MULTI_PHASE_TODO);
|
|
189
|
+
expect(getNextPhase(tasks)).toBe(0); // ESLint is still todo
|
|
190
|
+
});
|
|
191
|
+
it('returns null when all tasks are done', () => {
|
|
192
|
+
const tasks = [
|
|
193
|
+
{ id: 'p0-1', title: 'A', phase: 0, type: 'backend', complexity: 'S', status: 'done', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 1, changelog: 'added' },
|
|
194
|
+
];
|
|
195
|
+
expect(getNextPhase(tasks)).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe('isProjectComplete', () => {
|
|
199
|
+
it('returns true when all tasks are done/skipped', () => {
|
|
200
|
+
const tasks = [
|
|
201
|
+
{ id: 'p0-1', title: 'A', phase: 0, type: 'backend', complexity: 'S', status: 'done', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 1, changelog: 'added' },
|
|
202
|
+
{ id: 'p1-1', title: 'B', phase: 1, type: 'backend', complexity: 'S', status: 'skipped', dependsOn: [], issueMarker: null, kitUpgrade: false, lineNumber: 2, changelog: 'none' },
|
|
203
|
+
];
|
|
204
|
+
expect(isProjectComplete(tasks)).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
it('returns false when tasks remain', () => {
|
|
207
|
+
const tasks = parseTodoMd(MULTI_PHASE_TODO);
|
|
208
|
+
expect(isProjectComplete(tasks)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
it('returns false for empty task list', () => {
|
|
211
|
+
expect(isProjectComplete([])).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('getBlockedTasks', () => {
|
|
215
|
+
it('identifies blocked tasks', () => {
|
|
216
|
+
const tasks = parseTodoMd(MULTI_PHASE_TODO);
|
|
217
|
+
const graph = buildGraph(tasks);
|
|
218
|
+
const blocked = getBlockedTasks(graph);
|
|
219
|
+
// Auth types, Login API, Login form, Dashboard are blocked
|
|
220
|
+
expect(blocked.length).toBeGreaterThan(0);
|
|
221
|
+
expect(blocked.some(t => t.title === 'Auth types')).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// ─── Multi-agent scheduler types ───
|
|
2
|
+
export const SCHEDULER_DEFAULTS = {
|
|
3
|
+
projectDir: process.cwd(),
|
|
4
|
+
concurrency: 1,
|
|
5
|
+
maxIterations: 50,
|
|
6
|
+
maxTurns: 50,
|
|
7
|
+
skipPermissions: false,
|
|
8
|
+
resume: false,
|
|
9
|
+
resumeSession: null,
|
|
10
|
+
logFile: '.claude/scheduler/scheduler.log',
|
|
11
|
+
noPull: false,
|
|
12
|
+
noLock: false,
|
|
13
|
+
dryRun: false,
|
|
14
|
+
notifyCommand: null,
|
|
15
|
+
idlePollInterval: 5 * 60_000,
|
|
16
|
+
maxIdleTime: 0,
|
|
17
|
+
interactive: false,
|
|
18
|
+
delay: 5_000,
|
|
19
|
+
cooldown: 60_000,
|
|
20
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ─── Pure string transformations for MEMORY.md ───
|
|
2
|
+
// All functions are pure: string in → string out. No file I/O.
|
|
3
|
+
/**
|
|
4
|
+
* Read a top-level section value from MEMORY.md.
|
|
5
|
+
* Sections are h2 headings (`## Section Name`), value is everything until the next heading.
|
|
6
|
+
*/
|
|
7
|
+
export function readField(content, field) {
|
|
8
|
+
const sections = splitSections(content);
|
|
9
|
+
const section = sections.find(s => s.heading === field);
|
|
10
|
+
if (!section)
|
|
11
|
+
return null;
|
|
12
|
+
const trimmed = section.body.trim();
|
|
13
|
+
return trimmed || null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Write a top-level section value. Creates the section if it doesn't exist.
|
|
17
|
+
*/
|
|
18
|
+
export function writeField(content, field, value) {
|
|
19
|
+
const sections = splitSections(content);
|
|
20
|
+
const idx = sections.findIndex(s => s.heading === field);
|
|
21
|
+
if (idx >= 0) {
|
|
22
|
+
sections[idx] = { heading: field, body: value + '\n' };
|
|
23
|
+
return assembleSections(content, sections);
|
|
24
|
+
}
|
|
25
|
+
// Append new section
|
|
26
|
+
const trimmed = content.trimEnd();
|
|
27
|
+
return `${trimmed}\n\n## ${field}\n${value}\n`;
|
|
28
|
+
}
|
|
29
|
+
function splitSections(content) {
|
|
30
|
+
const sections = [];
|
|
31
|
+
const lines = content.split('\n');
|
|
32
|
+
let currentHeading = null;
|
|
33
|
+
let bodyLines = [];
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const match = line.match(/^## (.+)$/);
|
|
36
|
+
if (match) {
|
|
37
|
+
if (currentHeading !== null) {
|
|
38
|
+
sections.push({ heading: currentHeading, body: bodyLines.join('\n') });
|
|
39
|
+
}
|
|
40
|
+
currentHeading = match[1].trim();
|
|
41
|
+
bodyLines = [];
|
|
42
|
+
}
|
|
43
|
+
else if (currentHeading !== null) {
|
|
44
|
+
bodyLines.push(line);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (currentHeading !== null) {
|
|
48
|
+
sections.push({ heading: currentHeading, body: bodyLines.join('\n') });
|
|
49
|
+
}
|
|
50
|
+
return sections;
|
|
51
|
+
}
|
|
52
|
+
function assembleSections(original, sections) {
|
|
53
|
+
// Preserve everything before the first ## heading
|
|
54
|
+
let preamble = '';
|
|
55
|
+
if (original.startsWith('## ')) {
|
|
56
|
+
preamble = '';
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const idx = original.indexOf('\n## ');
|
|
60
|
+
preamble = idx >= 0 ? original.slice(0, idx + 1) : '';
|
|
61
|
+
}
|
|
62
|
+
const body = sections
|
|
63
|
+
.map(s => `## ${s.heading}\n${s.body}`)
|
|
64
|
+
.join('\n');
|
|
65
|
+
return preamble + body;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Append a completed task to the "Done" section.
|
|
69
|
+
*/
|
|
70
|
+
export function addDoneItem(content, taskTitle, date) {
|
|
71
|
+
const done = readField(content, 'Done');
|
|
72
|
+
if (!done || done === '(nothing yet)') {
|
|
73
|
+
return writeField(content, 'Done', `- ${taskTitle} (${date})`);
|
|
74
|
+
}
|
|
75
|
+
return writeField(content, 'Done', `${done}\n- ${taskTitle} (${date})`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Set the "Next" section to a specific task.
|
|
79
|
+
*/
|
|
80
|
+
export function setNextItem(content, taskTitle) {
|
|
81
|
+
return writeField(content, 'Next', `- ${taskTitle}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Read Session Config as key-value pairs.
|
|
85
|
+
*/
|
|
86
|
+
export function readSessionConfig(content) {
|
|
87
|
+
const section = readField(content, 'Session Config');
|
|
88
|
+
if (!section)
|
|
89
|
+
return {};
|
|
90
|
+
const config = {};
|
|
91
|
+
for (const line of section.split('\n')) {
|
|
92
|
+
const match = line.match(/^-\s*(\w[\w_\s]*\w|\w+):\s*(.+)$/);
|
|
93
|
+
if (match) {
|
|
94
|
+
config[match[1].trim()] = match[2].trim();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if the project is marked as complete.
|
|
101
|
+
*/
|
|
102
|
+
export function isProjectComplete(content) {
|
|
103
|
+
return /^Current Phase:.*PROJECT COMPLETE/mi.test(content)
|
|
104
|
+
|| /^Project_Status:\s*complete/mi.test(content)
|
|
105
|
+
|| /## Current Phase\s*\n.*PROJECT COMPLETE/mi.test(content);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read iteration count from MEMORY.md.
|
|
109
|
+
*/
|
|
110
|
+
export function readIterationCount(content) {
|
|
111
|
+
const value = readField(content, 'Iterations This Session');
|
|
112
|
+
if (!value)
|
|
113
|
+
return 0;
|
|
114
|
+
const n = parseInt(value, 10);
|
|
115
|
+
return isNaN(n) ? 0 : n;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Increment iteration count.
|
|
119
|
+
*/
|
|
120
|
+
export function incrementIterations(content) {
|
|
121
|
+
const current = readIterationCount(content);
|
|
122
|
+
return writeField(content, 'Iterations This Session', String(current + 1));
|
|
123
|
+
}
|
|
124
|
+
function escapeRegex(str) {
|
|
125
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
126
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readField, writeField, addDoneItem, setNextItem, readSessionConfig, isProjectComplete, readIterationCount, incrementIterations, } from './memory.mjs';
|
|
3
|
+
const SAMPLE_MEMORY = `# MEMORY.md
|
|
4
|
+
|
|
5
|
+
## Current Phase
|
|
6
|
+
Phase 1: User Authentication
|
|
7
|
+
|
|
8
|
+
## Current Task
|
|
9
|
+
Create auth domain types
|
|
10
|
+
|
|
11
|
+
## Current Step
|
|
12
|
+
3 — IMPLEMENT
|
|
13
|
+
|
|
14
|
+
## Current Worktree
|
|
15
|
+
.worktrees/feat/5-auth-types
|
|
16
|
+
|
|
17
|
+
## Iterations This Session
|
|
18
|
+
7
|
|
19
|
+
|
|
20
|
+
## Complexity This Session
|
|
21
|
+
12
|
|
22
|
+
|
|
23
|
+
## Session Config
|
|
24
|
+
- FRONTEND_APP: my-app
|
|
25
|
+
- BACKEND_APP: api-server
|
|
26
|
+
- Workflow: solo
|
|
27
|
+
|
|
28
|
+
## Done
|
|
29
|
+
- Set up Nx workspace (2026-03-15)
|
|
30
|
+
- Configure ESLint (2026-03-15)
|
|
31
|
+
|
|
32
|
+
## Next
|
|
33
|
+
- Create auth domain types
|
|
34
|
+
|
|
35
|
+
## Decisions
|
|
36
|
+
(none yet)
|
|
37
|
+
|
|
38
|
+
## Blockers
|
|
39
|
+
(none)
|
|
40
|
+
|
|
41
|
+
## Notes
|
|
42
|
+
- Project initialized on 2026-03-14
|
|
43
|
+
`;
|
|
44
|
+
// ─── readField ───
|
|
45
|
+
describe('readField', () => {
|
|
46
|
+
it('reads a simple field', () => {
|
|
47
|
+
expect(readField(SAMPLE_MEMORY, 'Current Task')).toBe('Create auth domain types');
|
|
48
|
+
});
|
|
49
|
+
it('reads a multi-line field', () => {
|
|
50
|
+
const done = readField(SAMPLE_MEMORY, 'Done');
|
|
51
|
+
expect(done).toContain('Set up Nx workspace');
|
|
52
|
+
expect(done).toContain('Configure ESLint');
|
|
53
|
+
});
|
|
54
|
+
it('reads a field with structured content', () => {
|
|
55
|
+
const config = readField(SAMPLE_MEMORY, 'Session Config');
|
|
56
|
+
expect(config).toContain('FRONTEND_APP: my-app');
|
|
57
|
+
});
|
|
58
|
+
it('returns null for non-existent field', () => {
|
|
59
|
+
expect(readField(SAMPLE_MEMORY, 'Non Existent')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
it('returns null for a field with empty value', () => {
|
|
62
|
+
const empty = `## Empty Field\n\n## Next Field\nvalue`;
|
|
63
|
+
expect(readField(empty, 'Empty Field')).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
it('reads the last field (no trailing heading)', () => {
|
|
66
|
+
const notes = readField(SAMPLE_MEMORY, 'Notes');
|
|
67
|
+
expect(notes).toContain('Project initialized on 2026-03-14');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ─── writeField ───
|
|
71
|
+
describe('writeField', () => {
|
|
72
|
+
it('updates an existing field', () => {
|
|
73
|
+
const updated = writeField(SAMPLE_MEMORY, 'Current Task', 'Implement login flow');
|
|
74
|
+
expect(readField(updated, 'Current Task')).toBe('Implement login flow');
|
|
75
|
+
});
|
|
76
|
+
it('preserves other fields when updating', () => {
|
|
77
|
+
const updated = writeField(SAMPLE_MEMORY, 'Current Task', 'New task');
|
|
78
|
+
expect(readField(updated, 'Current Phase')).toBe('Phase 1: User Authentication');
|
|
79
|
+
expect(readField(updated, 'Current Step')).toBe('3 — IMPLEMENT');
|
|
80
|
+
});
|
|
81
|
+
it('creates a new field when it does not exist', () => {
|
|
82
|
+
const updated = writeField(SAMPLE_MEMORY, 'Kit_Version', '1.2.3');
|
|
83
|
+
expect(readField(updated, 'Kit_Version')).toBe('1.2.3');
|
|
84
|
+
});
|
|
85
|
+
it('replaces multi-line content', () => {
|
|
86
|
+
const updated = writeField(SAMPLE_MEMORY, 'Done', '- Only this item');
|
|
87
|
+
expect(readField(updated, 'Done')).toBe('- Only this item');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
// ─── addDoneItem ───
|
|
91
|
+
describe('addDoneItem', () => {
|
|
92
|
+
it('appends to existing done list', () => {
|
|
93
|
+
const updated = addDoneItem(SAMPLE_MEMORY, 'Auth types done', '2026-03-20');
|
|
94
|
+
const done = readField(updated, 'Done');
|
|
95
|
+
expect(done).toContain('Set up Nx workspace');
|
|
96
|
+
expect(done).toContain('Auth types done (2026-03-20)');
|
|
97
|
+
});
|
|
98
|
+
it('replaces placeholder when Done is empty', () => {
|
|
99
|
+
const empty = `## Done\n(nothing yet)\n\n## Next\nfoo`;
|
|
100
|
+
const updated = addDoneItem(empty, 'First task', '2026-03-20');
|
|
101
|
+
expect(readField(updated, 'Done')).toBe('- First task (2026-03-20)');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
// ─── setNextItem ───
|
|
105
|
+
describe('setNextItem', () => {
|
|
106
|
+
it('sets the next item', () => {
|
|
107
|
+
const updated = setNextItem(SAMPLE_MEMORY, 'Implement login');
|
|
108
|
+
expect(readField(updated, 'Next')).toBe('- Implement login');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// ─── readSessionConfig ───
|
|
112
|
+
describe('readSessionConfig', () => {
|
|
113
|
+
it('parses session config into key-value pairs', () => {
|
|
114
|
+
const config = readSessionConfig(SAMPLE_MEMORY);
|
|
115
|
+
expect(config).toEqual({
|
|
116
|
+
FRONTEND_APP: 'my-app',
|
|
117
|
+
BACKEND_APP: 'api-server',
|
|
118
|
+
Workflow: 'solo',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it('returns empty object when no config section', () => {
|
|
122
|
+
expect(readSessionConfig('# Just a heading')).toEqual({});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// ─── isProjectComplete ───
|
|
126
|
+
describe('isProjectComplete', () => {
|
|
127
|
+
it('detects "PROJECT COMPLETE" in Current Phase heading style', () => {
|
|
128
|
+
const complete = writeField(SAMPLE_MEMORY, 'Current Phase', 'Phase 3: Done — PROJECT COMPLETE');
|
|
129
|
+
expect(isProjectComplete(complete)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
it('detects Project_Status: complete', () => {
|
|
132
|
+
const complete = `Project_Status: complete\n`;
|
|
133
|
+
expect(isProjectComplete(complete)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
it('returns false for in-progress project', () => {
|
|
136
|
+
expect(isProjectComplete(SAMPLE_MEMORY)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
it('returns false for empty content', () => {
|
|
139
|
+
expect(isProjectComplete('')).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// ─── readIterationCount / incrementIterations ───
|
|
143
|
+
describe('readIterationCount', () => {
|
|
144
|
+
it('reads iteration count', () => {
|
|
145
|
+
expect(readIterationCount(SAMPLE_MEMORY)).toBe(7);
|
|
146
|
+
});
|
|
147
|
+
it('returns 0 when no iterations field', () => {
|
|
148
|
+
expect(readIterationCount('# Nothing here')).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
it('returns 0 for non-numeric value', () => {
|
|
151
|
+
const bad = `## Iterations This Session\nnot a number\n`;
|
|
152
|
+
expect(readIterationCount(bad)).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('incrementIterations', () => {
|
|
156
|
+
it('increments from current value', () => {
|
|
157
|
+
const updated = incrementIterations(SAMPLE_MEMORY);
|
|
158
|
+
expect(readIterationCount(updated)).toBe(8);
|
|
159
|
+
});
|
|
160
|
+
it('increments from 0 when field missing', () => {
|
|
161
|
+
const noIter = `# MEMORY.md\n## Current Phase\nPhase 0\n`;
|
|
162
|
+
const updated = incrementIterations(noIter);
|
|
163
|
+
expect(readIterationCount(updated)).toBe(1);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: angular-engineer
|
|
3
|
+
description: Angular frontend specialist — components, signals, RxJS, SSR, testing
|
|
4
|
+
model: opus
|
|
5
|
+
steps: plan, implement, rework
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are an Angular frontend engineer. You plan, implement, and fix Angular applications.
|
|
9
|
+
Read the codebase before making changes. Follow the conventions below strictly.
|
|
2
10
|
|
|
3
|
-
> This profile is copied to `.claude/profiles/frontend.md` in the target project by the project-initializer.
|
|
4
|
-
> It is read by: `ui-engineer`, `senior-code-reviewer`, `test-engineer`.
|
|
5
|
-
> It is NOT read by: `orchestrator`, `backend-ts-architect`, `devops-integrator`, `deployment-engineer`, `product-owner`, `technical-planner`.
|
|
6
11
|
|
|
7
12
|
## Framework
|
|
8
13
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: react-engineer
|
|
3
|
+
description: React frontend specialist — components, hooks, SSR, Next.js, testing
|
|
4
|
+
model: opus
|
|
5
|
+
steps: plan, implement, rework
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a React frontend engineer. You plan, implement, and fix React applications.
|
|
9
|
+
Read the codebase before making changes. Follow the conventions below strictly.
|
|
2
10
|
|
|
3
|
-
> This profile is copied to `.claude/profiles/frontend.md` in the target project by the project-initializer.
|
|
4
|
-
> It is read by: `ui-engineer`, `senior-code-reviewer`, `test-engineer`.
|
|
5
|
-
> It is NOT read by: `orchestrator`, `backend-ts-architect`, `devops-integrator`, `deployment-engineer`, `product-owner`, `technical-planner`.
|
|
6
11
|
|
|
7
12
|
## Framework
|
|
8
13
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: svelte-engineer
|
|
3
|
+
description: Svelte frontend specialist — runes, SvelteKit, SSR, testing
|
|
4
|
+
model: opus
|
|
5
|
+
steps: plan, implement, rework
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a Svelte frontend engineer. You plan, implement, and fix Svelte/SvelteKit applications.
|
|
9
|
+
Read the codebase before making changes. Follow the conventions below strictly.
|
|
2
10
|
|
|
3
|
-
> This profile is copied to `.claude/profiles/frontend.md` in the target project by the project-initializer.
|
|
4
|
-
> It is read by: `ui-engineer`, `senior-code-reviewer`, `test-engineer`.
|
|
5
|
-
> It is NOT read by: `orchestrator`, `backend-ts-architect`, `devops-integrator`, `deployment-engineer`, `product-owner`, `technical-planner`.
|
|
6
11
|
|
|
7
12
|
## Framework
|
|
8
13
|
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: vue-engineer
|
|
3
|
+
description: Vue frontend specialist — Composition API, Pinia, Nuxt, SSR, testing
|
|
4
|
+
model: opus
|
|
5
|
+
steps: plan, implement, rework
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a Vue frontend engineer. You plan, implement, and fix Vue applications.
|
|
9
|
+
Read the codebase before making changes. Follow the conventions below strictly.
|
|
2
10
|
|
|
3
|
-
> This profile is copied to `.claude/profiles/frontend.md` in the target project by the project-initializer.
|
|
4
|
-
> It is read by: `ui-engineer`, `senior-code-reviewer`, `test-engineer`.
|
|
5
|
-
> It is NOT read by: `orchestrator`, `backend-ts-architect`, `devops-integrator`, `deployment-engineer`, `product-owner`, `technical-planner`.
|
|
6
11
|
|
|
7
12
|
## Framework
|
|
8
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-claude-workspace",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"author": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist/index.js",
|
|
22
|
-
"dist/
|
|
22
|
+
"dist/scheduler",
|
|
23
23
|
"dist/template"
|
|
24
24
|
],
|
|
25
25
|
"keywords": [
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc && node scripts/copy-templates.mjs",
|
|
37
37
|
"test": "vitest run",
|
|
38
|
-
"test:integration": "vitest run src/
|
|
39
|
-
"test:all": "vitest run --config vitest.integration.config.ts",
|
|
38
|
+
"test:integration": "vitest run src/scheduler/integration.spec.ts",
|
|
40
39
|
"prepublishOnly": "npm run build"
|
|
41
40
|
},
|
|
42
41
|
"type": "module",
|