@bvdm/delano 0.2.3 → 0.2.5
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/.delano/viewer/README.md +3 -2
- package/.delano/viewer/public/app.js +13 -1
- package/.delano/viewer/public/app.jsx +2312 -0
- package/.delano/viewer/public/delano-mark.svg +4 -0
- package/.delano/viewer/public/index.html +12 -14
- package/.delano/viewer/public/styles.css +1005 -833
- package/.delano/viewer/server.js +46 -5
- package/README.md +63 -3
- package/assets/install-manifest.json +7 -0
- package/assets/payload/.agents/adapters/manifest.schema.json +103 -0
- package/assets/payload/.agents/adapters/spec-kit/adapter.json +71 -0
- package/assets/payload/.agents/hooks/README.md +6 -1
- package/assets/payload/.agents/hooks/codex-session-status.js +123 -0
- package/assets/payload/.agents/schemas/status-transitions.json +35 -0
- package/assets/payload/.agents/scripts/README.md +1 -1
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +171 -2
- package/assets/payload/.agents/scripts/pm/import-spec-kit.sh +605 -0
- package/assets/payload/.agents/scripts/pm/init.sh +31 -2
- package/assets/payload/.agents/scripts/pm/research.sh +296 -0
- package/assets/payload/.agents/scripts/pm/status.sh +135 -28
- package/assets/payload/.agents/scripts/pm/validate.sh +16 -0
- package/assets/payload/.codex/hooks.json +17 -0
- package/assets/payload/.delano/viewer/README.md +3 -2
- package/assets/payload/.delano/viewer/public/app.js +13 -1
- package/assets/payload/.delano/viewer/public/index.html +12 -14
- package/assets/payload/.delano/viewer/public/styles.css +1005 -833
- package/assets/payload/.delano/viewer/server.js +46 -5
- package/assets/payload/.project/templates/decisions.md +18 -0
- package/assets/payload/.project/templates/plan.md +17 -0
- package/assets/payload/.project/templates/spec.md +12 -0
- package/assets/payload/.project/templates/task.md +6 -0
- package/assets/payload/.project/templates/workstream.md +1 -0
- package/package.json +4 -2
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/state.js +689 -0
- package/src/cli/commands/viewer.js +2 -1
- package/src/cli/commands/wrapper.js +29 -5
- package/src/cli/index.js +120 -7
- package/src/cli/lib/install.js +179 -2
- package/src/cli/lib/project-state.js +918 -0
|
@@ -15,7 +15,14 @@ if (contract.schema_version !== 1) {
|
|
|
15
15
|
errors.push("status-transitions.json schema_version must be 1.");
|
|
16
16
|
}
|
|
17
17
|
const rules = Array.isArray(contract.task_rules) ? contract.task_rules : [];
|
|
18
|
-
for (const requiredRule of [
|
|
18
|
+
for (const requiredRule of [
|
|
19
|
+
"ready-dependencies-done",
|
|
20
|
+
"blocked-owner-check-back",
|
|
21
|
+
"progressed-task-requires-active-project",
|
|
22
|
+
"closed-task-set-requires-closed-project",
|
|
23
|
+
"progressed-task-requires-active-workstream",
|
|
24
|
+
"closed-task-set-requires-closed-workstream"
|
|
25
|
+
]) {
|
|
19
26
|
if (!rules.some((rule) => rule.id === requiredRule)) {
|
|
20
27
|
errors.push(`status transition contract missing rule: ${requiredRule}`);
|
|
21
28
|
}
|
|
@@ -28,19 +35,69 @@ if (transitionRequest) {
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
for (const projectDir of listDirectories(projectsRoot)) {
|
|
38
|
+
const specPath = path.join(projectDir, "spec.md");
|
|
39
|
+
const planPath = path.join(projectDir, "plan.md");
|
|
40
|
+
const specFrontmatter = existsSync(specPath) ? parseFrontmatter(specPath) : null;
|
|
41
|
+
const planFrontmatter = existsSync(planPath) ? parseFrontmatter(planPath) : null;
|
|
42
|
+
const hasProjectLifecycle = Boolean(specFrontmatter || planFrontmatter);
|
|
43
|
+
const workstreams = collectWorkstreams(projectDir);
|
|
44
|
+
const workstreamSummaries = new Map();
|
|
45
|
+
for (const [workstreamId, workstream] of workstreams.entries()) {
|
|
46
|
+
workstreamSummaries.set(workstreamId, {
|
|
47
|
+
workstream,
|
|
48
|
+
totalTaskCount: 0,
|
|
49
|
+
openTaskCount: 0
|
|
50
|
+
});
|
|
51
|
+
}
|
|
31
52
|
const tasksDir = path.join(projectDir, "tasks");
|
|
32
53
|
if (!existsSync(tasksDir)) continue;
|
|
33
54
|
|
|
34
55
|
const tasks = new Map();
|
|
56
|
+
let totalTaskCount = 0;
|
|
57
|
+
let openTaskCount = 0;
|
|
58
|
+
let progressedTaskCount = 0;
|
|
35
59
|
for (const taskFile of listMarkdownFiles(tasksDir)) {
|
|
36
60
|
const frontmatter = parseFrontmatter(taskFile);
|
|
37
61
|
const id = frontmatter.id || path.basename(taskFile, ".md").split("-").slice(0, 2).join("-");
|
|
62
|
+
const status = frontmatter.status || "";
|
|
63
|
+
totalTaskCount += 1;
|
|
64
|
+
if (!isClosedTaskStatus(status)) openTaskCount += 1;
|
|
65
|
+
if (isProgressedTaskStatus(status)) progressedTaskCount += 1;
|
|
66
|
+
const taskWorkstream = frontmatter.workstream || "";
|
|
67
|
+
if (taskWorkstream && workstreamSummaries.has(taskWorkstream)) {
|
|
68
|
+
const summary = workstreamSummaries.get(taskWorkstream);
|
|
69
|
+
summary.totalTaskCount += 1;
|
|
70
|
+
if (!isClosedTaskStatus(status)) summary.openTaskCount += 1;
|
|
71
|
+
}
|
|
38
72
|
tasks.set(id, { file: taskFile, frontmatter });
|
|
39
73
|
}
|
|
40
74
|
|
|
75
|
+
if (hasProjectLifecycle) {
|
|
76
|
+
validateProjectLifecycle({
|
|
77
|
+
projectDir,
|
|
78
|
+
specStatus: specFrontmatter?.status || "",
|
|
79
|
+
planStatus: planFrontmatter?.status || "",
|
|
80
|
+
totalTaskCount,
|
|
81
|
+
openTaskCount,
|
|
82
|
+
progressedTaskCount
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
41
86
|
for (const [taskId, task] of tasks.entries()) {
|
|
42
87
|
const status = task.frontmatter.status || "";
|
|
43
88
|
const dependencies = parseList(task.frontmatter.depends_on || "[]");
|
|
89
|
+
const taskWorkstream = task.frontmatter.workstream || "";
|
|
90
|
+
const workstream = taskWorkstream ? workstreams.get(taskWorkstream) : null;
|
|
91
|
+
|
|
92
|
+
if (isProgressedTaskStatus(status)) {
|
|
93
|
+
if (!taskWorkstream) {
|
|
94
|
+
errors.push(`${toRepoPath(task.file)} has status ${status} but is missing workstream frontmatter; expected an existing workstream id.`);
|
|
95
|
+
} else if (!workstream) {
|
|
96
|
+
errors.push(`${toRepoPath(task.file)} has status ${status} but workstream ${taskWorkstream} does not exist; expected an existing workstream id.`);
|
|
97
|
+
} else {
|
|
98
|
+
validateTaskWorkstreamLifecycle({ task, workstream });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
44
101
|
|
|
45
102
|
if (["ready", "in-progress", "done"].includes(status)) {
|
|
46
103
|
for (const dependencyId of dependencies) {
|
|
@@ -62,6 +119,10 @@ for (const projectDir of listDirectories(projectsRoot)) {
|
|
|
62
119
|
}
|
|
63
120
|
}
|
|
64
121
|
}
|
|
122
|
+
|
|
123
|
+
for (const summary of workstreamSummaries.values()) {
|
|
124
|
+
validateWorkstreamLifecycle(summary);
|
|
125
|
+
}
|
|
65
126
|
}
|
|
66
127
|
|
|
67
128
|
finish();
|
|
@@ -75,7 +136,10 @@ function parseTransitionArgs(args) {
|
|
|
75
136
|
.filter(Boolean);
|
|
76
137
|
const blockedOwner = valueAfter(args, "--blocked-owner");
|
|
77
138
|
const blockedCheckBack = valueAfter(args, "--blocked-check-back");
|
|
78
|
-
|
|
139
|
+
const specStatus = valueAfter(args, "--spec-status");
|
|
140
|
+
const planStatus = valueAfter(args, "--plan-status");
|
|
141
|
+
const workstreamStatus = valueAfter(args, "--workstream-status");
|
|
142
|
+
return { nextStatus, dependencyStatuses, blockedOwner, blockedCheckBack, specStatus, planStatus, workstreamStatus };
|
|
79
143
|
}
|
|
80
144
|
|
|
81
145
|
function validateTransitionRequest(request) {
|
|
@@ -87,12 +151,106 @@ function validateTransitionRequest(request) {
|
|
|
87
151
|
}
|
|
88
152
|
}
|
|
89
153
|
|
|
154
|
+
if (["in-progress", "done"].includes(request.nextStatus)) {
|
|
155
|
+
if (request.specStatus && !isActiveOrClosedSpecStatus(request.specStatus)) {
|
|
156
|
+
errors.push(`cannot transition to ${request.nextStatus} while spec status is ${request.specStatus}; expected active or complete`);
|
|
157
|
+
}
|
|
158
|
+
if (request.planStatus && !isActiveOrClosedPlanStatus(request.planStatus)) {
|
|
159
|
+
errors.push(`cannot transition to ${request.nextStatus} while plan status is ${request.planStatus}; expected active or done`);
|
|
160
|
+
}
|
|
161
|
+
if (request.nextStatus === "in-progress" && request.workstreamStatus && !isActiveWorkstreamStatus(request.workstreamStatus)) {
|
|
162
|
+
errors.push(`cannot transition to in-progress while workstream status is ${request.workstreamStatus}; expected active`);
|
|
163
|
+
}
|
|
164
|
+
if (request.nextStatus === "done" && request.workstreamStatus && !isActiveOrClosedWorkstreamStatus(request.workstreamStatus)) {
|
|
165
|
+
errors.push(`cannot transition to done while workstream status is ${request.workstreamStatus}; expected active or done`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
90
169
|
if (request.nextStatus === "blocked") {
|
|
91
170
|
if (!request.blockedOwner) errors.push("cannot transition to blocked without blocked_owner");
|
|
92
171
|
if (!request.blockedCheckBack) errors.push("cannot transition to blocked without blocked_check_back");
|
|
93
172
|
}
|
|
94
173
|
}
|
|
95
174
|
|
|
175
|
+
function validateProjectLifecycle(request) {
|
|
176
|
+
const projectPath = toRepoPath(request.projectDir);
|
|
177
|
+
if (request.progressedTaskCount > 0) {
|
|
178
|
+
if (!isActiveOrClosedSpecStatus(request.specStatus)) {
|
|
179
|
+
errors.push(`${projectPath} has ${request.progressedTaskCount} progressed task(s) but spec.md status is ${describeStatus(request.specStatus)}; expected active or complete before tasks can progress.`);
|
|
180
|
+
}
|
|
181
|
+
if (!isActiveOrClosedPlanStatus(request.planStatus)) {
|
|
182
|
+
errors.push(`${projectPath} has ${request.progressedTaskCount} progressed task(s) but plan.md status is ${describeStatus(request.planStatus)}; expected active or done before tasks can progress.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (request.totalTaskCount > 0 && request.openTaskCount === 0) {
|
|
187
|
+
if (!isClosedSpecStatus(request.specStatus)) {
|
|
188
|
+
errors.push(`${projectPath} has no open tasks but spec.md status is ${describeStatus(request.specStatus)}; expected complete or deferred.`);
|
|
189
|
+
}
|
|
190
|
+
if (!isClosedPlanStatus(request.planStatus)) {
|
|
191
|
+
errors.push(`${projectPath} has no open tasks but plan.md status is ${describeStatus(request.planStatus)}; expected done or deferred.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validateTaskWorkstreamLifecycle({ task, workstream }) {
|
|
197
|
+
const status = task.frontmatter.status || "";
|
|
198
|
+
const workstreamStatus = workstream.frontmatter.status || "";
|
|
199
|
+
if (status === "in-progress" && !isActiveWorkstreamStatus(workstreamStatus)) {
|
|
200
|
+
errors.push(`${toRepoPath(task.file)} has status in-progress but workstream ${workstream.id} status is ${describeStatus(workstreamStatus)}; expected active.`);
|
|
201
|
+
}
|
|
202
|
+
if (status === "done" && !isActiveOrClosedWorkstreamStatus(workstreamStatus)) {
|
|
203
|
+
errors.push(`${toRepoPath(task.file)} has status done but workstream ${workstream.id} status is ${describeStatus(workstreamStatus)}; expected active or done.`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateWorkstreamLifecycle({ workstream, totalTaskCount, openTaskCount }) {
|
|
208
|
+
const workstreamStatus = workstream.frontmatter.status || "";
|
|
209
|
+
if (totalTaskCount > 0 && openTaskCount === 0 && !isClosedWorkstreamStatus(workstreamStatus)) {
|
|
210
|
+
errors.push(`${toRepoPath(workstream.file)} has no open tasks but status is ${describeStatus(workstreamStatus)}; expected done or deferred.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isProgressedTaskStatus(status) {
|
|
215
|
+
return ["in-progress", "done"].includes(status);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isClosedTaskStatus(status) {
|
|
219
|
+
return ["done", "deferred", "canceled"].includes(status);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isActiveOrClosedSpecStatus(status) {
|
|
223
|
+
return ["active", "complete"].includes(status);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isActiveOrClosedPlanStatus(status) {
|
|
227
|
+
return ["active", "done"].includes(status);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isClosedSpecStatus(status) {
|
|
231
|
+
return ["complete", "deferred"].includes(status);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isClosedPlanStatus(status) {
|
|
235
|
+
return ["done", "deferred"].includes(status);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isActiveWorkstreamStatus(status) {
|
|
239
|
+
return status === "active";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isActiveOrClosedWorkstreamStatus(status) {
|
|
243
|
+
return ["active", "done"].includes(status);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isClosedWorkstreamStatus(status) {
|
|
247
|
+
return ["done", "deferred"].includes(status);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function describeStatus(status) {
|
|
251
|
+
return status || "missing status";
|
|
252
|
+
}
|
|
253
|
+
|
|
96
254
|
function valueAfter(args, flag) {
|
|
97
255
|
const index = args.indexOf(flag);
|
|
98
256
|
if (index === -1 || index === args.length - 1) return "";
|
|
@@ -156,6 +314,17 @@ function listMarkdownFiles(root) {
|
|
|
156
314
|
.map((entry) => path.join(root, entry.name));
|
|
157
315
|
}
|
|
158
316
|
|
|
317
|
+
function collectWorkstreams(projectDir) {
|
|
318
|
+
const workstreamsDir = path.join(projectDir, "workstreams");
|
|
319
|
+
const workstreams = new Map();
|
|
320
|
+
for (const workstreamFile of listMarkdownFiles(workstreamsDir)) {
|
|
321
|
+
const frontmatter = parseFrontmatter(workstreamFile);
|
|
322
|
+
const id = frontmatter.id || path.basename(workstreamFile, ".md").match(/^(WS-[A-Za-z0-9]+)/)?.[1] || "";
|
|
323
|
+
if (id) workstreams.set(id, { id, file: workstreamFile, frontmatter });
|
|
324
|
+
}
|
|
325
|
+
return workstreams;
|
|
326
|
+
}
|
|
327
|
+
|
|
159
328
|
function resolveRepoRoot(startDir) {
|
|
160
329
|
const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
|
|
161
330
|
for (const candidate of candidates) {
|