@delete101020/aidlc 4.0.0

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 (93) hide show
  1. package/AUTOMATION.md +148 -0
  2. package/README.md +684 -0
  3. package/dist/assets/annotate-artifact.skill.md +121 -0
  4. package/dist/assets/epic-context.skill.md +55 -0
  5. package/dist/bundle.js +49470 -0
  6. package/dist/templates/ainative/agents/native-engineer.md +66 -0
  7. package/dist/templates/ainative/agents/native-operator.md +71 -0
  8. package/dist/templates/ainative/agents/native-originator.md +56 -0
  9. package/dist/templates/ainative/agents/native-product-owner.md +68 -0
  10. package/dist/templates/ainative/agents/native-reviewer.md +65 -0
  11. package/dist/templates/ainative/agents/native-verifier.md +58 -0
  12. package/dist/templates/ainative/artifacts/build-plan.md +56 -0
  13. package/dist/templates/ainative/artifacts/implement.md +55 -0
  14. package/dist/templates/ainative/artifacts/intent.md +50 -0
  15. package/dist/templates/ainative/artifacts/maintain.md +85 -0
  16. package/dist/templates/ainative/artifacts/review.md +68 -0
  17. package/dist/templates/ainative/artifacts/spec.md +69 -0
  18. package/dist/templates/ainative/artifacts/verify.md +55 -0
  19. package/dist/templates/ainative/skills/native-build-plan.md +74 -0
  20. package/dist/templates/ainative/skills/native-implement.md +62 -0
  21. package/dist/templates/ainative/skills/native-intent.md +50 -0
  22. package/dist/templates/ainative/skills/native-maintain.md +94 -0
  23. package/dist/templates/ainative/skills/native-review.md +82 -0
  24. package/dist/templates/ainative/skills/native-spec.md +83 -0
  25. package/dist/templates/ainative/skills/native-verify.md +60 -0
  26. package/dist/templates/sdlc/agents/designer.md +87 -0
  27. package/dist/templates/sdlc/agents/developer.md +178 -0
  28. package/dist/templates/sdlc/agents/example-with-skills.md +59 -0
  29. package/dist/templates/sdlc/agents/orchestrator.md +186 -0
  30. package/dist/templates/sdlc/agents/po.md +115 -0
  31. package/dist/templates/sdlc/agents/qa.md +141 -0
  32. package/dist/templates/sdlc/agents/tech-lead.md +124 -0
  33. package/dist/templates/sdlc/artifacts/design.md +130 -0
  34. package/dist/templates/sdlc/artifacts/execute-test.md +64 -0
  35. package/dist/templates/sdlc/artifacts/implement.backend.md +96 -0
  36. package/dist/templates/sdlc/artifacts/implement.md +70 -0
  37. package/dist/templates/sdlc/artifacts/implement.web-react.md +109 -0
  38. package/dist/templates/sdlc/artifacts/implement.web.md +99 -0
  39. package/dist/templates/sdlc/artifacts/plan.md +97 -0
  40. package/dist/templates/sdlc/artifacts/prototype.md +141 -0
  41. package/dist/templates/sdlc/artifacts/test-cases.md +47 -0
  42. package/dist/templates/sdlc/artifacts/test-plan.md +100 -0
  43. package/dist/templates/sdlc/artifacts/unit-test.md +32 -0
  44. package/dist/templates/sdlc/profiles/agile-lite.yaml +29 -0
  45. package/dist/templates/sdlc/profiles/hybrid.yaml +43 -0
  46. package/dist/templates/sdlc/profiles/iso-ieee.yaml +67 -0
  47. package/dist/templates/sdlc/profiles/none.yaml +21 -0
  48. package/dist/templates/sdlc/skills/advance-epic.md +77 -0
  49. package/dist/templates/sdlc/skills/analyze-requirements.md +279 -0
  50. package/dist/templates/sdlc/skills/discovery-gate.md +118 -0
  51. package/dist/templates/sdlc/skills/execute-test.md +74 -0
  52. package/dist/templates/sdlc/skills/generate-test-cases.md +77 -0
  53. package/dist/templates/sdlc/skills/implement.md +74 -0
  54. package/dist/templates/sdlc/skills/prd.md +114 -0
  55. package/dist/templates/sdlc/skills/prototype.md +128 -0
  56. package/dist/templates/sdlc/skills/tech-design.md +134 -0
  57. package/dist/templates/sdlc/skills/test-plan.md +115 -0
  58. package/dist/templates/sdlc/skills/test-report.md +38 -0
  59. package/dist/templates/sdlc/skills/unit-test.md +43 -0
  60. package/dist/templates/sdlc/validators/ci.mjs +49 -0
  61. package/dist/templates/sdlc/validators/traceability.mjs +244 -0
  62. package/dist/templates/speckit/agents/analyst.md +42 -0
  63. package/dist/templates/speckit/agents/speckit-developer.md +36 -0
  64. package/dist/templates/speckit/agents/speckit-qa.md +38 -0
  65. package/dist/templates/speckit/agents/speckit-tech-lead.md +40 -0
  66. package/dist/templates/speckit/artifacts/analyze.md +48 -0
  67. package/dist/templates/speckit/artifacts/clarify.md +64 -0
  68. package/dist/templates/speckit/artifacts/plan.md +57 -0
  69. package/dist/templates/speckit/artifacts/specify.md +64 -0
  70. package/dist/templates/speckit/artifacts/tasks.md +33 -0
  71. package/dist/templates/speckit/skills/analyze.md +50 -0
  72. package/dist/templates/speckit/skills/clarify.md +31 -0
  73. package/dist/templates/speckit/skills/plan.md +56 -0
  74. package/dist/templates/speckit/skills/specify.md +52 -0
  75. package/dist/templates/speckit/skills/speckit-implement.md +33 -0
  76. package/dist/templates/speckit/skills/tasks.md +38 -0
  77. package/dist/tools/epic-memory-hook.mjs +78 -0
  78. package/dist/tools/epic-memory.mjs +130 -0
  79. package/dist/tools/md-to-html.mjs +478 -0
  80. package/dist/tools/vendor/marked.esm.mjs +2432 -0
  81. package/dist/vendor/annotron/LICENSE +21 -0
  82. package/dist/vendor/annotron/README.md +213 -0
  83. package/dist/vendor/annotron/bin/annotron +467 -0
  84. package/dist/vendor/annotron/commands/annotron.md +11 -0
  85. package/dist/vendor/annotron/hooks/annotron-hook.sh +38 -0
  86. package/dist/vendor/annotron/hooks/hooks.json +50 -0
  87. package/dist/vendor/annotron/package.json +72 -0
  88. package/dist/vendor/annotron/skills/annotron-review/SKILL.md +86 -0
  89. package/dist/vendor/annotron/src/chrome.html +2029 -0
  90. package/dist/vendor/annotron/src/mdRender.js +208 -0
  91. package/dist/vendor/annotron/src/sdk.js +1042 -0
  92. package/dist/vendor/annotron/src/server.js +780 -0
  93. package/package.json +60 -0
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Per-epic memory: a compact, durable digest of an epic kept in
4
+ * `<epicDir>/epic-memory.json`, so continuing the epic later — with any
5
+ * agent — is cheap on tokens. Instead of re-reading every artifact and the
6
+ * git history, an agent reads this digest first.
7
+ *
8
+ * It holds three things:
9
+ * - summary : one-paragraph "what this epic is + where it stands"
10
+ * - entries[] : distilled decisions / constraints / context / notes
11
+ * - reflections[]: lessons on how to prompt / work more effectively next time
12
+ *
13
+ * Every write records who made it (git identity, hostname fallback) + a timestamp.
14
+ * Zero external deps — runs with just `node`.
15
+ *
16
+ * Usage:
17
+ * node epic-memory.mjs show <epicDir>
18
+ * node epic-memory.mjs add <epicDir> --kind decision|constraint|context|note --text "..."
19
+ * node epic-memory.mjs reflect <epicDir> --text "..."
20
+ * node epic-memory.mjs summary <epicDir> --text "..."
21
+ */
22
+ import fs from 'fs';
23
+ import os from 'os';
24
+ import path from 'path';
25
+ import { execFileSync } from 'child_process';
26
+
27
+ const FILE = 'epic-memory.json';
28
+ const KINDS = ['decision', 'constraint', 'context', 'note'];
29
+
30
+ function die(msg) { console.error(msg); process.exit(1); }
31
+
32
+ function detectAuthor(cwd) {
33
+ const git = (args) => {
34
+ try {
35
+ return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 })
36
+ .toString().trim();
37
+ } catch { return ''; }
38
+ };
39
+ const name = git(['config', 'user.name']);
40
+ if (name) {
41
+ const email = git(['config', 'user.email']);
42
+ return email ? `${name} <${email}>` : name;
43
+ }
44
+ return os.hostname();
45
+ }
46
+
47
+ function load(epicDir) {
48
+ const p = path.join(epicDir, FILE);
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(p, 'utf8'));
51
+ if (data && typeof data === 'object') { return data; }
52
+ } catch { /* missing or invalid → fresh */ }
53
+ return { epic: path.basename(epicDir), summary: '', entries: [], reflections: [] };
54
+ }
55
+
56
+ function save(epicDir, mem) {
57
+ mem.epic = mem.epic || path.basename(epicDir);
58
+ mem.updatedAt = new Date().toISOString();
59
+ mem.entries = mem.entries || [];
60
+ mem.reflections = mem.reflections || [];
61
+ fs.writeFileSync(path.join(epicDir, FILE), JSON.stringify(mem, null, 2) + '\n', 'utf8');
62
+ }
63
+
64
+ function parseFlags(args) {
65
+ const out = {};
66
+ for (let i = 0; i < args.length; i++) {
67
+ if (args[i] === '--kind') { out.kind = args[++i]; }
68
+ else if (args[i] === '--text') { out.text = args[++i]; }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ function show(epicDir) {
74
+ const p = path.join(epicDir, FILE);
75
+ if (!fs.existsSync(p)) {
76
+ console.log(`(no epic memory yet at ${p} — start fresh; add decisions with \`epic-memory.mjs add\`)`);
77
+ return;
78
+ }
79
+ const m = load(epicDir);
80
+ const lines = [`# Epic memory — ${m.epic}${m.updatedAt ? ` (updated ${m.updatedAt})` : ''}`];
81
+ if (m.summary) { lines.push('', '## Summary', m.summary); }
82
+ if (m.entries?.length) {
83
+ lines.push('', '## Context & decisions');
84
+ for (const e of m.entries) {
85
+ lines.push(`- [${e.kind}] ${e.text} — ${e.author || '?'}, ${e.at || ''}`);
86
+ }
87
+ }
88
+ if (m.reflections?.length) {
89
+ lines.push('', '## Reflections (prompt/work better next time)');
90
+ for (const r of m.reflections) {
91
+ lines.push(`- ${r.text} — ${r.author || '?'}, ${r.at || ''}`);
92
+ }
93
+ }
94
+ console.log(lines.join('\n'));
95
+ }
96
+
97
+ function main() {
98
+ const [cmd, epicDir, ...rest] = process.argv.slice(2);
99
+ if (!cmd || !epicDir) {
100
+ die('usage: node epic-memory.mjs show|add|reflect|summary <epicDir> [--kind K] [--text "..."]');
101
+ }
102
+ if (!fs.existsSync(epicDir)) { die(`epic dir not found: ${epicDir}`); }
103
+
104
+ if (cmd === 'show') { show(epicDir); return; }
105
+
106
+ const { kind, text } = parseFlags(rest);
107
+ const mem = load(epicDir);
108
+
109
+ if (cmd === 'add') {
110
+ if (!text) { die('add requires --text "..."'); }
111
+ const k = KINDS.includes(kind) ? kind : 'note';
112
+ mem.entries.push({ at: new Date().toISOString(), author: detectAuthor(epicDir), kind: k, text });
113
+ save(epicDir, mem);
114
+ console.log(`added ${k} to ${mem.epic} memory`);
115
+ } else if (cmd === 'reflect') {
116
+ if (!text) { die('reflect requires --text "..."'); }
117
+ mem.reflections.push({ at: new Date().toISOString(), author: detectAuthor(epicDir), text });
118
+ save(epicDir, mem);
119
+ console.log(`added reflection to ${mem.epic} memory`);
120
+ } else if (cmd === 'summary') {
121
+ if (!text) { die('summary requires --text "..."'); }
122
+ mem.summary = text;
123
+ save(epicDir, mem);
124
+ console.log(`set summary for ${mem.epic} memory`);
125
+ } else {
126
+ die(`unknown command: ${cmd}`);
127
+ }
128
+ }
129
+
130
+ main();
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Render Markdown artifact(s) to standalone, self-contained HTML for annotation.
4
+ *
5
+ * Markdown stays the canonical source (cheaper for agents/pipeline to read, and the
6
+ * `produces:` paths in workspace.yaml all point at .md). This script produces a
7
+ * throwaway .html render next to the .md whenever you want to open an artifact in an
8
+ * annotation tool (e.g. annotron) and comment on it. The HTML is a *render*, not a
9
+ * source: to change it, edit the .md and re-run.
10
+ *
11
+ * Zero external/runtime dependencies: `marked` is vendored next to this file, so the
12
+ * script runs with just `node` — no `npm install`, no Python, no `pip`. That makes it
13
+ * safe to bundle in the VS Code extension and to copy into ~/.claude/tools for the
14
+ * /annotate-artifact skill.
15
+ *
16
+ * Usage:
17
+ * node md-to-html.mjs path/to/PRD.md # -> PRD.html next to it
18
+ * node md-to-html.mjs PRD.md /tmp/PRD.html # explicit output
19
+ * node md-to-html.mjs --all path/to/artifacts # whole folder, cross-links rewritten
20
+ *
21
+ * --all mode rewrites cross-links between sibling artifacts (PRD.md -> PRD.html) so the
22
+ * rendered set stays navigable, while leaving links that point outside the folder
23
+ * (../foo.md, sub/bar.md) or to external URLs untouched — those still point at .md.
24
+ */
25
+ import fs from 'fs';
26
+ import os from 'os';
27
+ import path from 'path';
28
+ import { execFileSync } from 'child_process';
29
+ import { fileURLToPath } from 'url';
30
+ import { marked, Renderer } from './vendor/marked.esm.mjs';
31
+
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+
34
+ const CSS = `
35
+ :root {
36
+ color-scheme: light dark;
37
+ --bg: #faf9f6;
38
+ --surface: #f2f0e9;
39
+ --surface-2: #f6f4ee;
40
+ --text: #1f1d1a;
41
+ --muted: #6f6b62;
42
+ --border: #e6e2d6;
43
+ --border-strong: #d9d4c6;
44
+ --accent: #c15f3c;
45
+ --accent-soft: rgba(193, 95, 60, .12);
46
+ --code-text: #1f1d1a;
47
+ }
48
+ @media (prefers-color-scheme: dark) {
49
+ :root {
50
+ --bg: #1e1c19;
51
+ --surface: #29261f;
52
+ --surface-2: #26231d;
53
+ --text: #ecebe3;
54
+ --muted: #a6a196;
55
+ --border: #3a362d;
56
+ --border-strong: #474237;
57
+ --accent: #e0996f;
58
+ --accent-soft: rgba(224, 153, 111, .16);
59
+ --code-text: #ecebe3;
60
+ }
61
+ }
62
+ * { box-sizing: border-box; }
63
+ html { -webkit-text-size-adjust: 100%; }
64
+ body {
65
+ font-family: ui-serif, "Charter", "Iowan Old Style", "Palatino", Georgia, Cambria, "Times New Roman", serif;
66
+ font-size: 1.06rem;
67
+ line-height: 1.72;
68
+ color: var(--text);
69
+ background: var(--bg);
70
+ max-width: 46rem;
71
+ margin: 0 auto;
72
+ padding: 4.5rem 1.75rem 8rem;
73
+ -webkit-font-smoothing: antialiased;
74
+ text-rendering: optimizeLegibility;
75
+ }
76
+ ::selection { background: var(--accent-soft); }
77
+
78
+ h1, h2, h3, h4, h5, h6 {
79
+ font-weight: 640;
80
+ line-height: 1.25;
81
+ letter-spacing: -0.011em;
82
+ color: var(--text);
83
+ }
84
+ h1 { font-size: 2.1rem; margin: 0 0 .1em; letter-spacing: -0.02em; }
85
+ h2 { font-size: 1.5rem; margin: 2.4em 0 .6em; padding-bottom: .28em; border-bottom: 1px solid var(--border); }
86
+ h3 { font-size: 1.2rem; margin: 2em 0 .5em; }
87
+ h4 { font-size: 1.03rem; margin: 1.7em 0 .4em; }
88
+ h5, h6 { font-size: .92rem; margin: 1.5em 0 .4em; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
89
+
90
+ p { margin: 0 0 1.1em; }
91
+ a { color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color .12s ease; }
92
+ a:hover { border-bottom-color: var(--accent); }
93
+
94
+ ul, ol { margin: 0 0 1.1em; padding-left: 1.5em; }
95
+ li { margin: .3em 0; }
96
+ li > ul, li > ol { margin: .3em 0; }
97
+ li::marker { color: var(--muted); }
98
+
99
+ strong { font-weight: 680; }
100
+ em { font-style: italic; }
101
+
102
+ code {
103
+ font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
104
+ font-size: .86em;
105
+ background: var(--accent-soft);
106
+ color: var(--code-text);
107
+ padding: .12em .38em;
108
+ border-radius: 5px;
109
+ }
110
+ pre {
111
+ background: var(--surface);
112
+ border: 1px solid var(--border);
113
+ border-radius: 12px;
114
+ padding: 1rem 1.15rem;
115
+ overflow-x: auto;
116
+ line-height: 1.55;
117
+ margin: 1.3em 0;
118
+ }
119
+ pre code {
120
+ background: none;
121
+ color: var(--text);
122
+ padding: 0;
123
+ font-size: .84rem;
124
+ border-radius: 0;
125
+ }
126
+
127
+ blockquote {
128
+ border-left: 3px solid var(--accent);
129
+ margin: 1.3em 0;
130
+ padding: .1em 1.1em;
131
+ color: var(--muted);
132
+ background: var(--surface-2);
133
+ border-radius: 0 8px 8px 0;
134
+ font-style: italic;
135
+ }
136
+ blockquote p { margin: .5em 0; }
137
+
138
+ .table-wrap { overflow-x: auto; margin: 1.4em 0; }
139
+ table {
140
+ border-collapse: collapse;
141
+ width: 100%;
142
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
143
+ font-size: .9rem;
144
+ border: 1px solid var(--border);
145
+ border-radius: 10px;
146
+ overflow: hidden;
147
+ }
148
+ th, td { border-bottom: 1px solid var(--border); padding: .6em .85em; text-align: left; vertical-align: top; }
149
+ th { background: var(--surface); font-weight: 640; letter-spacing: -0.005em; }
150
+ tr:last-child td { border-bottom: none; }
151
+ tbody tr:nth-child(even) td { background: var(--surface-2); }
152
+
153
+ hr { border: none; border-top: 1px solid var(--border-strong); margin: 2.6em 0; }
154
+ img { max-width: 100%; height: auto; border-radius: 8px; }
155
+
156
+ .doc-meta {
157
+ display: inline-flex;
158
+ align-items: center;
159
+ gap: .5em;
160
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
161
+ font-size: .78rem;
162
+ color: var(--muted);
163
+ background: var(--surface);
164
+ border: 1px solid var(--border);
165
+ border-radius: 999px;
166
+ padding: .35em .85em;
167
+ margin-bottom: 2.4em;
168
+ }
169
+ .doc-meta::before {
170
+ content: "";
171
+ width: 7px; height: 7px; border-radius: 50%;
172
+ background: var(--accent);
173
+ flex: none;
174
+ }
175
+
176
+ .rev-history {
177
+ margin-top: 3.5em;
178
+ border-top: 1px solid var(--border-strong);
179
+ padding-top: 1.4em;
180
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
181
+ font-size: .85rem;
182
+ }
183
+ .rev-history > summary {
184
+ cursor: pointer;
185
+ color: var(--muted);
186
+ font-weight: 600;
187
+ letter-spacing: .01em;
188
+ list-style: none;
189
+ }
190
+ .rev-history > summary::-webkit-details-marker { display: none; }
191
+ .rev-history > summary::before { content: "▸ "; color: var(--accent); }
192
+ .rev-history[open] > summary::before { content: "▾ "; }
193
+ .rev-history ol { margin: 1em 0 0; padding: 0; list-style: none; }
194
+ .rev-history li {
195
+ margin: 0 0 1em; padding-left: 1em;
196
+ border-left: 2px solid var(--border);
197
+ }
198
+ .rev-history .rev-head {
199
+ display: flex; gap: .6em; align-items: baseline; flex-wrap: wrap;
200
+ color: var(--muted); font-size: .78rem;
201
+ }
202
+ .rev-history .rev-n { color: var(--accent); font-weight: 700; }
203
+ a.rev-n { border-bottom: 1px dotted var(--accent); }
204
+ a.rev-n:hover { border-bottom-style: solid; }
205
+ .rev-history .rev-author { color: var(--text); font-weight: 600; }
206
+ .doc-historical { border-style: dashed; }
207
+ .doc-historical::before { background: #d98b3c; }
208
+ .rev-history .rev-note { color: var(--text); font-weight: 600; margin: .2em 0 .1em; }
209
+ .rev-history .rev-summary { color: var(--muted); }
210
+
211
+ @media (max-width: 640px) {
212
+ body { padding: 2.5rem 1.15rem 5rem; font-size: 1.02rem; }
213
+ h1 { font-size: 1.7rem; }
214
+ h2 { font-size: 1.3rem; }
215
+ }
216
+ @media print {
217
+ body { max-width: none; color: #000; background: #fff; padding: 0; }
218
+ .doc-meta { display: none; }
219
+ pre, blockquote, table { break-inside: avoid; }
220
+ a { color: inherit; border-bottom: none; }
221
+ }
222
+ `;
223
+
224
+ const LINK_RE = /\b(href|src)="([^"]*)"/g;
225
+
226
+ function escapeHtml(s) {
227
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
228
+ .replace(/"/g, '&quot;');
229
+ }
230
+
231
+ function firstH1(text, fallback) {
232
+ const m = text.match(/^#\s+(.+)$/m);
233
+ return m ? m[1].trim() : fallback;
234
+ }
235
+
236
+ // ── Annotation revision history ──────────────────────────────────────────────
237
+ // One JSON file per artifacts folder, keyed by artifact .md filename:
238
+ // { "PRD.md": [ { at, rev, note, summary } ], ... }
239
+ // The /annotate-artifact loop appends an entry each time it applies feedback to
240
+ // the .md, so re-rendering surfaces "what changed, and why" per revision.
241
+ const HISTORY_FILE = '.annotation-history.json';
242
+ // Per-revision snapshots (a .md + rendered .html copy) so old revisions can be
243
+ // viewed later without re-deriving them.
244
+ const REV_DIR = '.revisions';
245
+
246
+ function readHistory(dir) {
247
+ try {
248
+ return JSON.parse(fs.readFileSync(path.join(dir, HISTORY_FILE), 'utf8'));
249
+ } catch {
250
+ return {};
251
+ }
252
+ }
253
+
254
+ // Who made the edit: git identity (repo-local or global), falling back to the
255
+ // machine hostname when there's no git / no configured user.
256
+ function detectAuthor(cwd) {
257
+ const git = (args) => {
258
+ try {
259
+ return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 })
260
+ .toString().trim();
261
+ } catch { return ''; }
262
+ };
263
+ const name = git(['config', 'user.name']);
264
+ if (name) {
265
+ const email = git(['config', 'user.email']);
266
+ return email ? `${name} <${email}>` : name;
267
+ }
268
+ return os.hostname();
269
+ }
270
+
271
+ function logHistory(dir, mdName, note, summary) {
272
+ const all = readHistory(dir);
273
+ const list = Array.isArray(all[mdName]) ? all[mdName] : [];
274
+ const rev = list.length ? (list[list.length - 1].rev ?? list.length) + 1 : 1;
275
+ const stem = mdName.replace(/\.md$/i, '');
276
+
277
+ // Snapshot the current .md (the post-edit state for this revision) plus a
278
+ // rendered HTML copy, so this revision can be reopened later as-is.
279
+ const snapshot = `${REV_DIR}/${stem}/rev-${rev}.html`;
280
+ try {
281
+ const snapDir = path.join(dir, REV_DIR, stem);
282
+ fs.mkdirSync(snapDir, { recursive: true });
283
+ const mdText = fs.readFileSync(path.join(dir, mdName), 'utf8');
284
+ fs.writeFileSync(path.join(snapDir, `rev-${rev}.md`), mdText, 'utf8');
285
+ const html = render(mdText, `${firstH1(mdText, stem)} (rev ${rev})`, mdName, [], [], {
286
+ historical: { rev, backHref: `../../${stem}.html` },
287
+ });
288
+ fs.writeFileSync(path.join(snapDir, `rev-${rev}.html`), html, 'utf8');
289
+ } catch (e) {
290
+ console.error(`warning: could not snapshot rev ${rev}: ${e.message}`);
291
+ }
292
+
293
+ list.push({
294
+ at: new Date().toISOString(),
295
+ rev,
296
+ author: detectAuthor(dir),
297
+ note: note || '',
298
+ summary: summary || '',
299
+ snapshot,
300
+ });
301
+ all[mdName] = list;
302
+ fs.writeFileSync(path.join(dir, HISTORY_FILE), JSON.stringify(all, null, 2) + '\n', 'utf8');
303
+ console.log(`logged rev ${rev} for ${mdName} (snapshot ${snapshot})`);
304
+
305
+ // Also record a concise entry in the epic's memory (parent dir) so the epic
306
+ // accumulates context automatically — the Memory button / next session sees
307
+ // what changed, not just the per-artifact revision history. Best-effort.
308
+ try {
309
+ const epicMem = path.join(__dirname, 'epic-memory.mjs');
310
+ const epicDir = path.dirname(dir); // artifacts/ → epic dir
311
+ if (fs.existsSync(epicMem) && fs.existsSync(path.join(epicDir, 'state.json'))) {
312
+ const text = `${mdName} (rev ${rev}): ${summary || note || 'edited via annotation'}`;
313
+ execFileSync(process.execPath, [epicMem, 'add', epicDir, '--kind', 'context', '--text', text], {
314
+ stdio: 'ignore', timeout: 5000,
315
+ });
316
+ }
317
+ } catch { /* epic memory is best-effort; never fail the render/log */ }
318
+ }
319
+
320
+ function renderHistorySection(entries) {
321
+ if (!entries || !entries.length) return '';
322
+ const items = entries.map((e) => {
323
+ const when = escapeHtml(e.at || '');
324
+ const revLabel = `rev ${escapeHtml(String(e.rev ?? ''))}`;
325
+ // Link each revision to its snapshot so you can open the old version.
326
+ // (Navigates when the .html is opened directly; annotron's own server
327
+ // doesn't serve sibling files.)
328
+ const revEl = e.snapshot
329
+ ? `<a class="rev-n" href="${escapeHtml(e.snapshot)}" title="Open this revision">${revLabel}</a>`
330
+ : `<span class="rev-n">${revLabel}</span>`;
331
+ const author = e.author ? `<span class="rev-author">${escapeHtml(e.author)}</span>` : '';
332
+ const note = e.note ? `<div class="rev-note">${escapeHtml(e.note)}</div>` : '';
333
+ const summary = e.summary ? `<div class="rev-summary">${escapeHtml(e.summary)}</div>` : '';
334
+ return `<li><div class="rev-head">${revEl}${author}<time>${when}</time></div>${note}${summary}</li>`;
335
+ }).join('\n');
336
+ return `\n<details class="rev-history">
337
+ <summary>Revision history (${entries.length})</summary>
338
+ <ol>
339
+ ${items}
340
+ </ol>
341
+ </details>`;
342
+ }
343
+
344
+ function slugify(text) {
345
+ return text.toLowerCase()
346
+ .replace(/<[^>]+>/g, '')
347
+ .replace(/[^\w\s-]/g, '')
348
+ .trim()
349
+ .replace(/\s+/g, '-');
350
+ }
351
+
352
+ // Heading ids (matches the old Python `toc` behaviour) so annotron selectors are stable.
353
+ function makeRenderer() {
354
+ const r = new Renderer();
355
+ r.heading = function (text, level, raw) {
356
+ const id = slugify(raw ?? text);
357
+ return `<h${level} id="${id}">${text}</h${level}>\n`;
358
+ };
359
+ return r;
360
+ }
361
+
362
+ /**
363
+ * Rewrite same-directory links to sibling artifacts (foo.md -> foo.html). Only touches
364
+ * relative links resolving to a .md in the same folder that matches a rendered sibling;
365
+ * ../x.md, sub/y.md, absolute paths, and external URLs keep pointing at the .md source.
366
+ */
367
+ function rewriteSiblingLinks(body, siblingStems) {
368
+ const stems = new Set(siblingStems);
369
+ if (!stems.size) return body;
370
+ return body.replace(LINK_RE, (whole, attr, url) => {
371
+ const hashIdx = url.indexOf('#');
372
+ const target = hashIdx === -1 ? url : url.slice(0, hashIdx);
373
+ const frag = hashIdx === -1 ? '' : url.slice(hashIdx);
374
+ if (!target.endsWith('.md')) return whole;
375
+ if (/^[a-zA-Z][\w+.-]*:/.test(target) || target.startsWith('/')) return whole; // external/absolute
376
+ const clean = target.startsWith('./') ? target.slice(2) : target;
377
+ if (clean.includes('/')) return whole; // points into another directory
378
+ const stem = clean.slice(0, -3);
379
+ if (!stems.has(stem)) return whole;
380
+ const prefix = target.startsWith('./') ? './' : '';
381
+ return `${attr}="${prefix}${stem}.html${frag}"`;
382
+ });
383
+ }
384
+
385
+ function wrapTables(body) {
386
+ return body
387
+ .replace(/<table>/g, '<div class="table-wrap"><table>')
388
+ .replace(/<\/table>/g, '</table></div>');
389
+ }
390
+
391
+ function render(text, title, sourceName, siblingStems = [], history = [], opts = {}) {
392
+ let body = marked.parse(text, { renderer: makeRenderer(), gfm: true });
393
+ body = rewriteSiblingLinks(body, siblingStems);
394
+ body = wrapTables(body);
395
+ const meta = opts.historical
396
+ ? `<div class="doc-meta doc-historical">Historical snapshot · rev ${escapeHtml(String(opts.historical.rev))} · <a href="${escapeHtml(opts.historical.backHref)}">← back to current</a></div>`
397
+ : `<div class="doc-meta">Rendered from ${escapeHtml(sourceName)} · annotation copy — edit the .md source, not this file</div>`;
398
+ // Snapshots don't carry a revision-history section (they're a point in time).
399
+ const historySection = opts.historical ? '' : renderHistorySection(history);
400
+ return `<!doctype html>
401
+ <html lang="en">
402
+ <head>
403
+ <meta charset="utf-8">
404
+ <meta name="viewport" content="width=device-width, initial-scale=1">
405
+ <title>${escapeHtml(title)}</title>
406
+ <style>${CSS}</style>
407
+ </head>
408
+ <body>
409
+ ${meta}
410
+ ${body}${historySection}
411
+ </body>
412
+ </html>
413
+ `;
414
+ }
415
+
416
+ function convertOne(mdPath, outPath, siblingStems = [], history = []) {
417
+ const text = fs.readFileSync(mdPath, 'utf8');
418
+ const base = path.basename(mdPath);
419
+ const title = firstH1(text, base.replace(/\.md$/i, ''));
420
+ fs.writeFileSync(outPath, render(text, title, base, siblingStems, history), 'utf8');
421
+ console.log(`wrote ${outPath}`);
422
+ }
423
+
424
+ function die(msg) { console.error(msg); process.exit(1); }
425
+
426
+ function mtime(p) {
427
+ try { return fs.statSync(p).mtimeMs; } catch { return 0; }
428
+ }
429
+
430
+ function main() {
431
+ const args = process.argv.slice(2);
432
+ if (!args.length) {
433
+ die('usage: node md-to-html.mjs <in.md> [out.html] | --all <dir> | --log <dir> <artifact.md> <note> <summary>');
434
+ }
435
+
436
+ // --log <artifactsDir> <artifact.md> <note> <summary>
437
+ // Append a revision-history entry; used by the /annotate-artifact loop after
438
+ // it applies feedback to the .md.
439
+ if (args[0] === '--log') {
440
+ const [, dir, mdName, note, summary] = args;
441
+ if (!dir || !mdName) die('usage: node md-to-html.mjs --log <dir> <artifact.md> <note> <summary>');
442
+ logHistory(dir, path.basename(mdName), note, summary);
443
+ return;
444
+ }
445
+
446
+ if (args[0] === '--all') {
447
+ const rest = args.slice(1);
448
+ const force = rest.includes('--force');
449
+ const dir = rest.find((a) => a !== '--force');
450
+ if (!dir) die('usage: node md-to-html.mjs --all <dir> [--force]');
451
+ const mdFiles = fs.readdirSync(dir)
452
+ .filter((n) => n.toLowerCase().endsWith('.md'))
453
+ .sort()
454
+ .map((n) => path.join(dir, n));
455
+ if (!mdFiles.length) die(`no .md files in ${dir}`);
456
+ const stems = mdFiles.map((p) => path.basename(p).replace(/\.md$/i, ''));
457
+ const history = readHistory(dir);
458
+ const historyMtime = mtime(path.join(dir, HISTORY_FILE));
459
+ for (const p of mdFiles) {
460
+ const out = p.replace(/\.md$/i, '.html');
461
+ // Skip re-gen when the .html is already newer than both the .md and the
462
+ // history file — reopening an unchanged artifact costs nothing.
463
+ if (!force && mtime(out) >= mtime(p) && mtime(out) >= historyMtime) {
464
+ console.log(`skip (up to date) ${out}`);
465
+ continue;
466
+ }
467
+ convertOne(p, out, stems, history[path.basename(p)] ?? []);
468
+ }
469
+ return;
470
+ }
471
+
472
+ const mdPath = args[0];
473
+ const outPath = args[1] || mdPath.replace(/\.md$/i, '.html');
474
+ const history = readHistory(path.dirname(mdPath));
475
+ convertOne(mdPath, outPath, [], history[path.basename(mdPath)] ?? []);
476
+ }
477
+
478
+ main();