@bvdm/delano 0.2.3 → 0.2.4
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 +28 -3
- package/assets/install-manifest.json +2 -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 +18 -0
- package/assets/payload/.agents/scripts/README.md +1 -1
- package/assets/payload/.agents/scripts/check-status-transitions.mjs +90 -2
- package/assets/payload/.agents/scripts/pm/status.sh +135 -28
- package/assets/payload/.agents/scripts/pm/validate.sh +2 -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/package.json +1 -1
- package/src/cli/commands/install.js +2 -1
- package/src/cli/commands/viewer.js +2 -1
- package/src/cli/commands/wrapper.js +13 -2
- package/src/cli/index.js +1 -0
- package/src/cli/lib/install.js +179 -2
package/.delano/viewer/server.js
CHANGED
|
@@ -12,7 +12,16 @@ const { spawn, spawnSync } = require('node:child_process');
|
|
|
12
12
|
const repoRoot = path.resolve(process.env.DELANO_VIEWER_ROOT || path.resolve(__dirname, '..', '..'));
|
|
13
13
|
const projectRoot = path.join(repoRoot, '.project');
|
|
14
14
|
const publicRoot = path.join(__dirname, 'public');
|
|
15
|
-
const
|
|
15
|
+
const DEFAULT_PORT = 3977;
|
|
16
|
+
const MAX_PORT = 65535;
|
|
17
|
+
const MAX_PORT_ATTEMPTS = 100;
|
|
18
|
+
const startPort = normalizePort(process.env.DELANO_VIEWER_PORT || process.env.PORT, DEFAULT_PORT);
|
|
19
|
+
|
|
20
|
+
function normalizePort(value, fallback) {
|
|
21
|
+
const parsed = Number(value || fallback);
|
|
22
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_PORT) return fallback;
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
function isInside(parent, child) {
|
|
18
27
|
const rel = path.relative(parent, child);
|
|
@@ -332,6 +341,7 @@ function sendStatic(res, pathname) {
|
|
|
332
341
|
const ext = path.extname(resolved).toLowerCase();
|
|
333
342
|
const mimeMap = {
|
|
334
343
|
'.js': 'text/javascript',
|
|
344
|
+
'.jsx': 'text/javascript',
|
|
335
345
|
'.css': 'text/css',
|
|
336
346
|
'.svg': 'image/svg+xml',
|
|
337
347
|
'.png': 'image/png',
|
|
@@ -340,7 +350,7 @@ function sendStatic(res, pathname) {
|
|
|
340
350
|
'.webp': 'image/webp',
|
|
341
351
|
'.ico': 'image/x-icon',
|
|
342
352
|
};
|
|
343
|
-
const isText = ext === '.js' || ext === '.css' || ext === '.svg' || ext === '' || ext === '.html';
|
|
353
|
+
const isText = ext === '.js' || ext === '.jsx' || ext === '.css' || ext === '.svg' || ext === '' || ext === '.html';
|
|
344
354
|
const type = mimeMap[ext] || 'text/html';
|
|
345
355
|
const headers = isText ? { 'content-type': `${type}; charset=utf-8` } : { 'content-type': type };
|
|
346
356
|
res.writeHead(200, headers);
|
|
@@ -384,6 +394,37 @@ const server = http.createServer((req, res) => {
|
|
|
384
394
|
}
|
|
385
395
|
});
|
|
386
396
|
|
|
387
|
-
server
|
|
388
|
-
|
|
389
|
-
|
|
397
|
+
function listenWithPortFallback(server, firstPort, host = '127.0.0.1') {
|
|
398
|
+
let port = firstPort;
|
|
399
|
+
let attempts = 0;
|
|
400
|
+
|
|
401
|
+
const listen = () => {
|
|
402
|
+
server.once('error', onError);
|
|
403
|
+
server.listen(port, host);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const onError = (error) => {
|
|
407
|
+
if (error.code === 'EADDRINUSE' && port < MAX_PORT && attempts < MAX_PORT_ATTEMPTS) {
|
|
408
|
+
attempts += 1;
|
|
409
|
+
port += 1;
|
|
410
|
+
listen();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.error(`Failed to start Delano viewer on ${host}:${port}: ${error.message}`);
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const onListening = () => {
|
|
419
|
+
server.removeListener('error', onError);
|
|
420
|
+
const address = server.address();
|
|
421
|
+
const actualPort = typeof address === 'object' && address ? address.port : port;
|
|
422
|
+
const skipped = actualPort !== firstPort ? ` (${firstPort} was unavailable)` : '';
|
|
423
|
+
console.log(`Delano read-only viewer: http://${host}:${actualPort}${skipped}`);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
server.on('listening', onListening);
|
|
427
|
+
listen();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
listenWithPortFallback(server, startPort);
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ The npm package is intentionally thin. It distributes the approved runtime paylo
|
|
|
15
15
|
## Delano CLI
|
|
16
16
|
|
|
17
17
|
- Package: `@bvdm/delano`
|
|
18
|
-
- Current package version: `0.2.
|
|
18
|
+
- Current package version: `0.2.4`
|
|
19
19
|
- Binary: `delano`
|
|
20
20
|
- Commands: `onboarding`, `install`, `viewer`, `init`, `validate`, `status`, `next`
|
|
21
21
|
- Primary goal: bootstrap a repo safely, expose local delivery state clearly, and keep runtime gates verifiable
|
|
@@ -111,6 +111,7 @@ npx -y @bvdm/delano@latest onboarding --approve-agents-analysis
|
|
|
111
111
|
npx -y @bvdm/delano@latest viewer
|
|
112
112
|
npx -y @bvdm/delano@latest validate
|
|
113
113
|
npx -y @bvdm/delano@latest status
|
|
114
|
+
npx -y @bvdm/delano@latest status --open --brief
|
|
114
115
|
npx -y @bvdm/delano@latest next -- --all
|
|
115
116
|
```
|
|
116
117
|
|
|
@@ -121,6 +122,7 @@ delano onboarding
|
|
|
121
122
|
delano viewer
|
|
122
123
|
delano validate
|
|
123
124
|
delano status
|
|
125
|
+
delano status --open --brief
|
|
124
126
|
delano next -- --all
|
|
125
127
|
delano init <slug> "<Project Name>" [owner] [lead]
|
|
126
128
|
```
|
|
@@ -130,6 +132,7 @@ The wrapper commands call the existing PM scripts under `.agents/scripts/pm/`. Y
|
|
|
130
132
|
```bash
|
|
131
133
|
bash .agents/scripts/pm/validate.sh
|
|
132
134
|
bash .agents/scripts/pm/status.sh
|
|
135
|
+
bash .agents/scripts/pm/status.sh --open --brief
|
|
133
136
|
bash .agents/scripts/pm/next.sh --all
|
|
134
137
|
```
|
|
135
138
|
|
|
@@ -160,6 +163,28 @@ The CLI does not bundle its own shell or Python runtime.
|
|
|
160
163
|
|
|
161
164
|
The base install payload intentionally excludes top-level adapter entry docs such as `AGENTS.md`, `CLAUDE.md`, `CODEX.md`, `OPENCODE.md`, and `PI.md`. Those remain opt-in only.
|
|
162
165
|
The base install payload includes `.delano/`, including the read-only viewer UI.
|
|
166
|
+
The base install payload also includes `.codex/hooks.json`, a Codex `SessionStart` hook config that injects compact open-project context when Codex hooks are enabled. If a target repo already has `.codex/hooks.json`, `delano install` merges the Delano hook into the existing JSON instead of replacing it. Invalid or non-file hook configs are skipped without blocking the rest of the install.
|
|
167
|
+
|
|
168
|
+
Codex hook activation is intentionally manual:
|
|
169
|
+
|
|
170
|
+
1. Enable hooks for a session with `codex --enable hooks`, or persist the feature in `~/.codex/config.toml`:
|
|
171
|
+
|
|
172
|
+
```toml
|
|
173
|
+
[features]
|
|
174
|
+
hooks = true
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
2. Start Codex in the repository and approve the project trust prompt for the repo-local `.codex/` layer. Codex records trusted projects in `~/.codex/config.toml`, for example:
|
|
178
|
+
|
|
179
|
+
```toml
|
|
180
|
+
[projects."E:\\path\\to\\repo"]
|
|
181
|
+
trust_level = "trusted"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
3. Approve the Delano `SessionStart` hook when Codex asks whether to trust it.
|
|
185
|
+
|
|
186
|
+
Older docs and builds may refer to `[features].codex_hooks`; newer Codex builds warn that this key is deprecated in favor of `[features].hooks`.
|
|
187
|
+
|
|
163
188
|
The installable `.project/context/` pack is seeded from generic templates during packaging; it does not ship Delano's own repo-specific context files into consumer repositories.
|
|
164
189
|
After install, the recommended first step is `delano onboarding`, which requires explicit approval before it reviews `AGENTS.md`.
|
|
165
190
|
|
|
@@ -174,7 +199,7 @@ delano install --no-project-state --force --yes
|
|
|
174
199
|
|
|
175
200
|
The interactive installer presents presets for updating the runtime while preserving project state, updating only skills and project templates, full install or repair, and custom category selection.
|
|
176
201
|
|
|
177
|
-
Install categories are `agent-runtime`, `skills`, `viewer`, `project-context`, `project-templates`, `project-registry`, `project-projects`, `handbook`, and `legacy-installer`. The `--no-project-state` shortcut excludes `.project/context`, `.project/projects`, and `.project/registry`.
|
|
202
|
+
Install categories are `agent-runtime`, `codex-hooks`, `skills`, `viewer`, `project-context`, `project-templates`, `project-registry`, `project-projects`, `handbook`, and `legacy-installer`. The `--no-project-state` shortcut excludes `.project/context`, `.project/projects`, and `.project/registry`.
|
|
178
203
|
|
|
179
204
|
## Optional AGENTS.md / CLAUDE.md snippet
|
|
180
205
|
|
|
@@ -254,7 +279,7 @@ Before the first Actions publish, configure npm trusted publishing for `@bvdm/de
|
|
|
254
279
|
|
|
255
280
|
The package metadata must keep `repository.url` set to `https://github.com/MajesteitBart/delano`; npm validates that value against the GitHub Actions provenance bundle.
|
|
256
281
|
|
|
257
|
-
After trusted publishing is configured, publish by pushing a matching version tag such as `v0.2.
|
|
282
|
+
After trusted publishing is configured, publish by pushing a matching version tag such as `v0.2.4`, or run the `Publish package to npm` workflow manually from `main`. The workflow rebuilds the package payload, checks manifest drift, runs tests, dry-runs the package contents, verifies the version is not already published, and then runs `npm publish --access public` from GitHub Actions using OIDC. A manual `dry_run` input is available to run the same checks without publishing.
|
|
258
283
|
|
|
259
284
|
If npm publish fails after the package checks pass, verify that the npm trusted publisher settings match the repository and workflow filename exactly, and that the workflow has `id-token: write`.
|
|
260
285
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
".agents/fixtures/linear/issue-snapshot.json",
|
|
15
15
|
".agents/hooks/README.md",
|
|
16
16
|
".agents/hooks/bash-worktree-fix.sh",
|
|
17
|
+
".agents/hooks/codex-session-status.js",
|
|
17
18
|
".agents/hooks/post-tool-logger.js",
|
|
18
19
|
".agents/hooks/session-tracker.js",
|
|
19
20
|
".agents/hooks/user-prompt-logger.js",
|
|
@@ -155,6 +156,7 @@
|
|
|
155
156
|
".agents/validation-fixtures/strict/invalid/stale-context/context.md",
|
|
156
157
|
".agents/validation-fixtures/strict/manifest.json",
|
|
157
158
|
".agents/validation-fixtures/strict/valid/minimal-project/task.md",
|
|
159
|
+
".codex/hooks.json",
|
|
158
160
|
".delano/README.md",
|
|
159
161
|
".delano/viewer/README.md",
|
|
160
162
|
".delano/viewer/server.js",
|
|
@@ -7,5 +7,10 @@ Default hooks:
|
|
|
7
7
|
- `post-tool-logger.js`
|
|
8
8
|
- `user-prompt-logger.js`
|
|
9
9
|
- `bash-worktree-fix.sh`
|
|
10
|
+
- `codex-session-status.js`
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
The logging hooks append JSONL records in `.agents/logs/`.
|
|
13
|
+
|
|
14
|
+
`codex-session-status.js` is used by the optional `.codex/hooks.json` SessionStart
|
|
15
|
+
configuration. It emits `delano status --open --brief` context and fails open if
|
|
16
|
+
the local runtime is not available.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { existsSync } = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
|
|
6
|
+
function findDelanoRoot(startDir) {
|
|
7
|
+
let current = path.resolve(startDir);
|
|
8
|
+
while (true) {
|
|
9
|
+
if (
|
|
10
|
+
existsSync(path.join(current, ".project", "projects")) &&
|
|
11
|
+
existsSync(path.join(current, ".agents", "scripts", "pm", "status.sh"))
|
|
12
|
+
) {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parent = path.dirname(current);
|
|
17
|
+
if (parent === current) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toBashPath(filePath) {
|
|
25
|
+
return filePath.replace(/\\/g, "/");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveBash() {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
|
|
31
|
+
if (process.env.DELANO_BASH) {
|
|
32
|
+
candidates.push(process.env.DELANO_BASH);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (process.platform === "win32") {
|
|
36
|
+
candidates.push(
|
|
37
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
38
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe"
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const whereResult = spawnSync("where.exe", ["bash"], {
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
44
|
+
});
|
|
45
|
+
if (whereResult.status === 0 && whereResult.stdout) {
|
|
46
|
+
candidates.push(...whereResult.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
const whichResult = spawnSync("which", ["bash"], {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
52
|
+
});
|
|
53
|
+
if (whichResult.status === 0 && whichResult.stdout.trim()) {
|
|
54
|
+
candidates.push(whichResult.stdout.trim());
|
|
55
|
+
}
|
|
56
|
+
candidates.push("/usr/bin/bash", "/bin/bash");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return candidates.find((candidate) => candidate && existsSync(candidate)) || null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const root = findDelanoRoot(process.cwd());
|
|
63
|
+
const bashPath = resolveBash();
|
|
64
|
+
if (!root || !bashPath) {
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const statusScript = toBashPath(path.join(root, ".agents", "scripts", "pm", "status.sh"));
|
|
69
|
+
const result = spawnSync(bashPath, [statusScript, "--open", "--brief"], {
|
|
70
|
+
cwd: root,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
timeout: 4500,
|
|
73
|
+
maxBuffer: 64 * 1024
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.error || result.status !== 0) {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const statusOutput = result.stdout.trim();
|
|
81
|
+
if (!statusOutput) {
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const additionalContext = formatStatusContext(statusOutput);
|
|
86
|
+
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
hookSpecificOutput: {
|
|
89
|
+
hookEventName: "SessionStart",
|
|
90
|
+
additionalContext
|
|
91
|
+
}
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
function formatStatusContext(rawStatusOutput) {
|
|
95
|
+
const lines = rawStatusOutput.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
96
|
+
const projectLines = lines.filter((line) => (
|
|
97
|
+
!line.startsWith("Delano ") &&
|
|
98
|
+
!/^=+$/.test(line) &&
|
|
99
|
+
!line.startsWith("No open projects")
|
|
100
|
+
));
|
|
101
|
+
|
|
102
|
+
if (projectLines.length === 0) {
|
|
103
|
+
return "Delano startup context. Open projects: none.";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const projects = projectLines.map(formatProjectLine);
|
|
107
|
+
return `Delano startup context. Open projects: ${projects.join("; ")}.`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatProjectLine(line) {
|
|
111
|
+
const match = line.match(/^(\S+)\s+spec=(\S+)\s+plan=(\S+)\s+open_tasks=(\d+)\s+total_tasks=(\d+)$/);
|
|
112
|
+
if (!match) {
|
|
113
|
+
return line;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const [, slug, spec, plan, openTasks, totalTasks] = match;
|
|
117
|
+
return `${slug} (spec=${spec}, plan=${plan}, open_tasks=${openTasks}, total_tasks=${totalTasks})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
formatProjectLine,
|
|
122
|
+
formatStatusContext
|
|
123
|
+
};
|
|
@@ -21,6 +21,24 @@
|
|
|
21
21
|
"description": "A done task must not close over unresolved local dependencies.",
|
|
22
22
|
"requires": ["all local depends_on task statuses are done"]
|
|
23
23
|
},
|
|
24
|
+
{
|
|
25
|
+
"id": "progressed-task-requires-active-project",
|
|
26
|
+
"status": "in-progress|done",
|
|
27
|
+
"description": "A task must not start or complete while the parent project spec or plan is still planned.",
|
|
28
|
+
"requires": [
|
|
29
|
+
"spec.status is active or complete",
|
|
30
|
+
"plan.status is active or done"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "closed-task-set-requires-closed-project",
|
|
35
|
+
"status": "done|deferred",
|
|
36
|
+
"description": "A project with no open tasks must not remain open through stale spec or plan statuses.",
|
|
37
|
+
"requires": [
|
|
38
|
+
"spec.status is complete or deferred",
|
|
39
|
+
"plan.status is done or deferred"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
24
42
|
{
|
|
25
43
|
"id": "blocked-owner-check-back",
|
|
26
44
|
"status": "blocked",
|
|
@@ -15,7 +15,12 @@ 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
|
+
]) {
|
|
19
24
|
if (!rules.some((rule) => rule.id === requiredRule)) {
|
|
20
25
|
errors.push(`status transition contract missing rule: ${requiredRule}`);
|
|
21
26
|
}
|
|
@@ -28,16 +33,39 @@ if (transitionRequest) {
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
for (const projectDir of listDirectories(projectsRoot)) {
|
|
36
|
+
const specPath = path.join(projectDir, "spec.md");
|
|
37
|
+
const planPath = path.join(projectDir, "plan.md");
|
|
38
|
+
const specFrontmatter = existsSync(specPath) ? parseFrontmatter(specPath) : null;
|
|
39
|
+
const planFrontmatter = existsSync(planPath) ? parseFrontmatter(planPath) : null;
|
|
40
|
+
const hasProjectLifecycle = Boolean(specFrontmatter || planFrontmatter);
|
|
31
41
|
const tasksDir = path.join(projectDir, "tasks");
|
|
32
42
|
if (!existsSync(tasksDir)) continue;
|
|
33
43
|
|
|
34
44
|
const tasks = new Map();
|
|
45
|
+
let totalTaskCount = 0;
|
|
46
|
+
let openTaskCount = 0;
|
|
47
|
+
let progressedTaskCount = 0;
|
|
35
48
|
for (const taskFile of listMarkdownFiles(tasksDir)) {
|
|
36
49
|
const frontmatter = parseFrontmatter(taskFile);
|
|
37
50
|
const id = frontmatter.id || path.basename(taskFile, ".md").split("-").slice(0, 2).join("-");
|
|
51
|
+
const status = frontmatter.status || "";
|
|
52
|
+
totalTaskCount += 1;
|
|
53
|
+
if (!isClosedTaskStatus(status)) openTaskCount += 1;
|
|
54
|
+
if (isProgressedTaskStatus(status)) progressedTaskCount += 1;
|
|
38
55
|
tasks.set(id, { file: taskFile, frontmatter });
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
if (hasProjectLifecycle) {
|
|
59
|
+
validateProjectLifecycle({
|
|
60
|
+
projectDir,
|
|
61
|
+
specStatus: specFrontmatter?.status || "",
|
|
62
|
+
planStatus: planFrontmatter?.status || "",
|
|
63
|
+
totalTaskCount,
|
|
64
|
+
openTaskCount,
|
|
65
|
+
progressedTaskCount
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
for (const [taskId, task] of tasks.entries()) {
|
|
42
70
|
const status = task.frontmatter.status || "";
|
|
43
71
|
const dependencies = parseList(task.frontmatter.depends_on || "[]");
|
|
@@ -75,7 +103,9 @@ function parseTransitionArgs(args) {
|
|
|
75
103
|
.filter(Boolean);
|
|
76
104
|
const blockedOwner = valueAfter(args, "--blocked-owner");
|
|
77
105
|
const blockedCheckBack = valueAfter(args, "--blocked-check-back");
|
|
78
|
-
|
|
106
|
+
const specStatus = valueAfter(args, "--spec-status");
|
|
107
|
+
const planStatus = valueAfter(args, "--plan-status");
|
|
108
|
+
return { nextStatus, dependencyStatuses, blockedOwner, blockedCheckBack, specStatus, planStatus };
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
function validateTransitionRequest(request) {
|
|
@@ -87,12 +117,70 @@ function validateTransitionRequest(request) {
|
|
|
87
117
|
}
|
|
88
118
|
}
|
|
89
119
|
|
|
120
|
+
if (["in-progress", "done"].includes(request.nextStatus)) {
|
|
121
|
+
if (request.specStatus && !isActiveOrClosedSpecStatus(request.specStatus)) {
|
|
122
|
+
errors.push(`cannot transition to ${request.nextStatus} while spec status is ${request.specStatus}; expected active or complete`);
|
|
123
|
+
}
|
|
124
|
+
if (request.planStatus && !isActiveOrClosedPlanStatus(request.planStatus)) {
|
|
125
|
+
errors.push(`cannot transition to ${request.nextStatus} while plan status is ${request.planStatus}; expected active or done`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
90
129
|
if (request.nextStatus === "blocked") {
|
|
91
130
|
if (!request.blockedOwner) errors.push("cannot transition to blocked without blocked_owner");
|
|
92
131
|
if (!request.blockedCheckBack) errors.push("cannot transition to blocked without blocked_check_back");
|
|
93
132
|
}
|
|
94
133
|
}
|
|
95
134
|
|
|
135
|
+
function validateProjectLifecycle(request) {
|
|
136
|
+
const projectPath = toRepoPath(request.projectDir);
|
|
137
|
+
if (request.progressedTaskCount > 0) {
|
|
138
|
+
if (!isActiveOrClosedSpecStatus(request.specStatus)) {
|
|
139
|
+
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.`);
|
|
140
|
+
}
|
|
141
|
+
if (!isActiveOrClosedPlanStatus(request.planStatus)) {
|
|
142
|
+
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.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (request.totalTaskCount > 0 && request.openTaskCount === 0) {
|
|
147
|
+
if (!isClosedSpecStatus(request.specStatus)) {
|
|
148
|
+
errors.push(`${projectPath} has no open tasks but spec.md status is ${describeStatus(request.specStatus)}; expected complete or deferred.`);
|
|
149
|
+
}
|
|
150
|
+
if (!isClosedPlanStatus(request.planStatus)) {
|
|
151
|
+
errors.push(`${projectPath} has no open tasks but plan.md status is ${describeStatus(request.planStatus)}; expected done or deferred.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isProgressedTaskStatus(status) {
|
|
157
|
+
return ["in-progress", "done"].includes(status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isClosedTaskStatus(status) {
|
|
161
|
+
return ["done", "deferred", "canceled"].includes(status);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isActiveOrClosedSpecStatus(status) {
|
|
165
|
+
return ["active", "complete"].includes(status);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isActiveOrClosedPlanStatus(status) {
|
|
169
|
+
return ["active", "done"].includes(status);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isClosedSpecStatus(status) {
|
|
173
|
+
return ["complete", "deferred"].includes(status);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isClosedPlanStatus(status) {
|
|
177
|
+
return ["done", "deferred"].includes(status);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function describeStatus(status) {
|
|
181
|
+
return status || "missing status";
|
|
182
|
+
}
|
|
183
|
+
|
|
96
184
|
function valueAfter(args, flag) {
|
|
97
185
|
const index = args.indexOf(flag);
|
|
98
186
|
if (index === -1 || index === args.length - 1) return "";
|
|
@@ -4,24 +4,92 @@ set -euo pipefail
|
|
|
4
4
|
root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
5
5
|
cd "$root"
|
|
6
6
|
|
|
7
|
+
open_only=false
|
|
8
|
+
brief=false
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<'EOF'
|
|
12
|
+
Usage:
|
|
13
|
+
status.sh [--open] [--brief]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--open Show only projects that are not closed.
|
|
17
|
+
--brief Show one compact line per project.
|
|
18
|
+
-h, --help
|
|
19
|
+
Show this help.
|
|
20
|
+
EOF
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--open)
|
|
26
|
+
open_only=true
|
|
27
|
+
;;
|
|
28
|
+
--brief)
|
|
29
|
+
brief=true
|
|
30
|
+
;;
|
|
31
|
+
-h|--help)
|
|
32
|
+
usage
|
|
33
|
+
exit 0
|
|
34
|
+
;;
|
|
35
|
+
*)
|
|
36
|
+
echo "Unknown status option: $1" >&2
|
|
37
|
+
usage >&2
|
|
38
|
+
exit 1
|
|
39
|
+
;;
|
|
40
|
+
esac
|
|
41
|
+
shift
|
|
42
|
+
done
|
|
43
|
+
|
|
7
44
|
fm_get() {
|
|
8
45
|
local file="$1"
|
|
9
46
|
local key="$2"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
47
|
+
local line
|
|
48
|
+
local in_fm=0
|
|
49
|
+
while IFS= read -r line; do
|
|
50
|
+
if [[ "$line" =~ ^---[[:space:]]*$ ]]; then
|
|
51
|
+
if [[ $in_fm -eq 0 ]]; then
|
|
52
|
+
in_fm=1
|
|
53
|
+
continue
|
|
54
|
+
fi
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ $in_fm -eq 1 && "$line" == "$key:"* ]]; then
|
|
59
|
+
local value="${line#"$key:"}"
|
|
60
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
61
|
+
printf '%s\n' "$value"
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
done < "$file"
|
|
65
|
+
return 1
|
|
19
66
|
}
|
|
20
67
|
|
|
21
|
-
|
|
22
|
-
|
|
68
|
+
is_closed_spec_status() {
|
|
69
|
+
local status="${1:-unknown}"
|
|
70
|
+
[[ "$status" == "complete" || "$status" == "deferred" ]]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
is_closed_plan_status() {
|
|
74
|
+
local status="${1:-unknown}"
|
|
75
|
+
[[ "$status" == "done" || "$status" == "deferred" ]]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
is_closed_task_status() {
|
|
79
|
+
local status="${1:-unknown}"
|
|
80
|
+
[[ "$status" == "done" || "$status" == "deferred" || "$status" == "canceled" ]]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if [[ "$open_only" == "true" ]]; then
|
|
84
|
+
echo "Delano open project status"
|
|
85
|
+
echo "=========================="
|
|
86
|
+
else
|
|
87
|
+
echo "Delano portfolio status"
|
|
88
|
+
echo "======================="
|
|
89
|
+
fi
|
|
23
90
|
|
|
24
91
|
project_count=0
|
|
92
|
+
printed_count=0
|
|
25
93
|
for project_dir in .project/projects/*; do
|
|
26
94
|
[[ -d "$project_dir" ]] || continue
|
|
27
95
|
[[ "$(basename "$project_dir")" == ".gitkeep" ]] && continue
|
|
@@ -31,31 +99,70 @@ for project_dir in .project/projects/*; do
|
|
|
31
99
|
spec_status="$(fm_get "$project_dir/spec.md" status 2>/dev/null || true)"
|
|
32
100
|
plan_status="$(fm_get "$project_dir/plan.md" status 2>/dev/null || true)"
|
|
33
101
|
|
|
34
|
-
echo ""
|
|
35
|
-
echo "Project: $slug"
|
|
36
|
-
echo " Spec status: ${spec_status:-unknown}"
|
|
37
|
-
echo " Plan status: ${plan_status:-unknown}"
|
|
38
|
-
|
|
39
102
|
total=0
|
|
103
|
+
open_tasks=0
|
|
104
|
+
backlog_count=0
|
|
105
|
+
ready_count=0
|
|
106
|
+
in_progress_count=0
|
|
107
|
+
review_count=0
|
|
108
|
+
done_count=0
|
|
109
|
+
blocked_count=0
|
|
110
|
+
deferred_count=0
|
|
111
|
+
canceled_count=0
|
|
112
|
+
unknown_count=0
|
|
40
113
|
for task in "$project_dir"/tasks/*.md; do
|
|
41
114
|
[[ -f "$task" ]] || continue
|
|
115
|
+
status="$(fm_get "$task" status 2>/dev/null || true)"
|
|
42
116
|
total=$((total + 1))
|
|
117
|
+
if ! is_closed_task_status "$status"; then
|
|
118
|
+
open_tasks=$((open_tasks + 1))
|
|
119
|
+
fi
|
|
120
|
+
case "$status" in
|
|
121
|
+
backlog) backlog_count=$((backlog_count + 1)) ;;
|
|
122
|
+
ready) ready_count=$((ready_count + 1)) ;;
|
|
123
|
+
in-progress) in_progress_count=$((in_progress_count + 1)) ;;
|
|
124
|
+
review) review_count=$((review_count + 1)) ;;
|
|
125
|
+
done) done_count=$((done_count + 1)) ;;
|
|
126
|
+
blocked) blocked_count=$((blocked_count + 1)) ;;
|
|
127
|
+
deferred) deferred_count=$((deferred_count + 1)) ;;
|
|
128
|
+
canceled) canceled_count=$((canceled_count + 1)) ;;
|
|
129
|
+
*) unknown_count=$((unknown_count + 1)) ;;
|
|
130
|
+
esac
|
|
43
131
|
done
|
|
44
132
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
133
|
+
project_open=false
|
|
134
|
+
if ! is_closed_spec_status "$spec_status" || ! is_closed_plan_status "$plan_status" || [[ $open_tasks -gt 0 ]]; then
|
|
135
|
+
project_open=true
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
if [[ "$open_only" == "true" && "$project_open" != "true" ]]; then
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
printed_count=$((printed_count + 1))
|
|
143
|
+
|
|
144
|
+
if [[ "$brief" == "true" ]]; then
|
|
145
|
+
echo "${slug} spec=${spec_status:-unknown} plan=${plan_status:-unknown} open_tasks=${open_tasks} total_tasks=${total}"
|
|
146
|
+
else
|
|
147
|
+
echo ""
|
|
148
|
+
echo "Project: $slug"
|
|
149
|
+
echo " Spec status: ${spec_status:-unknown}"
|
|
150
|
+
echo " Plan status: ${plan_status:-unknown}"
|
|
151
|
+
[[ $backlog_count -gt 0 ]] && echo " backlog: $backlog_count"
|
|
152
|
+
[[ $ready_count -gt 0 ]] && echo " ready: $ready_count"
|
|
153
|
+
[[ $in_progress_count -gt 0 ]] && echo " in-progress: $in_progress_count"
|
|
154
|
+
[[ $review_count -gt 0 ]] && echo " review: $review_count"
|
|
155
|
+
[[ $done_count -gt 0 ]] && echo " done: $done_count"
|
|
156
|
+
[[ $blocked_count -gt 0 ]] && echo " blocked: $blocked_count"
|
|
157
|
+
[[ $deferred_count -gt 0 ]] && echo " deferred: $deferred_count"
|
|
158
|
+
[[ $canceled_count -gt 0 ]] && echo " canceled: $canceled_count"
|
|
159
|
+
[[ $unknown_count -gt 0 ]] && echo " unknown: $unknown_count"
|
|
160
|
+
echo " total tasks: $total"
|
|
161
|
+
fi
|
|
57
162
|
done
|
|
58
163
|
|
|
59
164
|
if [[ $project_count -eq 0 ]]; then
|
|
60
165
|
echo "No projects found. Create one with: .agents/scripts/pm/init.sh <slug> <project-name>"
|
|
166
|
+
elif [[ "$open_only" == "true" && $printed_count -eq 0 ]]; then
|
|
167
|
+
echo "No open projects found."
|
|
61
168
|
fi
|
|
@@ -497,6 +497,8 @@ fi
|
|
|
497
497
|
|
|
498
498
|
if [[ -n "$status_transition_check" ]]; then
|
|
499
499
|
echo ""
|
|
500
|
+
echo "Project lifecycle and status transition check"
|
|
501
|
+
echo "---------------------------------------------"
|
|
500
502
|
if command -v node >/dev/null 2>&1; then
|
|
501
503
|
if node "$status_transition_check"; then
|
|
502
504
|
true
|