@bvdm/delano 0.1.5 → 0.1.8

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 (145) hide show
  1. package/.delano/README.md +7 -0
  2. package/.delano/viewer/README.md +19 -0
  3. package/.delano/viewer/public/app.js +818 -0
  4. package/.delano/viewer/public/explorer.svg +3 -0
  5. package/.delano/viewer/public/index.html +21 -0
  6. package/.delano/viewer/public/markdown.svg +6 -0
  7. package/.delano/viewer/public/styles.css +1042 -0
  8. package/.delano/viewer/public/vscode.svg +24 -0
  9. package/.delano/viewer/server.js +389 -0
  10. package/HANDBOOK.md +66 -45
  11. package/README.md +21 -2
  12. package/assets/install-manifest.json +112 -23
  13. package/assets/payload/.agents/README.md +31 -6
  14. package/assets/payload/.agents/adapters/claude/README.md +22 -3
  15. package/assets/payload/.agents/adapters/codex/README.md +22 -3
  16. package/assets/payload/.agents/adapters/opencode/README.md +22 -3
  17. package/assets/payload/.agents/adapters/pi/README.md +22 -3
  18. package/assets/payload/.agents/common/log-safety.js +55 -0
  19. package/assets/payload/.agents/eval-fixtures/skill-output/invalid/missing-evidence/output.json +6 -0
  20. package/assets/payload/.agents/eval-fixtures/skill-output/valid/summary/output.json +7 -0
  21. package/assets/payload/.agents/fixtures/github/status-snapshot.json +6 -0
  22. package/assets/payload/.agents/fixtures/linear/issue-snapshot.json +6 -0
  23. package/assets/payload/.agents/hooks/bash-worktree-fix.sh +2 -1
  24. package/assets/payload/.agents/hooks/post-tool-logger.js +2 -1
  25. package/assets/payload/.agents/hooks/session-tracker.js +0 -0
  26. package/assets/payload/.agents/hooks/user-prompt-logger.js +17 -1
  27. package/assets/payload/.agents/logs/delivery-metrics.md +22 -0
  28. package/assets/payload/.agents/logs/schema.md +20 -1
  29. package/assets/payload/.agents/rules/delivery-modes.md +17 -0
  30. package/assets/payload/.agents/schemas/README.md +22 -0
  31. package/assets/payload/.agents/schemas/artifact-scope.json +237 -0
  32. package/assets/payload/.agents/schemas/artifacts/context.schema.json +11 -0
  33. package/assets/payload/.agents/schemas/artifacts/decision_log.schema.json +12 -0
  34. package/assets/payload/.agents/schemas/artifacts/evidence.schema.json +17 -0
  35. package/assets/payload/.agents/schemas/artifacts/plan.schema.json +83 -0
  36. package/assets/payload/.agents/schemas/artifacts/spec.schema.json +101 -0
  37. package/assets/payload/.agents/schemas/artifacts/task.schema.json +121 -0
  38. package/assets/payload/.agents/schemas/artifacts/update.schema.json +12 -0
  39. package/assets/payload/.agents/schemas/artifacts/workstream.schema.json +66 -0
  40. package/assets/payload/.agents/schemas/evidence-map.json +53 -0
  41. package/assets/payload/.agents/schemas/learning/closeout-learning-proposal.schema.json +20 -0
  42. package/assets/payload/.agents/schemas/learning/delivery-metric-event.schema.json +21 -0
  43. package/assets/payload/.agents/schemas/leases/lease.schema.json +39 -0
  44. package/assets/payload/.agents/schemas/metrics/delivery-event.schema.json +29 -0
  45. package/assets/payload/.agents/schemas/metrics/delivery-events.schema.json +49 -0
  46. package/assets/payload/.agents/schemas/operating-modes.json +42 -0
  47. package/assets/payload/.agents/schemas/status-transitions.json +31 -0
  48. package/assets/payload/.agents/schemas/sync/drift-report.schema.json +25 -0
  49. package/assets/payload/.agents/schemas/sync/drift-taxonomy.json +38 -0
  50. package/assets/payload/.agents/schemas/sync/sync-map.schema.json +39 -0
  51. package/assets/payload/.agents/scripts/README.md +1 -0
  52. package/assets/payload/.agents/scripts/audit-context-files.mjs +54 -0
  53. package/assets/payload/.agents/scripts/audit-context-scoring.mjs +14 -0
  54. package/assets/payload/.agents/scripts/build-drift-report.mjs +133 -0
  55. package/assets/payload/.agents/scripts/check-artifact-schemas.mjs +116 -0
  56. package/assets/payload/.agents/scripts/check-closeout-learning-proposals.mjs +23 -0
  57. package/assets/payload/.agents/scripts/check-context-audit.mjs +61 -0
  58. package/assets/payload/.agents/scripts/check-delivery-metric-events.mjs +35 -0
  59. package/assets/payload/.agents/scripts/check-delivery-metrics.mjs +52 -0
  60. package/assets/payload/.agents/scripts/check-evidence-map.mjs +143 -0
  61. package/assets/payload/.agents/scripts/check-github-status-inspection.mjs +93 -0
  62. package/assets/payload/.agents/scripts/check-github-sync.mjs +159 -0
  63. package/assets/payload/.agents/scripts/check-handoff-summaries.mjs +57 -0
  64. package/assets/payload/.agents/scripts/check-lease-conflicts.mjs +24 -0
  65. package/assets/payload/.agents/scripts/check-lease-contracts.mjs +17 -0
  66. package/assets/payload/.agents/scripts/check-linear-issue-inspection.mjs +63 -0
  67. package/assets/payload/.agents/scripts/check-local-sync-map.mjs +151 -0
  68. package/assets/payload/.agents/scripts/check-log-safety.sh +62 -0
  69. package/assets/payload/.agents/scripts/check-operating-modes.mjs +99 -0
  70. package/assets/payload/.agents/scripts/check-path-standards.sh +1 -1
  71. package/assets/payload/.agents/scripts/check-skill-output-evals.mjs +13 -0
  72. package/assets/payload/.agents/scripts/check-status-transitions.mjs +169 -0
  73. package/assets/payload/.agents/scripts/check-strict-fixtures.mjs +140 -0
  74. package/assets/payload/.agents/scripts/check-sync-schemas.mjs +52 -0
  75. package/assets/payload/.agents/scripts/check-text-safety.mjs +158 -0
  76. package/assets/payload/.agents/scripts/check-worktree-health.mjs +100 -0
  77. package/assets/payload/.agents/scripts/fix-path-standards.sh +1 -1
  78. package/assets/payload/.agents/scripts/git-sparse-download.sh +0 -0
  79. package/assets/payload/.agents/scripts/inspect-github-sync.mjs +108 -0
  80. package/assets/payload/.agents/scripts/lease-manager.mjs +88 -0
  81. package/assets/payload/.agents/scripts/log-event.js +3 -0
  82. package/assets/payload/.agents/scripts/log-event.sh +0 -0
  83. package/assets/payload/.agents/scripts/plan-sync-repairs.mjs +66 -0
  84. package/assets/payload/.agents/scripts/pm/blocked.sh +0 -0
  85. package/assets/payload/.agents/scripts/pm/epic-list.sh +0 -0
  86. package/assets/payload/.agents/scripts/pm/in-progress.sh +0 -0
  87. package/assets/payload/.agents/scripts/pm/init.sh +0 -0
  88. package/assets/payload/.agents/scripts/pm/next.sh +0 -0
  89. package/assets/payload/.agents/scripts/pm/prd-list.sh +0 -0
  90. package/assets/payload/.agents/scripts/pm/search.sh +0 -0
  91. package/assets/payload/.agents/scripts/pm/standup.sh +0 -0
  92. package/assets/payload/.agents/scripts/pm/status.sh +0 -0
  93. package/assets/payload/.agents/scripts/pm/validate.sh +657 -2
  94. package/assets/payload/.agents/scripts/propose-closeout-learning.mjs +20 -0
  95. package/assets/payload/.agents/scripts/query-log.sh +0 -0
  96. package/assets/payload/.agents/scripts/read-local-sync-map.mjs +135 -0
  97. package/assets/payload/.agents/scripts/select-next-task.mjs +22 -0
  98. package/assets/payload/.agents/scripts/summarize-project-metrics.mjs +15 -0
  99. package/assets/payload/.agents/scripts/test-and-log.sh +0 -0
  100. package/assets/payload/.agents/skills/README.md +6 -0
  101. package/assets/payload/.agents/skills/closeout-skill/SKILL.md +3 -0
  102. package/assets/payload/.agents/skills/closeout-skill/references/runbook.md +5 -2
  103. package/assets/payload/.agents/skills/closeout-skill/templates/closure-checklist.md +2 -0
  104. package/assets/payload/.agents/skills/closeout-skill/templates/learning-proposal.md +21 -0
  105. package/assets/payload/.agents/skills/closeout-skill/templates/learning-proposals.md +25 -0
  106. package/assets/payload/.agents/skills/manage-context/SKILL.md +55 -0
  107. package/assets/payload/.agents/skills/manage-context/references/context-audit-checklist.md +26 -0
  108. package/assets/payload/.agents/skills/manage-context/references/runbook.md +26 -0
  109. package/assets/payload/.agents/skills/manage-context/templates/context-debt-report.md +22 -0
  110. package/assets/payload/.agents/skills/manage-context/templates/context-refresh-summary.md +13 -0
  111. package/assets/payload/.agents/skills/onboarding/SKILL.md +49 -0
  112. package/assets/payload/.agents/skills/onboarding/references/agents-md-best-practices.md +76 -0
  113. package/assets/payload/.agents/skills/prototype-skill/SKILL.md +51 -0
  114. package/assets/payload/.agents/skills/prototype-skill/references/probe-design-checklist.md +26 -0
  115. package/assets/payload/.agents/skills/prototype-skill/references/runbook.md +27 -0
  116. package/assets/payload/.agents/skills/prototype-skill/templates/probe-approval-recommendation.md +13 -0
  117. package/assets/payload/.agents/skills/prototype-skill/templates/probe-findings.md +16 -0
  118. package/assets/payload/.agents/validation-fixtures/strict/invalid/broken-dependencies/dependency.md +18 -0
  119. package/assets/payload/.agents/validation-fixtures/strict/invalid/broken-dependencies/task.md +24 -0
  120. package/assets/payload/.agents/validation-fixtures/strict/invalid/invalid-transition/task.md +20 -0
  121. package/assets/payload/.agents/validation-fixtures/strict/invalid/missing-evidence/task.md +27 -0
  122. package/assets/payload/.agents/validation-fixtures/strict/invalid/path-leak/task.md +27 -0
  123. package/assets/payload/.agents/validation-fixtures/strict/invalid/stale-context/context.md +9 -0
  124. package/assets/payload/.agents/validation-fixtures/strict/manifest.json +11 -0
  125. package/assets/payload/.agents/validation-fixtures/strict/valid/minimal-project/task.md +27 -0
  126. package/assets/payload/.delano/viewer/README.md +19 -0
  127. package/assets/payload/.delano/viewer/public/app.js +818 -0
  128. package/assets/payload/.delano/viewer/public/explorer.svg +3 -0
  129. package/assets/payload/.delano/viewer/public/index.html +21 -0
  130. package/assets/payload/.delano/viewer/public/markdown.svg +6 -0
  131. package/assets/payload/.delano/viewer/public/styles.css +1042 -0
  132. package/assets/payload/.delano/viewer/public/vscode.svg +24 -0
  133. package/assets/payload/.delano/viewer/server.js +389 -0
  134. package/assets/payload/.project/templates/plan.md +1 -1
  135. package/assets/payload/.project/templates/spec.md +1 -1
  136. package/assets/payload/.project/templates/task.md +1 -0
  137. package/assets/payload/HANDBOOK.md +66 -45
  138. package/assets/payload/install-delano.sh +0 -0
  139. package/install-delano.sh +0 -0
  140. package/package.json +31 -2
  141. package/src/cli/commands/onboarding.js +29 -0
  142. package/src/cli/commands/viewer.js +81 -0
  143. package/src/cli/index.js +20 -0
  144. package/src/cli/lib/install.js +1 -0
  145. package/src/cli/lib/onboarding.js +243 -0
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
+ <svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M21.0016 3.11679C21.0016 2.23783 20.0175 2.23782 19.5801 2.34769C20.1924 1.86426 20.9105 1.98147 21.1656 2.12796L27.079 5.02747C27.6424 5.30375 27.9998 5.8786 27.9998 6.50857V25.5831C27.9998 26.2215 27.6329 26.8025 27.058 27.0743L21.4937 29.7054C21.1109 29.8701 20.2799 30.2767 19.5801 29.7053C20.4549 29.8702 20.9287 29.2476 21.0016 28.8264V3.11679Z" fill="url(#paint0_linear_87_8101)"/>
4
+ <path d="M19.6512 2.3319C20.1154 2.24017 21.0018 2.28271 21.0018 3.11685V9.68254L3.07359 23.2453C2.76022 23.4824 2.3192 23.443 2.05229 23.1542L0.204532 21.1548C-0.0849358 20.8416 -0.0646824 20.3513 0.249624 20.0633L19.5802 2.34775L19.6512 2.3319Z" fill="url(#paint1_linear_87_8101)"/>
5
+ <path d="M21.0018 22.3708L3.07359 8.80801C2.76022 8.57094 2.3192 8.61028 2.05229 8.8991L0.204532 10.8985C-0.0849358 11.2117 -0.0646824 11.702 0.249624 11.9901L19.5802 29.7056C20.455 29.8704 20.9289 29.2478 21.0018 28.8266V22.3708Z" fill="url(#paint2_linear_87_8101)"/>
6
+ <defs>
7
+ <linearGradient id="paint0_linear_87_8101" x1="23.79" y1="2" x2="23.79" y2="30" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#32B5F1"/>
9
+ <stop offset="1" stop-color="#2B9FED"/>
10
+ </linearGradient>
11
+ <linearGradient id="paint1_linear_87_8101" x1="21.0018" y1="5.53398" x2="1.0217" y2="22.3051" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#0F6FB3"/>
13
+ <stop offset="0.270551" stop-color="#1279B7"/>
14
+ <stop offset="0.421376" stop-color="#1176B5"/>
15
+ <stop offset="0.618197" stop-color="#0E69AC"/>
16
+ <stop offset="0.855344" stop-color="#0F70AF"/>
17
+ <stop offset="1" stop-color="#0F6DAD"/>
18
+ </linearGradient>
19
+ <linearGradient id="paint2_linear_87_8101" x1="1.15522" y1="9.98389" x2="21.0791" y2="26.4808" gradientUnits="userSpaceOnUse">
20
+ <stop stop-color="#1791D2"/>
21
+ <stop offset="1" stop-color="#1173C5"/>
22
+ </linearGradient>
23
+ </defs>
24
+ </svg>
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Delano read-only markdown viewer.
4
+ * Serves .project markdown contracts without writing repo state.
5
+ */
6
+ const http = require('node:http');
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const url = require('node:url');
10
+ const { spawn, spawnSync } = require('node:child_process');
11
+
12
+ const repoRoot = path.resolve(process.env.DELANO_VIEWER_ROOT || path.resolve(__dirname, '..', '..'));
13
+ const projectRoot = path.join(repoRoot, '.project');
14
+ const publicRoot = path.join(__dirname, 'public');
15
+ const port = Number(process.env.DELANO_VIEWER_PORT || process.env.PORT || 3977);
16
+
17
+ function isInside(parent, child) {
18
+ const rel = path.relative(parent, child);
19
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
20
+ }
21
+
22
+ function readText(file) {
23
+ return fs.readFileSync(file, 'utf8');
24
+ }
25
+
26
+ function walkMarkdown(dir) {
27
+ if (!fs.existsSync(dir)) return [];
28
+ const out = [];
29
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
30
+ if (entry.name.startsWith('.')) continue;
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) out.push(...walkMarkdown(full));
33
+ if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
34
+ }
35
+ return out.sort((a, b) => a.localeCompare(b));
36
+ }
37
+
38
+ function splitFrontmatter(markdown) {
39
+ if (!markdown.startsWith('---\n') && !markdown.startsWith('---\r\n')) return { frontmatter: {}, body: markdown };
40
+ const normalized = markdown.replace(/^---\r?\n/, '');
41
+ const close = normalized.search(/\r?\n---\r?\n/);
42
+ if (close < 0) return { frontmatter: {}, body: markdown };
43
+ const yaml = normalized.slice(0, close);
44
+ const body = normalized.slice(close).replace(/^\r?\n---\r?\n/, '');
45
+ return { frontmatter: parseSimpleYaml(yaml), body };
46
+ }
47
+
48
+ function parseScalar(raw) {
49
+ const value = raw.trim().replace(/^['"]|['"]$/g, '');
50
+ if (value === '') return '';
51
+ if (/^(true|false)$/i.test(value)) return value.toLowerCase() === 'true';
52
+ if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
53
+ if (value.startsWith('[') && value.endsWith(']')) {
54
+ return value.slice(1, -1).split(',').map((item) => parseScalar(item)).filter((item) => item !== '');
55
+ }
56
+ return value;
57
+ }
58
+
59
+ function parseSimpleYaml(yaml) {
60
+ const data = {};
61
+ let currentKey = null;
62
+ for (const line of yaml.split(/\r?\n/)) {
63
+ const list = line.match(/^\s*-\s+(.*)$/);
64
+ if (list && currentKey) {
65
+ if (!Array.isArray(data[currentKey])) data[currentKey] = data[currentKey] ? [data[currentKey]] : [];
66
+ data[currentKey].push(parseScalar(list[1]));
67
+ continue;
68
+ }
69
+ const pair = line.match(/^([^:#][^:]*):\s*(.*)$/);
70
+ if (!pair) continue;
71
+ currentKey = pair[1].trim();
72
+ data[currentKey] = pair[2] ? parseScalar(pair[2]) : [];
73
+ }
74
+ return data;
75
+ }
76
+
77
+ function firstHeading(body) {
78
+ const heading = body.match(/^#\s+(.+)$/m);
79
+ return heading ? heading[1].trim() : null;
80
+ }
81
+
82
+ function snippet(body) {
83
+ return body
84
+ .replace(/```[\s\S]*?```/g, ' ')
85
+ .replace(/^#+\s+/gm, '')
86
+ .replace(/[*_`>#\-[\]]/g, ' ')
87
+ .replace(/\s+/g, ' ')
88
+ .trim()
89
+ .slice(0, 180);
90
+ }
91
+
92
+ function relationshipFields(frontmatter) {
93
+ const result = {};
94
+ for (const [key, value] of Object.entries(frontmatter)) {
95
+ const text = Array.isArray(value) ? value.join(' ') : String(value ?? '');
96
+ const links = [...text.matchAll(/\[\[([^\]]+)\]\]/g)].map((m) => m[1]);
97
+ if (links.length) result[key] = links;
98
+ }
99
+ return result;
100
+ }
101
+
102
+ function projectSlugFor(rel) {
103
+ const match = rel.match(/^projects\/([^/]+)\//);
104
+ return match ? match[1] : null;
105
+ }
106
+
107
+ function artifactRoleFor(rel) {
108
+ if (rel.startsWith('context/')) return rel.endsWith('/progress.md') || rel.endsWith('progress.md') ? 'progress' : 'context';
109
+ if (rel.startsWith('templates/')) return 'template';
110
+ if (/\/spec\.md$/.test(rel)) return 'spec';
111
+ if (/\/plan\.md$/.test(rel)) return 'plan';
112
+ if (/\/decisions\.md$/.test(rel)) return 'decision';
113
+ if (/\/progress\.md$/.test(rel) || /\/updates\//.test(rel) || /\/completion-summary\.md$/.test(rel)) return 'progress';
114
+ if (/\/workstreams\/[^/]+\.md$/.test(rel)) return 'workstream';
115
+ if (/\/tasks\/[^/]+\.md$/.test(rel)) return 'task';
116
+ return 'context';
117
+ }
118
+
119
+ function codeFromFilename(rel, prefix) {
120
+ const base = path.basename(rel, '.md');
121
+ const match = base.match(new RegExp(`^(${prefix}-[A-Za-z0-9]+)`));
122
+ return match ? match[1] : null;
123
+ }
124
+
125
+ function normalizeWorkstreamId(value) {
126
+ if (!value) return null;
127
+ const normalized = String(value).trim().toUpperCase();
128
+ return /^WS-[A-Z0-9]+$/.test(normalized) ? normalized : null;
129
+ }
130
+
131
+ function csvArray(value) {
132
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
133
+ if (!value) return [];
134
+ return [String(value)];
135
+ }
136
+
137
+ function docMeta(file) {
138
+ const markdown = readText(file);
139
+ const { frontmatter, body } = splitFrontmatter(markdown);
140
+ const rel = path.relative(projectRoot, file).replace(/\\/g, '/');
141
+ const stat = fs.statSync(file);
142
+ const role = artifactRoleFor(rel);
143
+ return {
144
+ path: rel,
145
+ title: frontmatter.name || firstHeading(body) || path.basename(file, '.md'),
146
+ status: frontmatter.status || null,
147
+ type: rel.startsWith('context/') ? 'context' : rel.split('/')[0],
148
+ project: projectSlugFor(rel),
149
+ role,
150
+ artifactRole: role,
151
+ workstreamId: role === 'workstream' ? codeFromFilename(rel, 'WS') : (role === 'task' ? normalizeWorkstreamId(frontmatter.workstream) : null),
152
+ taskId: role === 'task' ? frontmatter.id || codeFromFilename(rel, 'T') : null,
153
+ dependsOn: role === 'task' ? csvArray(frontmatter.depends_on) : [],
154
+ updated: frontmatter.updated || frontmatter.timestamp || stat.mtime.toISOString(),
155
+ frontmatter,
156
+ relationships: relationshipFields(frontmatter),
157
+ snippet: snippet(body),
158
+ size: stat.size,
159
+ };
160
+ }
161
+
162
+ function words(text) {
163
+ return new Set(String(text || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').split(/\s+/).filter((w) => w.length > 2 && !['and', 'the', 'for', 'with', 'docs'].includes(w)));
164
+ }
165
+
166
+ function overlapScore(a, b) {
167
+ let score = 0;
168
+ for (const word of a) if (b.has(word)) score += 1;
169
+ return score;
170
+ }
171
+
172
+ function relateTasksToWorkstreams(projectDocs) {
173
+ const workstreams = projectDocs.filter((doc) => doc.role === 'workstream');
174
+ const tasks = projectDocs.filter((doc) => doc.role === 'task');
175
+ const wsById = new Map(workstreams.map((ws) => [ws.workstreamId, ws]));
176
+ const wsWords = new Map(workstreams.map((ws) => [ws.path, words(`${ws.workstreamId} ${ws.title} ${ws.snippet}`)]));
177
+ for (const task of tasks) {
178
+ if (task.workstreamId && wsById.has(task.workstreamId)) {
179
+ task.workstreamPath = wsById.get(task.workstreamId).path;
180
+ continue;
181
+ }
182
+ const taskWords = words(`${task.taskId} ${task.title} ${task.snippet}`);
183
+ let best = null;
184
+ for (const ws of workstreams) {
185
+ const score = overlapScore(taskWords, wsWords.get(ws.path));
186
+ if (!best || score > best.score) best = { ws, score };
187
+ }
188
+ task.workstreamId = best && best.score > 0 ? best.ws.workstreamId : null;
189
+ task.workstreamPath = best && best.score > 0 ? best.ws.path : null;
190
+ }
191
+ }
192
+
193
+ function projectOutline(projectDocs) {
194
+ relateTasksToWorkstreams(projectDocs);
195
+ const byRole = (role) => projectDocs.filter((doc) => doc.role === role);
196
+ const byName = (docs) => docs.slice().sort((a, b) => a.path.localeCompare(b.path));
197
+ const tasks = byName(byRole('task'));
198
+ return {
199
+ spec: byRole('spec')[0]?.path || null,
200
+ plan: byRole('plan')[0]?.path || null,
201
+ progress: byRole('progress').map((doc) => doc.path),
202
+ decisions: byRole('decision').map((doc) => doc.path),
203
+ workstreams: byName(byRole('workstream')).map((ws) => ({
204
+ path: ws.path,
205
+ id: ws.workstreamId,
206
+ title: ws.title,
207
+ status: ws.status,
208
+ tasks: tasks.filter((task) => task.workstreamPath === ws.path).map((task) => task.path),
209
+ })),
210
+ unassignedTasks: tasks.filter((task) => !task.workstreamPath).map((task) => task.path),
211
+ };
212
+ }
213
+
214
+ function loadIndex() {
215
+ const docs = walkMarkdown(projectRoot).map(docMeta);
216
+ const projectSlugs = fs.existsSync(path.join(projectRoot, 'projects'))
217
+ ? fs.readdirSync(path.join(projectRoot, 'projects'), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort()
218
+ : [];
219
+ const fixed = [
220
+ {
221
+ slug: 'context',
222
+ title: 'Project',
223
+ status: null,
224
+ created: null,
225
+ pinned: true,
226
+ docs: docs.filter((doc) => doc.path.startsWith('context/')).map((doc) => doc.path),
227
+ },
228
+ {
229
+ slug: 'templates',
230
+ title: 'Templates',
231
+ status: null,
232
+ created: null,
233
+ pinned: true,
234
+ docs: docs.filter((doc) => doc.path.startsWith('templates/')).map((doc) => doc.path),
235
+ },
236
+ ];
237
+ const projectEntries = projectSlugs.map((slug) => {
238
+ const projectDocs = docs.filter((doc) => doc.path.startsWith(`projects/${slug}/`));
239
+ const spec = projectDocs.find((doc) => doc.path.endsWith('/spec.md'));
240
+ const plan = projectDocs.find((doc) => doc.path.endsWith('/plan.md'));
241
+ const outline = projectOutline(projectDocs);
242
+ return {
243
+ slug,
244
+ title: spec?.frontmatter.name || plan?.frontmatter.name || slug.replace(/-/g, ' '),
245
+ status: spec?.frontmatter.status || plan?.frontmatter.status || null,
246
+ created: spec?.frontmatter.created || plan?.frontmatter.created || null,
247
+ pinned: false,
248
+ docs: projectDocs.map((doc) => doc.path),
249
+ outline,
250
+ };
251
+ });
252
+ // Sort non-pinned project entries by `created` desc; entries without `created` keep their relative order at the end.
253
+ projectEntries.sort((a, b) => {
254
+ if (!a.created && !b.created) return 0;
255
+ if (!a.created) return 1;
256
+ if (!b.created) return -1;
257
+ return String(b.created).localeCompare(String(a.created));
258
+ });
259
+ const projects = [...fixed, ...projectEntries];
260
+ return { repo: path.basename(repoRoot), generatedAt: new Date().toISOString(), projects, docs };
261
+ }
262
+
263
+ function sendJson(res, data) {
264
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });
265
+ res.end(JSON.stringify(data, null, 2));
266
+ }
267
+
268
+ function projectFileFromRequest(rel) {
269
+ const file = path.resolve(projectRoot, String(rel || ''));
270
+ if (!String(rel || '').endsWith('.md') || !isInside(projectRoot, file) || !fs.existsSync(file)) return null;
271
+ return file;
272
+ }
273
+
274
+ function windowsPath(file) {
275
+ const converted = spawnSync('wslpath', ['-w', file], { encoding: 'utf8' });
276
+ return converted.status === 0 ? converted.stdout.trim() : file;
277
+ }
278
+
279
+ function commandExists(command) {
280
+ const check = spawnSync(process.platform === 'win32' ? 'where' : 'which', [command], { stdio: 'ignore' });
281
+ return check.status === 0;
282
+ }
283
+
284
+ function openTarget(target, file) {
285
+ const isWin = process.platform === 'win32';
286
+ const isMac = process.platform === 'darwin';
287
+
288
+ if (target === 'code') {
289
+ if (!commandExists('code')) return { ok: false, error: 'VS Code CLI `code` was not found on PATH.' };
290
+ // On Windows the CLI is `code.cmd`; spawn requires shell:true to resolve PATHEXT.
291
+ spawn('code', ['-g', file], { detached: true, stdio: 'ignore', shell: isWin }).unref();
292
+ return { ok: true, target, opened: file };
293
+ }
294
+
295
+ if (target === 'explorer') {
296
+ const dir = path.dirname(file);
297
+
298
+ // Native Windows: launch explorer.exe directly with the directory.
299
+ if (isWin) {
300
+ spawn('explorer.exe', [dir], { detached: true, stdio: 'ignore' }).unref();
301
+ return { ok: true, target, opened: dir };
302
+ }
303
+
304
+ // WSL: explorer.exe is reachable through the mounted Windows path.
305
+ const wslExplorer = '/mnt/c/Windows/explorer.exe';
306
+ if (fs.existsSync(wslExplorer)) {
307
+ spawn(wslExplorer, [windowsPath(dir)], { detached: true, stdio: 'ignore' }).unref();
308
+ return { ok: true, target, opened: dir };
309
+ }
310
+
311
+ // macOS / Linux fall back to `open` / `xdg-open`.
312
+ const opener = isMac ? 'open' : 'xdg-open';
313
+ if (!commandExists(opener)) return { ok: false, error: `System opener \`${opener}\` was not found.` };
314
+ spawn(opener, [dir], { detached: true, stdio: 'ignore' }).unref();
315
+ return { ok: true, target, opened: dir };
316
+ }
317
+
318
+ return { ok: false, error: 'Unknown open target.' };
319
+ }
320
+
321
+ function sendStatic(res, pathname) {
322
+ if (pathname === '/favicon.ico') {
323
+ res.writeHead(204, { 'cache-control': 'max-age=86400' });
324
+ res.end();
325
+ return;
326
+ }
327
+ const file = pathname === '/' ? path.join(publicRoot, 'index.html') : path.join(publicRoot, pathname);
328
+ const resolved = path.resolve(file);
329
+ if (!isInside(publicRoot, resolved) || !fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
330
+ res.writeHead(404); res.end('Not found'); return;
331
+ }
332
+ const ext = path.extname(resolved).toLowerCase();
333
+ const mimeMap = {
334
+ '.js': 'text/javascript',
335
+ '.css': 'text/css',
336
+ '.svg': 'image/svg+xml',
337
+ '.png': 'image/png',
338
+ '.jpg': 'image/jpeg',
339
+ '.jpeg': 'image/jpeg',
340
+ '.webp': 'image/webp',
341
+ '.ico': 'image/x-icon',
342
+ };
343
+ const isText = ext === '.js' || ext === '.css' || ext === '.svg' || ext === '' || ext === '.html';
344
+ const type = mimeMap[ext] || 'text/html';
345
+ const headers = isText ? { 'content-type': `${type}; charset=utf-8` } : { 'content-type': type };
346
+ res.writeHead(200, headers);
347
+ res.end(fs.readFileSync(resolved));
348
+ }
349
+
350
+ const server = http.createServer((req, res) => {
351
+ try {
352
+ const parsed = url.parse(req.url, true);
353
+ if (parsed.pathname === '/api/index') return sendJson(res, loadIndex());
354
+ if (parsed.pathname === '/api/doc') {
355
+ const rel = String(parsed.query.path || '');
356
+ const file = projectFileFromRequest(rel);
357
+ if (!file) {
358
+ res.writeHead(404); res.end('Document not found'); return;
359
+ }
360
+ const markdown = readText(file);
361
+ const meta = docMeta(file);
362
+ return sendJson(res, { ...meta, markdown, body: splitFrontmatter(markdown).body });
363
+ }
364
+ if (parsed.pathname === '/api/open') {
365
+ if (req.method !== 'POST') {
366
+ res.writeHead(405); res.end('Use POST'); return;
367
+ }
368
+ const rel = String(parsed.query.path || '');
369
+ const file = projectFileFromRequest(rel);
370
+ if (!file) {
371
+ res.writeHead(404); res.end('Document not found'); return;
372
+ }
373
+ const result = openTarget(String(parsed.query.target || ''), file);
374
+ if (!result.ok) {
375
+ res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
376
+ res.end(JSON.stringify(result, null, 2)); return;
377
+ }
378
+ return sendJson(res, result);
379
+ }
380
+ return sendStatic(res, decodeURIComponent(parsed.pathname));
381
+ } catch (error) {
382
+ res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
383
+ res.end(error.stack || String(error));
384
+ }
385
+ });
386
+
387
+ server.listen(port, '127.0.0.1', () => {
388
+ console.log(`Delano read-only viewer: http://127.0.0.1:${port}`);
389
+ });