@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.
@@ -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(260px, 1fr));
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
- return { nextStatus, dependencyStatuses, blockedOwner, blockedCheckBack, specStatus, planStatus };
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) {