@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.
Files changed (40) hide show
  1. package/.delano/viewer/README.md +3 -2
  2. package/.delano/viewer/public/app.js +13 -1
  3. package/.delano/viewer/public/app.jsx +2312 -0
  4. package/.delano/viewer/public/delano-mark.svg +4 -0
  5. package/.delano/viewer/public/index.html +12 -14
  6. package/.delano/viewer/public/styles.css +1005 -833
  7. package/.delano/viewer/server.js +46 -5
  8. package/README.md +63 -3
  9. package/assets/install-manifest.json +7 -0
  10. package/assets/payload/.agents/adapters/manifest.schema.json +103 -0
  11. package/assets/payload/.agents/adapters/spec-kit/adapter.json +71 -0
  12. package/assets/payload/.agents/hooks/README.md +6 -1
  13. package/assets/payload/.agents/hooks/codex-session-status.js +123 -0
  14. package/assets/payload/.agents/schemas/status-transitions.json +35 -0
  15. package/assets/payload/.agents/scripts/README.md +1 -1
  16. package/assets/payload/.agents/scripts/check-status-transitions.mjs +171 -2
  17. package/assets/payload/.agents/scripts/pm/import-spec-kit.sh +605 -0
  18. package/assets/payload/.agents/scripts/pm/init.sh +31 -2
  19. package/assets/payload/.agents/scripts/pm/research.sh +296 -0
  20. package/assets/payload/.agents/scripts/pm/status.sh +135 -28
  21. package/assets/payload/.agents/scripts/pm/validate.sh +16 -0
  22. package/assets/payload/.codex/hooks.json +17 -0
  23. package/assets/payload/.delano/viewer/README.md +3 -2
  24. package/assets/payload/.delano/viewer/public/app.js +13 -1
  25. package/assets/payload/.delano/viewer/public/index.html +12 -14
  26. package/assets/payload/.delano/viewer/public/styles.css +1005 -833
  27. package/assets/payload/.delano/viewer/server.js +46 -5
  28. package/assets/payload/.project/templates/decisions.md +18 -0
  29. package/assets/payload/.project/templates/plan.md +17 -0
  30. package/assets/payload/.project/templates/spec.md +12 -0
  31. package/assets/payload/.project/templates/task.md +6 -0
  32. package/assets/payload/.project/templates/workstream.md +1 -0
  33. package/package.json +4 -2
  34. package/src/cli/commands/install.js +2 -1
  35. package/src/cli/commands/state.js +689 -0
  36. package/src/cli/commands/viewer.js +2 -1
  37. package/src/cli/commands/wrapper.js +29 -5
  38. package/src/cli/index.js +120 -7
  39. package/src/cli/lib/install.js +179 -2
  40. 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 ["ready-dependencies-done", "blocked-owner-check-back"]) {
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
- return { nextStatus, dependencyStatuses, blockedOwner, blockedCheckBack };
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) {