@bvdm/delano 0.2.4 → 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/public/styles.css +1 -1
- package/README.md +35 -0
- package/assets/install-manifest.json +5 -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/schemas/status-transitions.json +17 -0
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +83 -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/validate.sh +14 -0
- package/assets/payload/.delano/viewer/public/styles.css +1 -1
- 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/state.js +689 -0
- package/src/cli/commands/wrapper.js +16 -3
- package/src/cli/index.js +119 -7
- package/src/cli/lib/project-state.js +918 -0
|
@@ -602,7 +602,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; background: n
|
|
|
602
602
|
/* ---------- Workspace dashboard ---------- */
|
|
603
603
|
.project-grid {
|
|
604
604
|
display: grid;
|
|
605
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
605
|
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
606
606
|
gap: 14px;
|
|
607
607
|
padding-top: 16px;
|
|
608
608
|
}
|
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ delano install --yes
|
|
|
72
72
|
delano viewer
|
|
73
73
|
delano validate
|
|
74
74
|
delano init <slug> "<Project Name>" [owner] [lead]
|
|
75
|
+
delano import-spec-kit <slug> <source-md> [--name <project-name>] [--owner <owner>] [--lead <lead>] [--json]
|
|
76
|
+
delano research <project-slug> <research-slug> [--title <title>] [--question <question>] [--json]
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
Command intent:
|
|
@@ -80,6 +82,8 @@ Command intent:
|
|
|
80
82
|
- `delano viewer` launches the read-only local UI for `.project` contracts
|
|
81
83
|
- `delano validate` checks whether the runtime and required assets are in place
|
|
82
84
|
- `delano init` creates a delivery project inside a repository that already has Delano installed
|
|
85
|
+
- `delano import-spec-kit` creates a planned Delano project from the first supported Spec Kit-style markdown fixture and then runs validation
|
|
86
|
+
- `delano research` creates repo-native research intake files inside an existing Delano project and then runs validation
|
|
83
87
|
|
|
84
88
|
`delano init` usage:
|
|
85
89
|
|
|
@@ -94,8 +98,36 @@ Notes:
|
|
|
94
98
|
- `lead` defaults to `owner`
|
|
95
99
|
- this is the right command for an agent to scaffold a new delivery project after `delano install`
|
|
96
100
|
|
|
101
|
+
`delano import-spec-kit` usage:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
delano import-spec-kit <slug> <source-md> [--name <project-name>] [--owner <owner>] [--lead <lead>] [--json]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Notes:
|
|
108
|
+
|
|
109
|
+
- the source markdown must use the initial supported shape documented in `docs/spec-kit/import-contract.md`
|
|
110
|
+
- agents should prefer named options over positional metadata, and `--json` when parsing the result
|
|
111
|
+
- imported artifacts start in planned/ready states and still have to pass Delano validation, probe, and evidence gates
|
|
112
|
+
- the command is additive and refuses to overwrite an existing `.project/projects/<slug>/` folder
|
|
113
|
+
|
|
114
|
+
`delano research` usage:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
delano research <project-slug> <research-slug> [--title <title>] [--question <question>] [--owner <owner>] [--json]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Notes:
|
|
121
|
+
|
|
122
|
+
- use this before mutating `spec.md`, `plan.md`, workstreams, or tasks when intent is unclear
|
|
123
|
+
- research files live under `.project/projects/<project-slug>/research/<research-slug>/`
|
|
124
|
+
- agents should use `--json` when parsing the result
|
|
125
|
+
- research findings must fold forward into canonical Delano artifacts or be explicitly closed as no-action
|
|
126
|
+
|
|
97
127
|
## How to use Delano after install
|
|
98
128
|
|
|
129
|
+
For a fast guided path, start with [Delano in the First 15 Minutes](docs/first-15-minutes.md).
|
|
130
|
+
|
|
99
131
|
Recommended first step:
|
|
100
132
|
|
|
101
133
|
```bash
|
|
@@ -125,6 +157,8 @@ delano status
|
|
|
125
157
|
delano status --open --brief
|
|
126
158
|
delano next -- --all
|
|
127
159
|
delano init <slug> "<Project Name>" [owner] [lead]
|
|
160
|
+
delano import-spec-kit <slug> <source-md> --json
|
|
161
|
+
delano research <project-slug> <research-slug> --title "Research title" --question "Primary question" --json
|
|
128
162
|
```
|
|
129
163
|
|
|
130
164
|
The wrapper commands call the existing PM scripts under `.agents/scripts/pm/`. You can also run those scripts directly:
|
|
@@ -134,6 +168,7 @@ bash .agents/scripts/pm/validate.sh
|
|
|
134
168
|
bash .agents/scripts/pm/status.sh
|
|
135
169
|
bash .agents/scripts/pm/status.sh --open --brief
|
|
136
170
|
bash .agents/scripts/pm/next.sh --all
|
|
171
|
+
bash .agents/scripts/pm/research.sh <project-slug> <research-slug> --title "Research title" --question "Primary question" --json
|
|
137
172
|
```
|
|
138
173
|
|
|
139
174
|
The viewer is packaged with `@bvdm/delano` and serves the selected repository's `.project` files read-only. It defaults to `http://127.0.0.1:3977`; set `DELANO_VIEWER_PORT` or `PORT` to use another port. It indexes `.project/context`, `.project/templates`, and `.project/projects`, then derives artifact roles, statuses, project outlines, task/workstream relationships, snippets, and rendered markdown for local inspection.
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
".agents/README.md",
|
|
5
5
|
".agents/adapters/claude/README.md",
|
|
6
6
|
".agents/adapters/codex/README.md",
|
|
7
|
+
".agents/adapters/manifest.schema.json",
|
|
7
8
|
".agents/adapters/opencode/README.md",
|
|
8
9
|
".agents/adapters/pi/README.md",
|
|
10
|
+
".agents/adapters/spec-kit/adapter.json",
|
|
9
11
|
".agents/common/README.md",
|
|
10
12
|
".agents/common/log-safety.js",
|
|
11
13
|
".agents/eval-fixtures/skill-output/invalid/missing-evidence/output.json",
|
|
@@ -88,8 +90,10 @@
|
|
|
88
90
|
".agents/scripts/pm/epic-list.sh",
|
|
89
91
|
".agents/scripts/pm/in-progress.sh",
|
|
90
92
|
".agents/scripts/pm/init.sh",
|
|
93
|
+
".agents/scripts/pm/import-spec-kit.sh",
|
|
91
94
|
".agents/scripts/pm/next.sh",
|
|
92
95
|
".agents/scripts/pm/prd-list.sh",
|
|
96
|
+
".agents/scripts/pm/research.sh",
|
|
93
97
|
".agents/scripts/pm/search.sh",
|
|
94
98
|
".agents/scripts/pm/standup.sh",
|
|
95
99
|
".agents/scripts/pm/status.sh",
|
|
@@ -170,6 +174,7 @@
|
|
|
170
174
|
".project/registry/linear-map.json",
|
|
171
175
|
".project/registry/migration-map.json",
|
|
172
176
|
".project/templates/completion-summary.md",
|
|
177
|
+
".project/templates/decisions.md",
|
|
173
178
|
".project/templates/plan.md",
|
|
174
179
|
".project/templates/progress-update.md",
|
|
175
180
|
".project/templates/spec.md",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://delano.dev/schemas/adapter-manifest.schema.json",
|
|
4
|
+
"title": "Delano Adapter Manifest",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"id",
|
|
9
|
+
"name",
|
|
10
|
+
"type",
|
|
11
|
+
"owner",
|
|
12
|
+
"status",
|
|
13
|
+
"summary",
|
|
14
|
+
"commands",
|
|
15
|
+
"generated_files",
|
|
16
|
+
"validation",
|
|
17
|
+
"install",
|
|
18
|
+
"limits"
|
|
19
|
+
],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
|
|
24
|
+
},
|
|
25
|
+
"name": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1
|
|
28
|
+
},
|
|
29
|
+
"type": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": ["agent", "authoring-tool", "sync-tool", "workflow"]
|
|
32
|
+
},
|
|
33
|
+
"owner": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
},
|
|
37
|
+
"status": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"enum": ["proposed", "experimental", "stable", "deprecated"]
|
|
40
|
+
},
|
|
41
|
+
"summary": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1
|
|
44
|
+
},
|
|
45
|
+
"commands": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"minItems": 1,
|
|
48
|
+
"items": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"required": ["name", "description", "input", "output", "writes", "validation"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"name": { "type": "string", "minLength": 1 },
|
|
54
|
+
"description": { "type": "string", "minLength": 1 },
|
|
55
|
+
"input": { "type": "array", "items": { "type": "string" } },
|
|
56
|
+
"output": { "type": "array", "items": { "type": "string" } },
|
|
57
|
+
"writes": { "type": "array", "items": { "type": "string" } },
|
|
58
|
+
"validation": { "type": "array", "items": { "type": "string" } }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"generated_files": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"required": ["path", "owner", "mode", "conflict_behavior", "fold_forward"],
|
|
68
|
+
"properties": {
|
|
69
|
+
"path": { "type": "string", "minLength": 1 },
|
|
70
|
+
"owner": { "type": "string", "minLength": 1 },
|
|
71
|
+
"mode": { "type": "string", "enum": ["create-only", "update-owned", "proposal-only", "never-overwrite"] },
|
|
72
|
+
"conflict_behavior": { "type": "string", "enum": ["abort", "diff-required", "operator-approval-required"] },
|
|
73
|
+
"fold_forward": { "type": "string", "minLength": 1 }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"validation": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"minItems": 1,
|
|
80
|
+
"items": { "type": "string" }
|
|
81
|
+
},
|
|
82
|
+
"install": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"required": ["categories", "conflict_policy"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"categories": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": { "type": "string" }
|
|
90
|
+
},
|
|
91
|
+
"conflict_policy": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"minLength": 1
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"limits": {
|
|
98
|
+
"type": "array",
|
|
99
|
+
"minItems": 1,
|
|
100
|
+
"items": { "type": "string" }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "spec-kit",
|
|
3
|
+
"name": "Spec Kit Interop",
|
|
4
|
+
"type": "authoring-tool",
|
|
5
|
+
"owner": "delano-team",
|
|
6
|
+
"status": "experimental",
|
|
7
|
+
"summary": "Imports Spec Kit-style intent artifacts into Delano-governed delivery projects.",
|
|
8
|
+
"commands": [
|
|
9
|
+
{
|
|
10
|
+
"name": "delano import-spec-kit",
|
|
11
|
+
"description": "Create a planned Delano project from a supported Spec Kit-style markdown artifact.",
|
|
12
|
+
"input": ["slug", "source-md", "--name", "--owner", "--lead", "--json"],
|
|
13
|
+
"output": ["human summary", "JSON result with ok, command, project, source, validation"],
|
|
14
|
+
"writes": [".project/projects/<slug>/"],
|
|
15
|
+
"validation": ["delano validate"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "delano research",
|
|
19
|
+
"description": "Open repo-native research intake for unclear imported intent.",
|
|
20
|
+
"input": ["project-slug", "research-slug", "--title", "--question", "--json"],
|
|
21
|
+
"output": ["human summary", "JSON result with ok, command, project, research, files, validation"],
|
|
22
|
+
"writes": [".project/projects/<project-slug>/research/<research-slug>/"],
|
|
23
|
+
"validation": ["delano validate"]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"generated_files": [
|
|
27
|
+
{
|
|
28
|
+
"path": ".project/projects/<slug>/spec.md",
|
|
29
|
+
"owner": "spec-kit adapter",
|
|
30
|
+
"mode": "create-only",
|
|
31
|
+
"conflict_behavior": "abort",
|
|
32
|
+
"fold_forward": "canonical spec"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": ".project/projects/<slug>/plan.md",
|
|
36
|
+
"owner": "spec-kit adapter",
|
|
37
|
+
"mode": "create-only",
|
|
38
|
+
"conflict_behavior": "abort",
|
|
39
|
+
"fold_forward": "canonical plan"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"path": ".project/projects/<slug>/tasks/*.md",
|
|
43
|
+
"owner": "spec-kit adapter",
|
|
44
|
+
"mode": "create-only",
|
|
45
|
+
"conflict_behavior": "abort",
|
|
46
|
+
"fold_forward": "canonical tasks with evidence gates"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"path": ".project/projects/<project-slug>/research/<research-slug>/",
|
|
50
|
+
"owner": "research intake",
|
|
51
|
+
"mode": "create-only",
|
|
52
|
+
"conflict_behavior": "abort",
|
|
53
|
+
"fold_forward": "spec, plan, decisions, workstreams, tasks, or updates"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"validation": [
|
|
57
|
+
"delano validate",
|
|
58
|
+
"npm run check:text-safety for Delano repo changes",
|
|
59
|
+
"fixture import smoke before release"
|
|
60
|
+
],
|
|
61
|
+
"install": {
|
|
62
|
+
"categories": ["agent-runtime", "project-templates", "skills"],
|
|
63
|
+
"conflict_policy": "Use existing Delano install allowlist behavior; abort on generated project collisions unless an operator approves a diff-backed change."
|
|
64
|
+
},
|
|
65
|
+
"limits": [
|
|
66
|
+
"Does not replace Spec Kit.",
|
|
67
|
+
"Does not execute imported tasks automatically.",
|
|
68
|
+
"Does not write Linear or GitHub state directly.",
|
|
69
|
+
"Does not depend on Obsidian, OpenClaw, or private local paths."
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -39,6 +39,23 @@
|
|
|
39
39
|
"plan.status is done or deferred"
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
"id": "progressed-task-requires-active-workstream",
|
|
44
|
+
"status": "in-progress|done",
|
|
45
|
+
"description": "A task must not start or complete before its parent workstream has started.",
|
|
46
|
+
"requires": [
|
|
47
|
+
"in-progress tasks require workstream.status active",
|
|
48
|
+
"done tasks require workstream.status active or done"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "closed-task-set-requires-closed-workstream",
|
|
53
|
+
"status": "done|deferred",
|
|
54
|
+
"description": "A workstream with no open tasks must not remain open through a stale status.",
|
|
55
|
+
"requires": [
|
|
56
|
+
"workstream.status is done or deferred"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
42
59
|
{
|
|
43
60
|
"id": "blocked-owner-check-back",
|
|
44
61
|
"status": "blocked",
|
|
@@ -19,7 +19,9 @@ for (const requiredRule of [
|
|
|
19
19
|
"ready-dependencies-done",
|
|
20
20
|
"blocked-owner-check-back",
|
|
21
21
|
"progressed-task-requires-active-project",
|
|
22
|
-
"closed-task-set-requires-closed-project"
|
|
22
|
+
"closed-task-set-requires-closed-project",
|
|
23
|
+
"progressed-task-requires-active-workstream",
|
|
24
|
+
"closed-task-set-requires-closed-workstream"
|
|
23
25
|
]) {
|
|
24
26
|
if (!rules.some((rule) => rule.id === requiredRule)) {
|
|
25
27
|
errors.push(`status transition contract missing rule: ${requiredRule}`);
|
|
@@ -38,6 +40,15 @@ for (const projectDir of listDirectories(projectsRoot)) {
|
|
|
38
40
|
const specFrontmatter = existsSync(specPath) ? parseFrontmatter(specPath) : null;
|
|
39
41
|
const planFrontmatter = existsSync(planPath) ? parseFrontmatter(planPath) : null;
|
|
40
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
|
+
}
|
|
41
52
|
const tasksDir = path.join(projectDir, "tasks");
|
|
42
53
|
if (!existsSync(tasksDir)) continue;
|
|
43
54
|
|
|
@@ -52,6 +63,12 @@ for (const projectDir of listDirectories(projectsRoot)) {
|
|
|
52
63
|
totalTaskCount += 1;
|
|
53
64
|
if (!isClosedTaskStatus(status)) openTaskCount += 1;
|
|
54
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
|
+
}
|
|
55
72
|
tasks.set(id, { file: taskFile, frontmatter });
|
|
56
73
|
}
|
|
57
74
|
|
|
@@ -69,6 +86,18 @@ for (const projectDir of listDirectories(projectsRoot)) {
|
|
|
69
86
|
for (const [taskId, task] of tasks.entries()) {
|
|
70
87
|
const status = task.frontmatter.status || "";
|
|
71
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
|
+
}
|
|
72
101
|
|
|
73
102
|
if (["ready", "in-progress", "done"].includes(status)) {
|
|
74
103
|
for (const dependencyId of dependencies) {
|
|
@@ -90,6 +119,10 @@ for (const projectDir of listDirectories(projectsRoot)) {
|
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
121
|
}
|
|
122
|
+
|
|
123
|
+
for (const summary of workstreamSummaries.values()) {
|
|
124
|
+
validateWorkstreamLifecycle(summary);
|
|
125
|
+
}
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
finish();
|
|
@@ -105,7 +138,8 @@ function parseTransitionArgs(args) {
|
|
|
105
138
|
const blockedCheckBack = valueAfter(args, "--blocked-check-back");
|
|
106
139
|
const specStatus = valueAfter(args, "--spec-status");
|
|
107
140
|
const planStatus = valueAfter(args, "--plan-status");
|
|
108
|
-
|
|
141
|
+
const workstreamStatus = valueAfter(args, "--workstream-status");
|
|
142
|
+
return { nextStatus, dependencyStatuses, blockedOwner, blockedCheckBack, specStatus, planStatus, workstreamStatus };
|
|
109
143
|
}
|
|
110
144
|
|
|
111
145
|
function validateTransitionRequest(request) {
|
|
@@ -124,6 +158,12 @@ function validateTransitionRequest(request) {
|
|
|
124
158
|
if (request.planStatus && !isActiveOrClosedPlanStatus(request.planStatus)) {
|
|
125
159
|
errors.push(`cannot transition to ${request.nextStatus} while plan status is ${request.planStatus}; expected active or done`);
|
|
126
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
|
+
}
|
|
127
167
|
}
|
|
128
168
|
|
|
129
169
|
if (request.nextStatus === "blocked") {
|
|
@@ -153,6 +193,24 @@ function validateProjectLifecycle(request) {
|
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
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
|
+
|
|
156
214
|
function isProgressedTaskStatus(status) {
|
|
157
215
|
return ["in-progress", "done"].includes(status);
|
|
158
216
|
}
|
|
@@ -177,6 +235,18 @@ function isClosedPlanStatus(status) {
|
|
|
177
235
|
return ["done", "deferred"].includes(status);
|
|
178
236
|
}
|
|
179
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
|
+
|
|
180
250
|
function describeStatus(status) {
|
|
181
251
|
return status || "missing status";
|
|
182
252
|
}
|
|
@@ -244,6 +314,17 @@ function listMarkdownFiles(root) {
|
|
|
244
314
|
.map((entry) => path.join(root, entry.name));
|
|
245
315
|
}
|
|
246
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
|
+
|
|
247
328
|
function resolveRepoRoot(startDir) {
|
|
248
329
|
const candidates = [path.resolve(startDir, ".."), path.resolve(startDir, "..", "..")];
|
|
249
330
|
for (const candidate of candidates) {
|