ralphctl 0.4.6 → 0.6.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 +29 -16
- package/dist/absolute-path-WUTZQ37D.mjs +8 -0
- package/dist/chunk-6RDMCLWU.mjs +108 -0
- package/dist/chunk-HIU74KTO.mjs +1046 -0
- package/dist/chunk-S3PTDH57.mjs +78 -0
- package/dist/chunk-WV4D2CPG.mjs +26 -0
- package/dist/cli.mjs +22413 -717
- package/dist/manifest.json +24 -0
- package/dist/prompt-adapter-JQICGVX7.mjs +7 -0
- package/dist/prompts/ideate.md +3 -1
- package/dist/prompts/plan-auto.md +23 -8
- package/dist/prompts/plan-common-examples.md +3 -3
- package/dist/prompts/plan-common.md +6 -5
- package/dist/prompts/plan-interactive.md +30 -7
- package/dist/prompts/repo-onboard.md +154 -64
- package/dist/prompts/signals-task.md +3 -0
- package/dist/prompts/sprint-feedback.md +3 -0
- package/dist/prompts/task-evaluation.md +74 -53
- package/dist/prompts/task-execution.md +65 -21
- package/dist/prompts/ticket-refine.md +11 -8
- package/dist/prompts/validation-checklist.md +3 -2
- package/dist/skills/default/abstraction-first/SKILL.md +45 -0
- package/dist/skills/default/alignment/SKILL.md +46 -0
- package/dist/skills/default/iterative-review/SKILL.md +48 -0
- package/dist/skills/exec/.gitkeep +0 -0
- package/dist/skills/plan/.gitkeep +0 -0
- package/dist/skills/refine/.gitkeep +0 -0
- package/dist/storage-paths-IPNZZM5D.mjs +15 -0
- package/dist/validation-error-QT6Q7FYU.mjs +7 -0
- package/package.json +9 -4
- package/dist/add-DVPVHENV.mjs +0 -18
- package/dist/add-YVXM34RP.mjs +0 -17
- package/dist/bootstrap-FMHG6DRY.mjs +0 -11
- package/dist/chunk-747KW2RW.mjs +0 -24
- package/dist/chunk-B3RCOHW3.mjs +0 -5519
- package/dist/chunk-BSB4EDGR.mjs +0 -260
- package/dist/chunk-CBMFRQ4Y.mjs +0 -441
- package/dist/chunk-CFUVE2BP.mjs +0 -16
- package/dist/chunk-FNAAA32W.mjs +0 -103
- package/dist/chunk-GQ2WFKBN.mjs +0 -269
- package/dist/chunk-IWXBJD2D.mjs +0 -27
- package/dist/chunk-O566EEDL.mjs +0 -5542
- package/dist/chunk-OGEXYSFS.mjs +0 -228
- package/dist/chunk-PYZEQ2VK.mjs +0 -787
- package/dist/chunk-VAZ3LJBI.mjs +0 -179
- package/dist/chunk-WDMLPXOD.mjs +0 -363
- package/dist/chunk-XN2UIHBY.mjs +0 -589
- package/dist/chunk-ZLWSPLWI.mjs +0 -1117
- package/dist/create-Z635FQKO.mjs +0 -15
- package/dist/handle-23EFF3BE.mjs +0 -22
- package/dist/mount-B3MLHNVY.mjs +0 -7434
- package/dist/project-DQHF4ISP.mjs +0 -34
- package/dist/prompts/check-script-discover.md +0 -69
- package/dist/prompts/ideate-auto.md +0 -195
- package/dist/prompts/task-evaluation-resume.md +0 -41
- package/dist/resolver-OVPYVW6Q.mjs +0 -163
- package/dist/sprint-4E26AB5F.mjs +0 -38
- package/dist/start-FP7MVN5P.mjs +0 -19
package/dist/chunk-BSB4EDGR.mjs
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ProjectsSchema,
|
|
4
|
-
expandTilde,
|
|
5
|
-
fileExists,
|
|
6
|
-
generateUuid8,
|
|
7
|
-
getProjectsFilePath,
|
|
8
|
-
readValidatedJson,
|
|
9
|
-
validateProjectPath,
|
|
10
|
-
writeValidatedJson
|
|
11
|
-
} from "./chunk-WDMLPXOD.mjs";
|
|
12
|
-
import {
|
|
13
|
-
ParseError,
|
|
14
|
-
ProjectExistsError,
|
|
15
|
-
ProjectNotFoundError,
|
|
16
|
-
ValidationError
|
|
17
|
-
} from "./chunk-VAZ3LJBI.mjs";
|
|
18
|
-
|
|
19
|
-
// src/integration/persistence/project.ts
|
|
20
|
-
import { basename, resolve } from "path";
|
|
21
|
-
function migrateProjectIfNeeded(project) {
|
|
22
|
-
const id = project.id ?? generateUuid8();
|
|
23
|
-
if (project.paths && !project.repositories) {
|
|
24
|
-
return {
|
|
25
|
-
id,
|
|
26
|
-
name: project.name,
|
|
27
|
-
displayName: project.displayName,
|
|
28
|
-
repositories: project.paths.map((p) => ({
|
|
29
|
-
id: generateUuid8(),
|
|
30
|
-
name: basename(p),
|
|
31
|
-
path: resolve(expandTilde(p))
|
|
32
|
-
})),
|
|
33
|
-
description: project.description
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
if (project.repositories) {
|
|
37
|
-
return {
|
|
38
|
-
id,
|
|
39
|
-
name: project.name,
|
|
40
|
-
displayName: project.displayName,
|
|
41
|
-
repositories: project.repositories.map((r) => ({
|
|
42
|
-
id: r.id ?? generateUuid8(),
|
|
43
|
-
name: r.name,
|
|
44
|
-
path: r.path,
|
|
45
|
-
checkScript: r.checkScript,
|
|
46
|
-
checkTimeout: r.checkTimeout
|
|
47
|
-
})),
|
|
48
|
-
description: project.description
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
throw new ParseError(`Invalid project data: no paths or repositories for ${project.name}`);
|
|
52
|
-
}
|
|
53
|
-
async function listProjects() {
|
|
54
|
-
const filePath = getProjectsFilePath();
|
|
55
|
-
if (!await fileExists(filePath)) {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
const { readFile } = await import("fs/promises");
|
|
59
|
-
const content = await readFile(filePath, "utf-8");
|
|
60
|
-
const rawData = JSON.parse(content);
|
|
61
|
-
const needsMigration = rawData.some((p) => {
|
|
62
|
-
if (!p.id) return true;
|
|
63
|
-
if (p.paths && !p.repositories) return true;
|
|
64
|
-
return (p.repositories ?? []).some((r) => !r.id);
|
|
65
|
-
});
|
|
66
|
-
if (needsMigration) {
|
|
67
|
-
const migrated = rawData.map(migrateProjectIfNeeded);
|
|
68
|
-
const validated = ProjectsSchema.parse(migrated);
|
|
69
|
-
const writeResult = await writeValidatedJson(filePath, validated, ProjectsSchema);
|
|
70
|
-
if (!writeResult.ok) throw writeResult.error;
|
|
71
|
-
return validated;
|
|
72
|
-
}
|
|
73
|
-
const result = await readValidatedJson(filePath, ProjectsSchema);
|
|
74
|
-
if (!result.ok) throw result.error;
|
|
75
|
-
const projects = result.value;
|
|
76
|
-
const hasTildePaths = projects.some((p) => p.repositories.some((r) => r.path.startsWith("~")));
|
|
77
|
-
if (hasTildePaths) {
|
|
78
|
-
const corrected = projects.map((project) => ({
|
|
79
|
-
...project,
|
|
80
|
-
repositories: project.repositories.map(
|
|
81
|
-
(repo) => repo.path.startsWith("~") ? { ...repo, path: resolve(expandTilde(repo.path)) } : repo
|
|
82
|
-
)
|
|
83
|
-
}));
|
|
84
|
-
const validated = ProjectsSchema.parse(corrected);
|
|
85
|
-
const writeResult = await writeValidatedJson(filePath, validated, ProjectsSchema);
|
|
86
|
-
if (!writeResult.ok) throw writeResult.error;
|
|
87
|
-
return validated;
|
|
88
|
-
}
|
|
89
|
-
return projects;
|
|
90
|
-
}
|
|
91
|
-
async function getProject(name) {
|
|
92
|
-
const projects = await listProjects();
|
|
93
|
-
const project = projects.find((p) => p.name === name);
|
|
94
|
-
if (!project) {
|
|
95
|
-
throw new ProjectNotFoundError(name);
|
|
96
|
-
}
|
|
97
|
-
return project;
|
|
98
|
-
}
|
|
99
|
-
async function getProjectById(id) {
|
|
100
|
-
const projects = await listProjects();
|
|
101
|
-
const project = projects.find((p) => p.id === id);
|
|
102
|
-
if (!project) {
|
|
103
|
-
throw new ProjectNotFoundError(id);
|
|
104
|
-
}
|
|
105
|
-
return project;
|
|
106
|
-
}
|
|
107
|
-
async function getRepoById(repoId) {
|
|
108
|
-
const projects = await listProjects();
|
|
109
|
-
for (const project of projects) {
|
|
110
|
-
const repo = project.repositories.find((r) => r.id === repoId);
|
|
111
|
-
if (repo) return { project, repo };
|
|
112
|
-
}
|
|
113
|
-
throw new ValidationError(`Repository not found: ${repoId}`, "repoId");
|
|
114
|
-
}
|
|
115
|
-
async function resolveRepoPath(repoId) {
|
|
116
|
-
const { repo } = await getRepoById(repoId);
|
|
117
|
-
return repo.path;
|
|
118
|
-
}
|
|
119
|
-
async function projectExists(name) {
|
|
120
|
-
const projects = await listProjects();
|
|
121
|
-
return projects.some((p) => p.name === name);
|
|
122
|
-
}
|
|
123
|
-
async function createProject(input) {
|
|
124
|
-
const projects = await listProjects();
|
|
125
|
-
if (projects.some((p) => p.name === input.name)) {
|
|
126
|
-
throw new ProjectExistsError(input.name);
|
|
127
|
-
}
|
|
128
|
-
const pathErrors = [];
|
|
129
|
-
for (const repo of input.repositories) {
|
|
130
|
-
const resolved = resolve(expandTilde(repo.path));
|
|
131
|
-
const validation = await validateProjectPath(resolved);
|
|
132
|
-
if (!validation.ok) {
|
|
133
|
-
pathErrors.push(` ${repo.path}: ${validation.error.message}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (pathErrors.length > 0) {
|
|
137
|
-
throw new ValidationError(`Invalid project paths:
|
|
138
|
-
${pathErrors.join("\n")}`, "repositories");
|
|
139
|
-
}
|
|
140
|
-
const normalizedProject = {
|
|
141
|
-
id: input.id ?? generateUuid8(),
|
|
142
|
-
name: input.name,
|
|
143
|
-
displayName: input.displayName,
|
|
144
|
-
description: input.description,
|
|
145
|
-
repositories: input.repositories.map((repo) => {
|
|
146
|
-
const resolvedPath = resolve(expandTilde(repo.path));
|
|
147
|
-
return {
|
|
148
|
-
id: repo.id ?? generateUuid8(),
|
|
149
|
-
name: repo.name && repo.name.length > 0 ? repo.name : basename(resolvedPath),
|
|
150
|
-
path: resolvedPath,
|
|
151
|
-
checkScript: repo.checkScript,
|
|
152
|
-
checkTimeout: repo.checkTimeout
|
|
153
|
-
};
|
|
154
|
-
})
|
|
155
|
-
};
|
|
156
|
-
projects.push(normalizedProject);
|
|
157
|
-
const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
|
|
158
|
-
if (!writeResult.ok) throw writeResult.error;
|
|
159
|
-
return normalizedProject;
|
|
160
|
-
}
|
|
161
|
-
async function updateProject(name, updates) {
|
|
162
|
-
const projects = await listProjects();
|
|
163
|
-
const index = projects.findIndex((p) => p.name === name);
|
|
164
|
-
if (index === -1) {
|
|
165
|
-
throw new ProjectNotFoundError(name);
|
|
166
|
-
}
|
|
167
|
-
if (updates.repositories) {
|
|
168
|
-
const pathErrors = [];
|
|
169
|
-
for (const repo of updates.repositories) {
|
|
170
|
-
const resolved = resolve(expandTilde(repo.path));
|
|
171
|
-
const validation = await validateProjectPath(resolved);
|
|
172
|
-
if (!validation.ok) {
|
|
173
|
-
pathErrors.push(` ${repo.path}: ${validation.error.message}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (pathErrors.length > 0) {
|
|
177
|
-
throw new ValidationError(`Invalid project paths:
|
|
178
|
-
${pathErrors.join("\n")}`, "repositories");
|
|
179
|
-
}
|
|
180
|
-
updates.repositories = updates.repositories.map((repo) => ({
|
|
181
|
-
id: repo.id.length > 0 ? repo.id : generateUuid8(),
|
|
182
|
-
name: repo.name && repo.name.length > 0 ? repo.name : basename(resolve(expandTilde(repo.path))),
|
|
183
|
-
path: resolve(expandTilde(repo.path)),
|
|
184
|
-
checkScript: repo.checkScript,
|
|
185
|
-
checkTimeout: repo.checkTimeout
|
|
186
|
-
}));
|
|
187
|
-
}
|
|
188
|
-
const existingProject = projects[index];
|
|
189
|
-
if (!existingProject) {
|
|
190
|
-
throw new ProjectNotFoundError(name);
|
|
191
|
-
}
|
|
192
|
-
const updatedProject = {
|
|
193
|
-
id: existingProject.id,
|
|
194
|
-
name: existingProject.name,
|
|
195
|
-
displayName: updates.displayName ?? existingProject.displayName,
|
|
196
|
-
repositories: updates.repositories ?? existingProject.repositories,
|
|
197
|
-
description: updates.description ?? existingProject.description
|
|
198
|
-
};
|
|
199
|
-
projects[index] = updatedProject;
|
|
200
|
-
const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
|
|
201
|
-
if (!writeResult.ok) throw writeResult.error;
|
|
202
|
-
return updatedProject;
|
|
203
|
-
}
|
|
204
|
-
async function removeProject(name) {
|
|
205
|
-
const projects = await listProjects();
|
|
206
|
-
const index = projects.findIndex((p) => p.name === name);
|
|
207
|
-
if (index === -1) {
|
|
208
|
-
throw new ProjectNotFoundError(name);
|
|
209
|
-
}
|
|
210
|
-
projects.splice(index, 1);
|
|
211
|
-
const writeResult = await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
|
|
212
|
-
if (!writeResult.ok) throw writeResult.error;
|
|
213
|
-
}
|
|
214
|
-
async function addProjectRepo(name, repo) {
|
|
215
|
-
const project = await getProject(name);
|
|
216
|
-
const resolvedPath = resolve(expandTilde(repo.path));
|
|
217
|
-
const validation = await validateProjectPath(resolvedPath);
|
|
218
|
-
if (!validation.ok) {
|
|
219
|
-
throw new ValidationError(`Invalid path ${repo.path}: ${validation.error.message}`, repo.path);
|
|
220
|
-
}
|
|
221
|
-
if (project.repositories.some((r) => r.path === resolvedPath)) {
|
|
222
|
-
return project;
|
|
223
|
-
}
|
|
224
|
-
const normalizedRepo = {
|
|
225
|
-
id: repo.id ?? generateUuid8(),
|
|
226
|
-
name: repo.name && repo.name.length > 0 ? repo.name : basename(resolvedPath),
|
|
227
|
-
path: resolvedPath,
|
|
228
|
-
checkScript: repo.checkScript,
|
|
229
|
-
checkTimeout: repo.checkTimeout
|
|
230
|
-
};
|
|
231
|
-
return updateProject(name, {
|
|
232
|
-
repositories: [...project.repositories, normalizedRepo]
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
async function removeProjectRepo(name, path) {
|
|
236
|
-
const project = await getProject(name);
|
|
237
|
-
const resolvedPath = resolve(expandTilde(path));
|
|
238
|
-
const newRepos = project.repositories.filter((r) => r.path !== resolvedPath);
|
|
239
|
-
if (newRepos.length === 0) {
|
|
240
|
-
throw new ValidationError("Cannot remove the last repository from a project", "repositories");
|
|
241
|
-
}
|
|
242
|
-
if (newRepos.length === project.repositories.length) {
|
|
243
|
-
return project;
|
|
244
|
-
}
|
|
245
|
-
return updateProject(name, { repositories: newRepos });
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export {
|
|
249
|
-
listProjects,
|
|
250
|
-
getProject,
|
|
251
|
-
getProjectById,
|
|
252
|
-
getRepoById,
|
|
253
|
-
resolveRepoPath,
|
|
254
|
-
projectExists,
|
|
255
|
-
createProject,
|
|
256
|
-
updateProject,
|
|
257
|
-
removeProject,
|
|
258
|
-
addProjectRepo,
|
|
259
|
-
removeProjectRepo
|
|
260
|
-
};
|
package/dist/chunk-CBMFRQ4Y.mjs
DELETED
|
@@ -1,441 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getCurrentSprint,
|
|
4
|
-
log
|
|
5
|
-
} from "./chunk-XN2UIHBY.mjs";
|
|
6
|
-
import {
|
|
7
|
-
SprintSchema,
|
|
8
|
-
TasksSchema,
|
|
9
|
-
appendToFile,
|
|
10
|
-
assertSafeCwd,
|
|
11
|
-
ensureDir,
|
|
12
|
-
fileExists,
|
|
13
|
-
generateSprintId,
|
|
14
|
-
getProgressFilePath,
|
|
15
|
-
getSprintDir,
|
|
16
|
-
getSprintFilePath,
|
|
17
|
-
getSprintsDir,
|
|
18
|
-
getTasksFilePath,
|
|
19
|
-
listDirs,
|
|
20
|
-
readTextFile,
|
|
21
|
-
readValidatedJson,
|
|
22
|
-
removeDir,
|
|
23
|
-
writeValidatedJson
|
|
24
|
-
} from "./chunk-WDMLPXOD.mjs";
|
|
25
|
-
import {
|
|
26
|
-
LockError,
|
|
27
|
-
NoCurrentSprintError,
|
|
28
|
-
SprintNotFoundError,
|
|
29
|
-
SprintStatusError,
|
|
30
|
-
StorageError
|
|
31
|
-
} from "./chunk-VAZ3LJBI.mjs";
|
|
32
|
-
|
|
33
|
-
// src/integration/persistence/progress.ts
|
|
34
|
-
import { execSync } from "child_process";
|
|
35
|
-
|
|
36
|
-
// src/integration/persistence/file-lock.ts
|
|
37
|
-
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
38
|
-
import { dirname } from "path";
|
|
39
|
-
import { Result } from "typescript-result";
|
|
40
|
-
var parsed = parseInt(process.env["RALPHCTL_LOCK_TIMEOUT_MS"] ?? "", 10);
|
|
41
|
-
var LOCK_TIMEOUT_MS = parsed > 0 && parsed <= 36e5 ? parsed : 3e4;
|
|
42
|
-
var RETRY_DELAY_MS = 50;
|
|
43
|
-
var MAX_RETRIES = 100;
|
|
44
|
-
function getLockPath(filePath) {
|
|
45
|
-
return `${filePath}.lock`;
|
|
46
|
-
}
|
|
47
|
-
async function sleep(ms) {
|
|
48
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
|
-
}
|
|
50
|
-
async function isLockStale(lockPath) {
|
|
51
|
-
try {
|
|
52
|
-
const content = await readFile(lockPath, "utf-8");
|
|
53
|
-
const info = JSON.parse(content);
|
|
54
|
-
const age = Date.now() - info.timestamp;
|
|
55
|
-
if (age > LOCK_TIMEOUT_MS) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
try {
|
|
59
|
-
process.kill(info.pid, 0);
|
|
60
|
-
return false;
|
|
61
|
-
} catch {
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async function acquireLock(filePath) {
|
|
69
|
-
const lockPath = getLockPath(filePath);
|
|
70
|
-
const lockInfo = {
|
|
71
|
-
pid: process.pid,
|
|
72
|
-
timestamp: Date.now()
|
|
73
|
-
};
|
|
74
|
-
let retries = 0;
|
|
75
|
-
while (retries < MAX_RETRIES) {
|
|
76
|
-
try {
|
|
77
|
-
await mkdir(dirname(lockPath), { recursive: true });
|
|
78
|
-
await writeFile(lockPath, JSON.stringify(lockInfo), { flag: "wx", mode: 384 });
|
|
79
|
-
return Result.ok(async () => {
|
|
80
|
-
try {
|
|
81
|
-
await unlink(lockPath);
|
|
82
|
-
} catch {
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
} catch (err) {
|
|
86
|
-
if (err instanceof Error && "code" in err && err.code === "EEXIST") {
|
|
87
|
-
if (await isLockStale(lockPath)) {
|
|
88
|
-
try {
|
|
89
|
-
await unlink(lockPath);
|
|
90
|
-
} catch {
|
|
91
|
-
}
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
retries++;
|
|
95
|
-
await sleep(RETRY_DELAY_MS);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
return Result.error(
|
|
99
|
-
new LockError(
|
|
100
|
-
`Failed to acquire lock: ${err instanceof Error ? err.message : String(err)}`,
|
|
101
|
-
lockPath,
|
|
102
|
-
err instanceof Error ? err : void 0
|
|
103
|
-
)
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return Result.error(new LockError(`Failed to acquire lock after ${String(MAX_RETRIES)} retries`, lockPath));
|
|
108
|
-
}
|
|
109
|
-
async function withFileLock(filePath, fn) {
|
|
110
|
-
const lockResult = await acquireLock(filePath);
|
|
111
|
-
if (!lockResult.ok) {
|
|
112
|
-
return lockResult;
|
|
113
|
-
}
|
|
114
|
-
const release = lockResult.value;
|
|
115
|
-
try {
|
|
116
|
-
const value = await fn();
|
|
117
|
-
return Result.ok(value);
|
|
118
|
-
} finally {
|
|
119
|
-
await release();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/integration/persistence/progress.ts
|
|
124
|
-
async function logProgress(message, options = {}) {
|
|
125
|
-
const id = await resolveSprintId(options.sprintId);
|
|
126
|
-
const sprint = await getSprint(id);
|
|
127
|
-
assertSprintStatus(sprint, ["active"], "log progress");
|
|
128
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
129
|
-
const projectMarker = options.projectPath ? `**Project:** ${options.projectPath}
|
|
130
|
-
|
|
131
|
-
` : "";
|
|
132
|
-
const entry = `## ${timestamp}
|
|
133
|
-
|
|
134
|
-
${projectMarker}${message}
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
`;
|
|
139
|
-
const progressPath = getProgressFilePath(id);
|
|
140
|
-
const lockResult = await withFileLock(progressPath, async () => {
|
|
141
|
-
const appendResult = await appendToFile(progressPath, entry);
|
|
142
|
-
if (!appendResult.ok) throw appendResult.error;
|
|
143
|
-
});
|
|
144
|
-
if (!lockResult.ok) throw lockResult.error;
|
|
145
|
-
}
|
|
146
|
-
function isExecError(err) {
|
|
147
|
-
return err instanceof Error && typeof err["status"] === "number";
|
|
148
|
-
}
|
|
149
|
-
function isNodeError(err) {
|
|
150
|
-
return err instanceof Error && typeof err["code"] === "string";
|
|
151
|
-
}
|
|
152
|
-
function getGitCommitInfo(projectPath) {
|
|
153
|
-
try {
|
|
154
|
-
assertSafeCwd(projectPath);
|
|
155
|
-
const output = execSync("git log -1 --pretty=format:%H\\ %s", {
|
|
156
|
-
cwd: projectPath,
|
|
157
|
-
encoding: "utf-8",
|
|
158
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
159
|
-
}).trim();
|
|
160
|
-
const spaceIndex = output.indexOf(" ");
|
|
161
|
-
return {
|
|
162
|
-
hash: output.slice(0, spaceIndex),
|
|
163
|
-
message: output.slice(spaceIndex + 1)
|
|
164
|
-
};
|
|
165
|
-
} catch (err) {
|
|
166
|
-
if (isExecError(err) && err.status === 128) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
if (isNodeError(err) && err.code === "ENOENT") {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
173
|
-
log.warn(`Failed to get git info for ${projectPath}: ${detail}`);
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async function logBaselines(options) {
|
|
178
|
-
const { sprintId, sprintName, projectPaths } = options;
|
|
179
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
180
|
-
const lines = [
|
|
181
|
-
`## ${timestamp}`,
|
|
182
|
-
"",
|
|
183
|
-
"### Sprint Baseline State",
|
|
184
|
-
"",
|
|
185
|
-
`Sprint: ${sprintName} (${sprintId})`,
|
|
186
|
-
`Activated: ${timestamp}`,
|
|
187
|
-
"",
|
|
188
|
-
"#### Project Git State at Activation",
|
|
189
|
-
""
|
|
190
|
-
];
|
|
191
|
-
const uniquePaths = [...new Set(projectPaths)];
|
|
192
|
-
for (const path of uniquePaths) {
|
|
193
|
-
const commitInfo = getGitCommitInfo(path);
|
|
194
|
-
if (commitInfo) {
|
|
195
|
-
lines.push(`- **${path}**`);
|
|
196
|
-
lines.push(` \`${commitInfo.hash} ${commitInfo.message}\``);
|
|
197
|
-
} else {
|
|
198
|
-
lines.push(`- **${path}**`);
|
|
199
|
-
lines.push(` *(not a git repository or unable to retrieve state)*`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
lines.push("");
|
|
203
|
-
lines.push("---");
|
|
204
|
-
lines.push("");
|
|
205
|
-
const appendResult = await appendToFile(getProgressFilePath(sprintId), lines.join("\n"));
|
|
206
|
-
if (!appendResult.ok) throw appendResult.error;
|
|
207
|
-
}
|
|
208
|
-
async function getProgress(sprintId) {
|
|
209
|
-
const id = await resolveSprintId(sprintId);
|
|
210
|
-
const result = await readTextFile(getProgressFilePath(id));
|
|
211
|
-
if (!result.ok) {
|
|
212
|
-
if (result.error instanceof StorageError && result.error.cause && "code" in result.error.cause && result.error.cause.code === "ENOENT") {
|
|
213
|
-
return "";
|
|
214
|
-
}
|
|
215
|
-
throw result.error;
|
|
216
|
-
}
|
|
217
|
-
return result.value;
|
|
218
|
-
}
|
|
219
|
-
function summarizeProgressForContext(progress, projectPath, maxEntries = 3) {
|
|
220
|
-
const filtered = filterProgressByProject(progress, projectPath);
|
|
221
|
-
if (!filtered.trim()) {
|
|
222
|
-
return "";
|
|
223
|
-
}
|
|
224
|
-
const entries = filtered.split(/\n---\n/).filter((e) => e.trim());
|
|
225
|
-
const recent = entries.slice(-maxEntries);
|
|
226
|
-
const summaries = [];
|
|
227
|
-
for (const entry of recent) {
|
|
228
|
-
const headerMatch = /^##\s+(.+)$/m.exec(entry);
|
|
229
|
-
const header = headerMatch?.[1] ?? "Unknown entry";
|
|
230
|
-
const learnings = extractSection(entry, "Learnings and Context");
|
|
231
|
-
const notes = extractSection(entry, "Notes for Next Tasks");
|
|
232
|
-
if (learnings || notes) {
|
|
233
|
-
const parts = [`**${header}**`];
|
|
234
|
-
if (learnings) {
|
|
235
|
-
parts.push(`**Learnings:** ${learnings}`);
|
|
236
|
-
}
|
|
237
|
-
if (notes) {
|
|
238
|
-
parts.push(`**Notes for next tasks:** ${notes}`);
|
|
239
|
-
}
|
|
240
|
-
summaries.push(parts.join("\n"));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (summaries.length === 0) {
|
|
244
|
-
return "";
|
|
245
|
-
}
|
|
246
|
-
return summaries.join("\n\n");
|
|
247
|
-
}
|
|
248
|
-
function extractSection(entry, sectionName) {
|
|
249
|
-
const regex = new RegExp(`###\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\n([\\s\\S]*?)(?=###|$)`);
|
|
250
|
-
const match = regex.exec(entry);
|
|
251
|
-
if (!match?.[1]) return null;
|
|
252
|
-
const content = match[1].trim();
|
|
253
|
-
return content || null;
|
|
254
|
-
}
|
|
255
|
-
function filterProgressByProject(progress, projectPath) {
|
|
256
|
-
if (!progress.trim()) {
|
|
257
|
-
return "";
|
|
258
|
-
}
|
|
259
|
-
const entries = progress.split(/\n---\n/).filter((e) => e.trim());
|
|
260
|
-
const filtered = entries.filter((entry) => {
|
|
261
|
-
const visibleMatch = /\*\*Project:\*\*\s*(.+?)(?:\n|$)/.exec(entry);
|
|
262
|
-
if (visibleMatch?.[1]) {
|
|
263
|
-
return visibleMatch[1].trim() === projectPath;
|
|
264
|
-
}
|
|
265
|
-
const htmlMatch = /<!--\s*project:\s*(.+?)\s*-->/.exec(entry);
|
|
266
|
-
if (htmlMatch?.[1]) {
|
|
267
|
-
return htmlMatch[1] === projectPath;
|
|
268
|
-
}
|
|
269
|
-
return true;
|
|
270
|
-
});
|
|
271
|
-
if (filtered.length === 0) {
|
|
272
|
-
return "";
|
|
273
|
-
}
|
|
274
|
-
return filtered.join("\n---\n") + "\n\n---\n\n";
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/integration/persistence/sprint.ts
|
|
278
|
-
function assertSprintStatus(sprint, allowedStatuses, operation) {
|
|
279
|
-
if (!allowedStatuses.includes(sprint.status)) {
|
|
280
|
-
const statusText = allowedStatuses.join(" or ");
|
|
281
|
-
const hints = {
|
|
282
|
-
"add tickets": "Close the current sprint and create a new one for additional work.",
|
|
283
|
-
"remove tickets": "Sprint must be in draft status to remove tickets.",
|
|
284
|
-
"add tasks": "Close the current sprint and create a new one for additional work.",
|
|
285
|
-
"remove tasks": "Sprint must be in draft status to remove tasks.",
|
|
286
|
-
"reorder tasks": "Sprint must be in draft status to reorder tasks.",
|
|
287
|
-
refine: "Refinement can only be done on draft sprints.",
|
|
288
|
-
plan: "Planning can only be done on draft sprints.",
|
|
289
|
-
activate: "Sprint must be in draft status to activate.",
|
|
290
|
-
start: "Sprint must be draft or active to start.",
|
|
291
|
-
"update task status": "Task status can only be updated during active execution.",
|
|
292
|
-
"log progress": "Progress can only be logged during active execution.",
|
|
293
|
-
close: "Sprint must be active to close."
|
|
294
|
-
};
|
|
295
|
-
const hint = hints[operation] ?? "";
|
|
296
|
-
const hintText = hint ? `
|
|
297
|
-
Hint: ${hint}` : "";
|
|
298
|
-
throw new SprintStatusError(
|
|
299
|
-
`Cannot ${operation}: sprint status is '${sprint.status}' (must be ${statusText}).${hintText}`,
|
|
300
|
-
sprint.status,
|
|
301
|
-
operation
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
async function createSprint(input) {
|
|
306
|
-
const { projectId, name } = input;
|
|
307
|
-
const id = generateSprintId(name);
|
|
308
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
309
|
-
const displayName = name ?? id.slice(16);
|
|
310
|
-
const sprint = {
|
|
311
|
-
id,
|
|
312
|
-
name: displayName,
|
|
313
|
-
projectId,
|
|
314
|
-
status: "draft",
|
|
315
|
-
createdAt: now,
|
|
316
|
-
activatedAt: null,
|
|
317
|
-
closedAt: null,
|
|
318
|
-
tickets: [],
|
|
319
|
-
checkRanAt: {},
|
|
320
|
-
branch: null
|
|
321
|
-
};
|
|
322
|
-
const sprintDir = getSprintDir(id);
|
|
323
|
-
await ensureDir(sprintDir);
|
|
324
|
-
const writeSprintResult = await writeValidatedJson(getSprintFilePath(id), sprint, SprintSchema);
|
|
325
|
-
if (!writeSprintResult.ok) throw writeSprintResult.error;
|
|
326
|
-
const writeTasksResult = await writeValidatedJson(getTasksFilePath(id), [], TasksSchema);
|
|
327
|
-
if (!writeTasksResult.ok) throw writeTasksResult.error;
|
|
328
|
-
const appendResult = await appendToFile(
|
|
329
|
-
getProgressFilePath(id),
|
|
330
|
-
`# Sprint: ${displayName}
|
|
331
|
-
|
|
332
|
-
Created: ${now}
|
|
333
|
-
|
|
334
|
-
---
|
|
335
|
-
|
|
336
|
-
`
|
|
337
|
-
);
|
|
338
|
-
if (!appendResult.ok) throw appendResult.error;
|
|
339
|
-
return sprint;
|
|
340
|
-
}
|
|
341
|
-
async function getSprint(sprintId) {
|
|
342
|
-
const sprintPath = getSprintFilePath(sprintId);
|
|
343
|
-
if (!await fileExists(sprintPath)) {
|
|
344
|
-
throw new SprintNotFoundError(sprintId);
|
|
345
|
-
}
|
|
346
|
-
const result = await readValidatedJson(sprintPath, SprintSchema);
|
|
347
|
-
if (!result.ok) throw result.error;
|
|
348
|
-
return result.value;
|
|
349
|
-
}
|
|
350
|
-
async function saveSprint(sprint) {
|
|
351
|
-
const result = await writeValidatedJson(getSprintFilePath(sprint.id), sprint, SprintSchema);
|
|
352
|
-
if (!result.ok) throw result.error;
|
|
353
|
-
}
|
|
354
|
-
async function listSprints() {
|
|
355
|
-
const sprintsDir = getSprintsDir();
|
|
356
|
-
const dirs = await listDirs(sprintsDir);
|
|
357
|
-
const sprints = [];
|
|
358
|
-
for (const dir of dirs) {
|
|
359
|
-
const sprintPath = getSprintFilePath(dir);
|
|
360
|
-
if (!await fileExists(sprintPath)) continue;
|
|
361
|
-
const result = await readValidatedJson(sprintPath, SprintSchema);
|
|
362
|
-
if (!result.ok) continue;
|
|
363
|
-
sprints.push(result.value);
|
|
364
|
-
}
|
|
365
|
-
return sprints.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
366
|
-
}
|
|
367
|
-
async function activateSprint(sprintId) {
|
|
368
|
-
const sprint = await getSprint(sprintId);
|
|
369
|
-
assertSprintStatus(sprint, ["draft"], "activate");
|
|
370
|
-
sprint.status = "active";
|
|
371
|
-
sprint.activatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
372
|
-
await saveSprint(sprint);
|
|
373
|
-
const tasksResult = await readValidatedJson(getTasksFilePath(sprintId), TasksSchema);
|
|
374
|
-
if (!tasksResult.ok) throw tasksResult.error;
|
|
375
|
-
const _tasks = tasksResult.value;
|
|
376
|
-
void _tasks;
|
|
377
|
-
return sprint;
|
|
378
|
-
}
|
|
379
|
-
async function logSprintBaselines(sprint, resolvePath) {
|
|
380
|
-
const tasksResult = await readValidatedJson(getTasksFilePath(sprint.id), TasksSchema);
|
|
381
|
-
if (!tasksResult.ok) throw tasksResult.error;
|
|
382
|
-
const repoIds = [...new Set(tasksResult.value.map((t) => t.repoId))];
|
|
383
|
-
const paths = [];
|
|
384
|
-
for (const repoId of repoIds) {
|
|
385
|
-
const p = await resolvePath(repoId);
|
|
386
|
-
if (p) paths.push(p);
|
|
387
|
-
}
|
|
388
|
-
if (paths.length > 0) {
|
|
389
|
-
await logBaselines({ sprintId: sprint.id, sprintName: sprint.name, projectPaths: paths });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
async function closeSprint(sprintId) {
|
|
393
|
-
const sprint = await getSprint(sprintId);
|
|
394
|
-
assertSprintStatus(sprint, ["active"], "close");
|
|
395
|
-
sprint.status = "closed";
|
|
396
|
-
sprint.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
397
|
-
sprint.checkRanAt = {};
|
|
398
|
-
await saveSprint(sprint);
|
|
399
|
-
return sprint;
|
|
400
|
-
}
|
|
401
|
-
async function deleteSprint(sprintId) {
|
|
402
|
-
const sprint = await getSprint(sprintId);
|
|
403
|
-
const sprintDir = getSprintDir(sprintId);
|
|
404
|
-
await removeDir(sprintDir);
|
|
405
|
-
return sprint;
|
|
406
|
-
}
|
|
407
|
-
async function getCurrentSprintOrThrow() {
|
|
408
|
-
const currentSprintId = await getCurrentSprint();
|
|
409
|
-
if (!currentSprintId) {
|
|
410
|
-
throw new NoCurrentSprintError();
|
|
411
|
-
}
|
|
412
|
-
return getSprint(currentSprintId);
|
|
413
|
-
}
|
|
414
|
-
async function resolveSprintId(sprintId) {
|
|
415
|
-
if (sprintId) {
|
|
416
|
-
return sprintId;
|
|
417
|
-
}
|
|
418
|
-
const currentSprintId = await getCurrentSprint();
|
|
419
|
-
if (!currentSprintId) {
|
|
420
|
-
throw new NoCurrentSprintError();
|
|
421
|
-
}
|
|
422
|
-
return currentSprintId;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export {
|
|
426
|
-
withFileLock,
|
|
427
|
-
logProgress,
|
|
428
|
-
getProgress,
|
|
429
|
-
summarizeProgressForContext,
|
|
430
|
-
assertSprintStatus,
|
|
431
|
-
createSprint,
|
|
432
|
-
getSprint,
|
|
433
|
-
saveSprint,
|
|
434
|
-
listSprints,
|
|
435
|
-
activateSprint,
|
|
436
|
-
logSprintBaselines,
|
|
437
|
-
closeSprint,
|
|
438
|
-
deleteSprint,
|
|
439
|
-
getCurrentSprintOrThrow,
|
|
440
|
-
resolveSprintId
|
|
441
|
-
};
|
package/dist/chunk-CFUVE2BP.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/domain/exit-codes.ts
|
|
4
|
-
var EXIT_ERROR = 1;
|
|
5
|
-
var EXIT_NO_TASKS = 2;
|
|
6
|
-
var EXIT_INTERRUPTED = 130;
|
|
7
|
-
function exitWithCode(code) {
|
|
8
|
-
process.exit(code);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
EXIT_ERROR,
|
|
13
|
-
EXIT_NO_TASKS,
|
|
14
|
-
EXIT_INTERRUPTED,
|
|
15
|
-
exitWithCode
|
|
16
|
-
};
|