@hybridlabor-api/aos 4.13.2 → 4.14.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 (150) hide show
  1. package/.agents/AGENTS.md +8 -0
  2. package/.agents/nodes.json +5 -2
  3. package/.claude/hooks/conventional-commits.mjs +14 -15
  4. package/.claude/hooks/env-file-protection.mjs +14 -15
  5. package/.claude/hooks/go-gate.mjs +152 -10
  6. package/.claude/hooks/go-token.mjs +55 -0
  7. package/.claude/hooks/memb-inject.mjs +75 -62
  8. package/.claude/hooks/trail-autostart.mjs +27 -0
  9. package/.claude/settings.json +18 -0
  10. package/.claude/workflows/startcycle-dispatch.mjs +11 -4
  11. package/.opencode/plugins/bdb-aos.js +98 -121
  12. package/.opencode/plugins/lib/trail-autostart.js +38 -0
  13. package/CLAUDE.md +1 -1
  14. package/README.de.md +6 -6
  15. package/README.md +6 -6
  16. package/README.pt.md +6 -6
  17. package/THIRD_PARTY_NOTICES.md +19 -3
  18. package/assets/header-v5.png +0 -0
  19. package/bin/aos-acp.mjs +211 -0
  20. package/bin/aos-doctor.mjs +1 -1
  21. package/bin/aos-uninstall.mjs +2 -2
  22. package/docs/master-session-acp.md +51 -0
  23. package/installer.js +252 -35
  24. package/mcps/mcsc/packages/mcp/server.js +6 -7
  25. package/package.json +4 -3
  26. package/scripts/validate-skills.mjs +76 -0
  27. package/skills/basic/bdbmediastorm/SKILL.md +1 -1
  28. package/skills/basic/godmode-shipping/SKILL.md +3 -0
  29. package/skills/basic/master-session/SKILL.md +89 -0
  30. package/skills/basic/startcycle/SKILL.md +1 -1
  31. package/skills/basic/startcycle-graph/SKILL.md +2 -2
  32. package/skills/basic/startcycle-graph-user/SKILL.md +1 -1
  33. package/skills/basic/teamwork-preview/SKILL.md +1 -1
  34. package/skills/bdbrainstorm/SKILL.md +7 -1
  35. package/skills/global_config/agentic-harness-patterns/SKILL.md +257 -0
  36. package/skills/global_config/agentic-harness-patterns/metadata.json +10 -0
  37. package/skills/global_config/agentic-harness-patterns/references/agent-orchestration-pattern.md +97 -0
  38. package/skills/global_config/agentic-harness-patterns/references/bootstrap-sequence-pattern.md +106 -0
  39. package/skills/global_config/agentic-harness-patterns/references/context-engineering/compress-pattern.md +78 -0
  40. package/skills/global_config/agentic-harness-patterns/references/context-engineering/isolate-pattern.md +82 -0
  41. package/skills/global_config/agentic-harness-patterns/references/context-engineering/select-pattern.md +86 -0
  42. package/skills/global_config/agentic-harness-patterns/references/context-engineering-pattern.md +29 -0
  43. package/skills/global_config/agentic-harness-patterns/references/hook-lifecycle-pattern.md +111 -0
  44. package/skills/global_config/agentic-harness-patterns/references/memory-persistence-pattern.md +109 -0
  45. package/skills/global_config/agentic-harness-patterns/references/permission-gate-pattern.md +111 -0
  46. package/skills/global_config/agentic-harness-patterns/references/skill-runtime-pattern.md +104 -0
  47. package/skills/global_config/agentic-harness-patterns/references/task-decomposition-pattern.md +92 -0
  48. package/skills/global_config/agentic-harness-patterns/references/tool-registry-pattern.md +101 -0
  49. package/skills/global_config/agenttrail/SKILL.md +8 -0
  50. package/skills/global_config/agenttrail/bin/agenttrail.mjs +14 -0
  51. package/skills/global_config/agenttrail/bin/ensure.mjs +118 -0
  52. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  53. package/skills/global_config/bdb-visual-edit/SKILL.md +51 -0
  54. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +59 -0
  55. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +27 -0
  56. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +123 -0
  57. package/skills/global_config/factory-collect/SKILL.md +74 -0
  58. package/skills/global_config/factory-human-digest/SKILL.md +92 -0
  59. package/skills/global_config/factory-lookback/SKILL.md +95 -0
  60. package/skills/global_config/factory-review-prs/SKILL.md +63 -0
  61. package/skills/global_config/git-pr-review/SKILL.md +3 -0
  62. package/skills/global_config/grilling/SKILL.md +2 -0
  63. package/skills/global_config/mcsc/SKILL.md +1 -1
  64. package/skills/global_config/plan-arbiter/SKILL.md +125 -0
  65. package/skills/global_config/plan-canvas/SKILL.md +62 -5
  66. package/skills/global_config/plan-canvas/scripts/lib/loopback-guard.js +19 -3
  67. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +285 -0
  68. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/agent-trail.js +129 -0
  69. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/board-client.js +124 -0
  70. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/canvas.mdx +19 -0
  71. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/demo-plan/plan.mdx +18 -0
  72. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/recap-demo/plan.mdx +72 -0
  73. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/README.md +29 -0
  74. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/architecture.json +30 -0
  75. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/00_architecture.html +14950 -0
  76. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/canvas.mdx +511 -0
  77. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/builder/plan.mdx +208 -0
  78. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/recap/plan.mdx +102 -0
  79. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/showcase/standard/plan.md +136 -0
  80. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/canvas.mdx +124 -0
  81. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/examples/signup-storyboard/plan.mdx +37 -0
  82. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/index.js +188 -0
  83. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/kit.js +123 -0
  84. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/mdx.js +411 -0
  85. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +1291 -0
  86. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/meta.json +1 -0
  87. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/plan.mdx +195 -0
  88. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/architecture/standard.md +95 -0
  89. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/meta.json +1 -0
  90. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/plan.mdx +105 -0
  91. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/bugfix/standard.md +76 -0
  92. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/canvas.mdx +81 -0
  93. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/meta.json +1 -0
  94. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/plan.mdx +145 -0
  95. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/feature/standard.md +76 -0
  96. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/meta.json +1 -0
  97. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/plan.mdx +172 -0
  98. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/migration/standard.md +100 -0
  99. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/meta.json +1 -0
  100. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/plan.mdx +67 -0
  101. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap/standard.md +49 -0
  102. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/canvas.mdx +63 -0
  103. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/meta.json +1 -0
  104. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/plan.mdx +49 -0
  105. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-board/standard.md +39 -0
  106. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/meta.json +1 -0
  107. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/plan.mdx +118 -0
  108. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/recap-review/standard.md +57 -0
  109. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/meta.json +1 -0
  110. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/plan.mdx +173 -0
  111. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/release/standard.md +96 -0
  112. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/meta.json +1 -0
  113. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/plan.mdx +91 -0
  114. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/research/standard.md +54 -0
  115. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/canvas.mdx +53 -0
  116. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/meta.json +1 -0
  117. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/plan.mdx +225 -0
  118. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/templates/show-control/standard.md +111 -0
  119. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/theme.css +472 -0
  120. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/trail.js +216 -0
  121. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/markdown.js +1 -1
  122. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +37 -4
  123. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +125 -29
  124. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +196 -8
  125. package/skills/global_config/pr-recap/SKILL.md +47 -0
  126. package/skills/global_config/pr-recap/scripts/pr-recap.mjs +200 -0
  127. package/skills/global_config/quick-recap/SKILL.md +55 -0
  128. package/skills/global_config/stay-within-limits/SKILL.md +85 -0
  129. package/skills/global_config/triage/SKILL.md +3 -0
  130. package/skills/global_config/visual-edit/README.md +96 -0
  131. package/skills/global_config/visual-edit/SKILL.md +615 -0
  132. package/skills/global_config/visual-plan/README.md +93 -0
  133. package/skills/global_config/visual-plan/SKILL.md +544 -0
  134. package/skills/global_config/visual-plan/references/canvas.md +139 -0
  135. package/skills/global_config/visual-plan/references/connection.md +51 -0
  136. package/skills/global_config/visual-plan/references/document-quality.md +186 -0
  137. package/skills/global_config/visual-plan/references/exemplar.md +62 -0
  138. package/skills/global_config/visual-plan/references/local-files.md +99 -0
  139. package/skills/global_config/visual-plan/references/wireframe.md +319 -0
  140. package/skills/global_config/visual-recap/README.md +103 -0
  141. package/skills/global_config/visual-recap/SKILL.md +560 -0
  142. package/skills/global_config/visual-recap/references/connection.md +51 -0
  143. package/skills/global_config/visual-recap/references/local-files.md +99 -0
  144. package/skills/global_config/visual-recap/references/wireframe.md +319 -0
  145. package/skills/playbooks/pb-ci-fix/SKILL.md +49 -0
  146. package/skills/playbooks/pb-event-tracker/SKILL.md +45 -0
  147. package/skills/playbooks/pb-meeting-actions/SKILL.md +42 -0
  148. package/skills/playbooks/pb-project-new/SKILL.md +48 -0
  149. package/skills/playbooks/pb-week-plan/SKILL.md +45 -0
  150. package/assets/header-v4.jpg +0 -0
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Derive an agenttrail plan file (skills/global_config/agenttrail "Plan
5
+ * convention") from a plan folder: `## Name {#id}` components with optional
6
+ * `needs:` / `files:` / `url:` lines and `- [ ] task {#id}` items.
7
+ *
8
+ * Sources, in document order: plan.mdx, canvas.mdx, prototype.mdx.
9
+ * Components: headings tagged `{#id}` and `<Section id title>`.
10
+ * needs: `<Section needs=[...]>`, or frontmatter `needs-<id>: a, b` for headings.
11
+ * files: `<ImplementationMap files=...>`. Tasks: `<Checklist>` items.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const { parseMdx } = require('./mdx');
18
+ const { checklistItems } = require('./render');
19
+
20
+ const ID_RE = /^[a-z0-9][a-z0-9-]*$/;
21
+ const HEADING_ID_RE = /\s*\{#([A-Za-z0-9-]+)\}\s*$/;
22
+ const DEFAULT_OUT = path.join('production_artifacts', '00_execution_plan.md');
23
+ const SOURCES = ['plan.mdx', 'canvas.mdx', 'prototype.mdx'];
24
+
25
+ class TrailError extends Error {}
26
+
27
+ const oneLine = (value) => String(value == null ? '' : value).replace(/\s+/g, ' ').replace(/\{#[^}]*\}/g, '').trim();
28
+
29
+ function toIdList(value) {
30
+ const list = Array.isArray(value) ? value : typeof value === 'string' ? value.split(',') : [];
31
+ return list.map((v) => String(v).trim().toLowerCase()).filter(Boolean);
32
+ }
33
+
34
+ function slug(value) {
35
+ return String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
36
+ }
37
+
38
+ function entryPath(entry) {
39
+ if (typeof entry === 'string') return entry.trim();
40
+ if (entry && typeof entry === 'object') return String(entry.path ?? entry.file ?? entry.name ?? '').trim();
41
+ return '';
42
+ }
43
+
44
+ function implementationFiles(block) {
45
+ const raw = block.props.files;
46
+ let entries = [];
47
+ if (Array.isArray(raw)) entries = raw;
48
+ else if (raw && typeof raw === 'object') entries = Object.keys(raw);
49
+ if (!entries.length) {
50
+ entries = (block.children || []).filter((c) => c.type === 'prose')
51
+ .flatMap((c) => c.text.split('\n')).map((l) => l.replace(/^\s*[-*]\s+/, '').trim());
52
+ }
53
+ return entries.map(entryPath).filter(Boolean);
54
+ }
55
+
56
+ function collect(dir) {
57
+ const blocks = [];
58
+ let frontmatter = null;
59
+ for (const file of SOURCES) {
60
+ const full = path.join(dir, file);
61
+ if (!fs.existsSync(full)) continue;
62
+ const parsed = parseMdx(fs.readFileSync(full, 'utf8'));
63
+ if (!frontmatter) frontmatter = parsed.frontmatter || {};
64
+ blocks.push(...parsed);
65
+ }
66
+ return { blocks, frontmatter: frontmatter || {} };
67
+ }
68
+
69
+ function derive(dir, { workspaceRoot = process.cwd() } = {}) {
70
+ const { blocks, frontmatter } = collect(dir);
71
+ const components = [];
72
+ const used = new Set();
73
+ const warnings = [];
74
+
75
+ const uniqueId = (wanted, fallback) => {
76
+ let base = ID_RE.test(String(wanted).toLowerCase()) ? String(wanted).toLowerCase() : slug(fallback) || 'item';
77
+ if (!ID_RE.test(base)) base = `n-${base}`;
78
+ let id = base;
79
+ for (let n = 2; used.has(id); n += 1) id = `${base}-${n}`;
80
+ used.add(id);
81
+ return id;
82
+ };
83
+
84
+ const open = (title, wantedId, needs) => {
85
+ const component = { id: uniqueId(wantedId, title), title: oneLine(title) || wantedId, needs: toIdList(needs), files: [], url: '', tasks: [] };
86
+ components.push(component);
87
+ return component;
88
+ };
89
+
90
+ const walk = (list, current) => {
91
+ let cur = current;
92
+ for (const block of list) {
93
+ if (block.type === 'heading') {
94
+ const m = HEADING_ID_RE.exec(block.text);
95
+ if (m && m[1].toLowerCase() !== 'board') {
96
+ cur = open(block.text.replace(HEADING_ID_RE, ''), m[1], frontmatter[`needs-${m[1].toLowerCase()}`]);
97
+ } else if (block.level <= 2) {
98
+ cur = current;
99
+ }
100
+ continue;
101
+ }
102
+ if (block.type !== 'tag') continue;
103
+ if (block.name === 'Section' && block.props.id) {
104
+ walk(block.children || [], open(block.props.title || block.props.id, block.props.id, block.props.needs));
105
+ continue;
106
+ }
107
+ if ((block.name === 'ImplementationMap' || block.name === 'Checklist') && !cur) {
108
+ warnings.push(`${block.name} is not under a component and was ignored; add {#id} to the heading above it`);
109
+ continue;
110
+ }
111
+ if (block.name === 'ImplementationMap' && cur) {
112
+ for (const file of implementationFiles(block)) {
113
+ if (/[,\[\]\n]/.test(file)) warnings.push(`skipped file with unsupported characters: ${file}`);
114
+ else if (!cur.files.includes(file)) cur.files.push(file);
115
+ }
116
+ } else if (block.name === 'Checklist' && cur) {
117
+ for (const item of checklistItems(block)) {
118
+ const m = HEADING_ID_RE.exec(item.label);
119
+ const label = oneLine(item.label);
120
+ if (!label) continue;
121
+ cur.tasks.push({ id: uniqueId(m ? m[1] : '', `${cur.id}-${label.slice(0, 24)}`), label, checked: item.checked });
122
+ }
123
+ } else if (block.name === 'Archify' && cur && !cur.url) {
124
+ const rel = path.relative(workspaceRoot, path.resolve(dir, String(block.props.src || ''))).split(path.sep).join('/');
125
+ if (/^production_artifacts\/\S+$/.test(rel)) cur.url = rel;
126
+ } else if (block.children && block.children.length) {
127
+ walk(block.children, cur);
128
+ }
129
+ }
130
+ };
131
+ walk(blocks, null);
132
+
133
+ const ids = new Set(components.map((c) => c.id));
134
+ for (const c of components) {
135
+ const known = c.needs.filter((n) => ids.has(n) && n !== c.id);
136
+ if (known.length !== c.needs.length) warnings.push(`${c.id}: dropped needs that are not components`);
137
+ c.needs = known;
138
+ }
139
+
140
+ const title = oneLine(frontmatter.title) || oneLine((blocks.find((b) => b.type === 'heading' && b.level === 1) || {}).text) || 'Plan';
141
+ const lines = [`# ${title}`, ''];
142
+ for (const c of components) {
143
+ lines.push(`## ${c.title} {#${c.id}}`);
144
+ if (c.needs.length) lines.push(`needs: [${c.needs.join(', ')}]`);
145
+ if (c.files.length) lines.push(`files: [${c.files.join(', ')}]`);
146
+ if (c.url) lines.push(`url: ${c.url}`);
147
+ for (const t of c.tasks) lines.push(`- [${t.checked ? 'x' : ' '}] ${t.label} {#${t.id}}`);
148
+ lines.push('');
149
+ }
150
+ return {
151
+ text: lines.join('\n'),
152
+ components: components.length,
153
+ tasks: components.reduce((n, c) => n + c.tasks.length, 0),
154
+ warnings,
155
+ graph: components
156
+ };
157
+ }
158
+
159
+ function nearestRealpath(target) {
160
+ let cur = target;
161
+ const tail = [];
162
+ for (;;) {
163
+ try {
164
+ return path.join(fs.realpathSync(cur), ...tail);
165
+ } catch {
166
+ const parent = path.dirname(cur);
167
+ if (parent === cur) return target;
168
+ tail.unshift(path.basename(cur));
169
+ cur = parent;
170
+ }
171
+ }
172
+ }
173
+
174
+ function insideRoot(root, target) {
175
+ const rel = path.relative(root, target);
176
+ return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
177
+ }
178
+
179
+ /**
180
+ * @returns {{out: string, components: number, tasks: number, next_step: string, warnings?: string[]}}
181
+ * @throws {TrailError} with a message fit for the user; callers map it to exit 2.
182
+ */
183
+ function writeTrail(input, { out, force = false, workspaceRoot = process.cwd() } = {}) {
184
+ if (!input) throw new TrailError('trail requires a plan folder or plan.mdx path');
185
+ const target = path.resolve(input);
186
+ let dir = target;
187
+ if (fs.existsSync(target) && fs.statSync(target).isFile()) {
188
+ if (path.basename(target) !== 'plan.mdx') throw new TrailError(`expected plan.mdx (got ${path.basename(target)})`);
189
+ dir = path.dirname(target);
190
+ } else if (!fs.existsSync(path.join(target, 'plan.mdx'))) {
191
+ throw new TrailError(`no plan.mdx in ${input}`);
192
+ }
193
+
194
+ const root = nearestRealpath(path.resolve(workspaceRoot));
195
+ const outPath = path.resolve(workspaceRoot, out || DEFAULT_OUT);
196
+ const real = nearestRealpath(outPath);
197
+ if (!insideRoot(root, real)) throw new TrailError(`output must be inside the workspace root (${workspaceRoot}): ${out}`);
198
+ if (fs.existsSync(outPath) && !force) throw new TrailError(`${path.relative(workspaceRoot, outPath)} already exists; pass --force to overwrite`);
199
+ if (fs.existsSync(outPath) && fs.statSync(outPath).isDirectory()) throw new TrailError(`output is a directory: ${out}`);
200
+
201
+ const result = derive(dir, { workspaceRoot });
202
+ if (!result.components) throw new TrailError('no components found: tag headings with {#id} or use <Section id="..."> in the plan');
203
+
204
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
205
+ fs.writeFileSync(outPath, result.text, { flag: force ? 'w' : 'wx' });
206
+ const rel = path.relative(workspaceRoot, outPath).split(path.sep).join('/');
207
+ return {
208
+ out: rel,
209
+ components: result.components,
210
+ tasks: result.tasks,
211
+ next_step: `aos-trail . --plan ${rel} --no-open`,
212
+ ...(result.warnings.length ? { warnings: result.warnings } : {})
213
+ };
214
+ }
215
+
216
+ module.exports = { derive, writeTrail, TrailError };
@@ -298,4 +298,4 @@ function renderMarkdown(text) {
298
298
  return out.join('\n');
299
299
  }
300
300
 
301
- module.exports = { renderMarkdown, escapeHtml, slugify };
301
+ module.exports = { renderMarkdown, renderInline, escapeHtml, slugify };
@@ -16,7 +16,7 @@ const fs = require('fs');
16
16
  const http = require('http');
17
17
  const path = require('path');
18
18
 
19
- const { buildAllowedHostnames, isAllowedHostHeader, isAllowedOrigin } = require('../loopback-guard');
19
+ const { buildAllowedHostnames, isAllowedFetchSite, isAllowedHostHeader, isAllowedOrigin } = require('../loopback-guard');
20
20
  const { renderMarkdown } = require('./markdown');
21
21
  const { artifactSdkJs } = require('./sdk');
22
22
  const {
@@ -24,7 +24,8 @@ const {
24
24
  canvasClientJs,
25
25
  renderCanvasHtml,
26
26
  renderMarkdownArtifactHtml,
27
- renderSessionListHtml
27
+ renderHomeHtml,
28
+ VISUAL_SKILLS
28
29
  } = require('./ui');
29
30
 
30
31
  const DEFAULT_PORT = 4519;
@@ -130,6 +131,29 @@ function sendHtml(res, statusCode, html, { csp = true } = {}) {
130
131
  res.end(html);
131
132
  }
132
133
 
134
+ const STATIC_READ_PATH = /^\/(sdk\.js|artifact\/[a-f0-9]{12}\/.*)$/;
135
+
136
+ function skillDescription(file) {
137
+ try {
138
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(fs.readFileSync(file, 'utf8'));
139
+ const m = fm && /^description:\s*(.*)$/m.exec(fm[1]);
140
+ return m ? m[1].trim().replace(/^(["'])(.*)\1$/, '$2') : '';
141
+ } catch { return ''; }
142
+ }
143
+
144
+ function homeData(store) {
145
+ // Lazy: plan-canvas.js requires this module at load time.
146
+ const { cmdTemplates, findSkillMd } = require('../../plan-canvas');
147
+ return {
148
+ sessions: store.list(),
149
+ templates: cmdTemplates(),
150
+ skills: VISUAL_SKILLS.map(name => {
151
+ const file = findSkillMd(name);
152
+ return { name, installed: Boolean(file), description: file ? skillDescription(file) : '' };
153
+ })
154
+ };
155
+ }
156
+
133
157
  function createPlanCanvasServer({
134
158
  store,
135
159
  host = DEFAULT_HOST,
@@ -157,6 +181,7 @@ function createPlanCanvasServer({
157
181
  let idleTimer = null;
158
182
  let presenceSweep = null;
159
183
  let closed = false;
184
+ let boundPort = null; // set once listening; pins the allowed Origin port
160
185
 
161
186
  // --- presence + SSE ---------------------------------------------------
162
187
 
@@ -559,11 +584,18 @@ function createPlanCanvasServer({
559
584
  if (!isAllowedHostHeader(req.headers.host, allowedHostnames)) {
560
585
  return sendJson(res, 403, { error: 'forbidden host header' });
561
586
  }
562
- if (!isAllowedOrigin(req.headers.origin, allowedHostnames)) {
587
+ if (!isAllowedOrigin(req.headers.origin, allowedHostnames, boundPort)) {
563
588
  return sendJson(res, 403, { error: 'forbidden origin' });
564
589
  }
565
590
  const url = new URL(req.url, `http://${req.headers.host}`);
566
591
  const { pathname } = url;
592
+ // The artifact iframe is sandboxed without allow-same-origin, so its script
593
+ // and asset loads arrive as Sec-Fetch-Site: cross-site. Only those read-only
594
+ // static paths may skip the fetch-site check; everything else keeps it.
595
+ const isStaticRead = req.method === 'GET' && STATIC_READ_PATH.test(pathname);
596
+ if (!isStaticRead && !isAllowedFetchSite(req.headers['sec-fetch-site'])) {
597
+ return sendJson(res, 403, { error: 'forbidden fetch site' });
598
+ }
567
599
 
568
600
  Promise.resolve()
569
601
  .then(() => {
@@ -578,7 +610,7 @@ function createPlanCanvasServer({
578
610
  return undefined;
579
611
  }
580
612
  if (req.method === 'GET' && pathname === '/') {
581
- return sendHtml(res, 200, renderSessionListHtml(store.list()));
613
+ return sendHtml(res, 200, renderHomeHtml(homeData(store)));
582
614
  }
583
615
  if (req.method === 'GET' && pathname === '/canvas.css') {
584
616
  res.writeHead(200, { 'content-type': 'text/css; charset=utf-8', 'cache-control': 'no-store' });
@@ -641,6 +673,7 @@ function createPlanCanvasServer({
641
673
  return new Promise((resolve, reject) => {
642
674
  server.once('error', reject);
643
675
  server.listen(port, host, () => {
676
+ boundPort = server.address().port;
644
677
  armIdleTimer();
645
678
  resolve({ port: server.address().port, host });
646
679
  });
@@ -127,6 +127,10 @@ function canvasCss() {
127
127
  .panel{width:340px;flex:none;display:flex;flex-direction:column;border-left:1px solid var(--border);background:var(--bg2)}
128
128
  .panel h2{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--text3);padding:12px 14px 8px}
129
129
 
130
+ body.chat-hidden .panel{display:none}
131
+ .icon-btn .count{display:none;min-width:16px;height:16px;padding:0 4px;border-radius:99px;background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
132
+ .icon-btn .count.show{display:inline-block}
133
+
130
134
  .verdict{display:flex;gap:8px;padding:0 14px 12px;border-bottom:1px solid var(--border)}
131
135
  .verdict button{flex:1;height:30px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .12s}
132
136
  .verdict .approve{border:1px solid var(--green);background:var(--green-glow);color:var(--green)}
@@ -236,6 +240,32 @@ function canvasClientJs() {
236
240
  applyTheme(theme);
237
241
  });
238
242
 
243
+ // --- conversation panel ---------------------------------------------
244
+ const chatKey = 'ecc-plan-canvas:chat-hidden';
245
+ let chatHidden = false;
246
+ try { chatHidden = localStorage.getItem(chatKey) === '1'; } catch { /* site data blocked */ }
247
+ function applyChatHidden() {
248
+ document.body.classList.toggle('chat-hidden', chatHidden);
249
+ $('chatBtn').setAttribute('aria-pressed', String(chatHidden));
250
+ $('chatBtnLabel').textContent = chatHidden ? 'Show chat' : 'Hide chat';
251
+ }
252
+ function updateChatCount() {
253
+ const el = $('chatCount');
254
+ el.textContent = String(queue.length);
255
+ el.classList.toggle('show', chatHidden && queue.length > 0);
256
+ }
257
+ function toggleChat() {
258
+ chatHidden = !chatHidden;
259
+ try { localStorage.setItem(chatKey, chatHidden ? '1' : '0'); } catch { /* site data blocked */ }
260
+ applyChatHidden();
261
+ updateChatCount();
262
+ }
263
+ applyChatHidden();
264
+ $('chatBtn').addEventListener('click', toggleChat);
265
+ document.addEventListener('keydown', e => {
266
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'j') { e.preventDefault(); toggleChat(); }
267
+ }, true);
268
+
239
269
  // --- annotate mode -------------------------------------------------
240
270
  let annotate = true;
241
271
  function setAnnotate(on) {
@@ -273,6 +303,7 @@ function canvasClientJs() {
273
303
  function addToQueue(item) { queue.push(item); persistQueue(); renderQueue(); }
274
304
  function renderQueue() {
275
305
  queueEl.innerHTML = '';
306
+ updateChatCount();
276
307
  queue.forEach((item, i) => {
277
308
  const pill = document.createElement('div');
278
309
  pill.className = 'pill kind-' + item.kind;
@@ -498,13 +529,14 @@ function renderCanvasHtml(session, { clientPath = '/client.js', cssPath = '/canv
498
529
  <div id="annotate" class="toggle" role="switch" aria-pressed="true" title="Toggle annotate mode (Cmd/Ctrl+I)">
499
530
  <span>Annotate</span><span class="track"><span class="knob"></span></span>
500
531
  </div>
532
+ <button id="chatBtn" class="icon-btn" type="button" aria-pressed="false" title="Show or hide the conversation panel (Cmd/Ctrl+J)"><span id="chatBtnLabel">Hide chat</span><span id="chatCount" class="count"></span></button>
501
533
  <button id="themeBtn" class="icon-btn" type="button">light</button>
502
534
  <button id="reloadBtn" class="icon-btn" type="button" title="Reload artifact">Reload</button>
503
535
  <button id="endBtn" class="icon-btn danger" type="button">End session</button>
504
536
  </header>
505
537
  <div class="layout">
506
538
  <main class="frame">
507
- <iframe id="artifact" title="Artifact under review" src="${artifactSrc}" data-artifact-src="${artifactSrc}" sandbox="allow-scripts allow-forms allow-popups"></iframe>
539
+ <iframe id="artifact" title="Artifact under review" src="${artifactSrc}" data-artifact-src="${artifactSrc}" sandbox="allow-scripts allow-forms allow-popups" allow="fullscreen"></iframe>
508
540
  <div id="endedOverlay" class="overlay"><div class="card"><h3>Session ended</h3><p id="endedWho"></p></div></div>
509
541
  </main>
510
542
  <aside class="panel">
@@ -592,39 +624,100 @@ ${hasMermaid ? mermaidLoaderScript(mermaidUrl()) : ''}
592
624
  </html>`;
593
625
  }
594
626
 
595
- // Landing page listing sessions (GET /).
596
- function renderSessionListHtml(sessions) {
597
- const rows = sessions.map(s => {
598
- const status = s.status === 'ended' ? `ended by ${escapeHtml(s.endedBy || 'agent')}` : s.status;
599
- const link = s.status === 'ended'
600
- ? escapeHtml(path.basename(s.file))
601
- : `<a href="/canvas/${escapeHtml(s.key)}">${escapeHtml(path.basename(s.file))}</a>`;
602
- return `<tr><td>${link}</td><td class="mono">${escapeHtml(s.file)}</td><td><span class="badge ${escapeHtml(s.status)}">${status}</span></td></tr>`;
603
- }).join('\n');
627
+ const VISUAL_SKILLS = ['visual-plan', 'visual-recap', 'visual-review', 'visual-edit', 'prototype', 'archify', 'agenttrail'];
628
+
629
+ function artifactKind(file) {
630
+ const base = path.basename(String(file));
631
+ if (base === 'plan.builder.html') return 'BDB Plan Builder';
632
+ if (/\.html?$/i.test(base)) return 'HTML artifact';
633
+ return 'Standard markdown';
634
+ }
635
+
636
+ const HOME_CSS = `
637
+ :root{--bg:#0a0a0a;--card:#161616;--line:#262626;--ink:#fff;--ink2:#b8b8b8;--ink3:#7a7a7a;--accent:#9b30c4;--accent2:#c05ee6;
638
+ --font:-apple-system,BlinkMacSystemFont,'SF Pro Display','Inter','Segoe UI',Roboto,sans-serif;--mono:'SF Mono','Fira Code','JetBrains Mono',Menlo,monospace}
639
+ *{margin:0;padding:0;box-sizing:border-box}
640
+ body{font-family:var(--font);background:var(--bg);color:var(--ink);line-height:1.5;padding:32px 20px 64px}
641
+ main{max-width:1000px;margin:0 auto}
642
+ header{display:flex;align-items:center;gap:10px;margin-bottom:8px}
643
+ .mark{width:12px;height:12px;border-radius:3px;background:var(--accent)}
644
+ h1{font-size:20px;letter-spacing:-.01em}
645
+ h2{font-size:11px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--ink3);margin:32px 0 12px}
646
+ .sub{font-size:13px;color:var(--ink2)}
647
+ a{color:var(--accent2);text-decoration:none}
648
+ a:hover{text-decoration:underline}
649
+ .grid{display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(290px,1fr))}
650
+ .card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;min-width:0}
651
+ .card .t{font-size:14px;font-weight:600;overflow-wrap:anywhere}
652
+ .card .d{font-size:12.5px;color:var(--ink2)}
653
+ .card .u{font-size:12px;color:var(--ink3)}
654
+ .row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
655
+ .chip{font-size:10.5px;font-weight:600;padding:2px 8px;border-radius:99px;border:1px solid var(--line);color:var(--ink2);white-space:nowrap}
656
+ .chip.accent{border-color:var(--accent);color:var(--accent2)}
657
+ .chip.ok{border-color:var(--accent);color:#fff;background:rgba(155,48,196,.25)}
658
+ .chip.missing{color:var(--ink3)}
659
+ .mono,code{font-family:var(--mono);font-size:11.5px;color:var(--ink2);overflow-wrap:anywhere}
660
+ code.cmd{display:block;background:#0f0f0f;border:1px solid var(--line);border-radius:6px;padding:6px 8px;user-select:all}
661
+ .empty{color:var(--ink3);font-size:13px}
662
+ .card.ended{opacity:.6}
663
+ `;
664
+
665
+ function chip(text, cls = '', title = '') {
666
+ return `<span class="chip ${cls}"${title ? ` title="${escapeHtml(title)}"` : ''}>${escapeHtml(text)}</span>`;
667
+ }
668
+
669
+ function sessionCard(s) {
670
+ const ended = s.status === 'ended';
671
+ const base = path.basename(s.file);
672
+ const name = escapeHtml(/^(plan\.builder\.html|plan\.md|index\.html)$/.test(base) ? `${path.basename(path.dirname(s.file))}/${base}` : base);
673
+ const title = ended ? name : `<a href="/canvas/${escapeHtml(s.key)}">${name}</a>`;
674
+ const status = ended ? `ended by ${s.endedBy || 'agent'}` : s.status;
675
+ const pending = Number(s.pending) > 0 ? chip(`${s.pending} pending feedback`, 'accent') : '';
676
+ return `<div class="card${ended ? ' ended' : ''}"><div class="t">${title}</div>
677
+ <div class="row">${chip(artifactKind(s.file), 'accent')}${chip(status, ended ? '' : 'ok')}${pending}</div>
678
+ <div class="mono">${escapeHtml(s.file)}</div></div>`;
679
+ }
680
+
681
+ function templateCard(t) {
682
+ const board = t.hasBoard ? chip('board', 'accent', 'includes a design canvas with screens and arrows') : '';
683
+ return `<div class="card"><div class="t">${escapeHtml(t.label || t.id)} <span class="mono">${escapeHtml(t.id)}</span></div>
684
+ <div class="row">${board}</div>
685
+ <div class="d">${escapeHtml(t.description || '')}</div>
686
+ <div class="u">Use when: ${escapeHtml(t.useWhen || '')}</div>
687
+ <code class="cmd">aos-plan-canvas new ${escapeHtml(t.id)} &lt;dir&gt;</code></div>`;
688
+ }
689
+
690
+ function skillCard(k) {
691
+ const desc = String(k.description || '');
692
+ const short = desc.length > 140 ? `${desc.slice(0, 139).trimEnd()}…` : desc;
693
+ return `<div class="card"><div class="t">${escapeHtml(k.name)}</div>
694
+ <div class="row">${k.installed ? chip('installed', 'ok') : chip('missing', 'missing')}</div>
695
+ <div class="d">${escapeHtml(short)}</div></div>`;
696
+ }
697
+
698
+ // Landing page (GET /): open reviews, plan templates, visual skills. Read-only, no scripts.
699
+ function renderHomeHtml({ sessions = [], templates = [], skills = [] } = {}) {
700
+ const sorted = [...sessions].sort((a, b) =>
701
+ (a.status === 'ended') - (b.status === 'ended') || String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
604
702
  return `<!DOCTYPE html>
605
703
  <html lang="en">
606
704
  <head>
607
705
  <meta charset="utf-8">
608
- <title>Plan Canvas · sessions</title>
609
- <style>
610
- ${TOKENS_CSS}
611
- *{margin:0;padding:0;box-sizing:border-box}
612
- body{font-family:var(--font);background:var(--bg);color:var(--text);padding:40px;line-height:1.5}
613
- .logo{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--pink));border-radius:7px;display:inline-flex;align-items:center;justify-content:center;font-weight:700;color:#fff;margin-right:10px;vertical-align:middle}
614
- h1{font-size:18px;display:inline-block;vertical-align:middle}
615
- table{margin-top:24px;border-collapse:collapse;width:100%;max-width:900px;font-size:13px}
616
- th,td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border)}
617
- th{color:var(--text3);font-size:11px;text-transform:uppercase;letter-spacing:.05em}
618
- a{color:var(--accent);text-decoration:none}
619
- .mono{font-family:var(--mono);font-size:11.5px;color:var(--text2)}
620
- .badge{font-size:11px;padding:2px 8px;border-radius:99px;background:var(--bg3);border:1px solid var(--border);color:var(--text2)}
621
- .badge.open,.badge.feedback{color:var(--green);border-color:var(--green);background:var(--green-glow)}
622
- .empty{margin-top:24px;color:var(--text3);font-size:13px}
623
- </style>
706
+ <meta name="viewport" content="width=device-width, initial-scale=1">
707
+ <title>Plan Canvas</title>
708
+ <style>${HOME_CSS}</style>
624
709
  </head>
625
710
  <body>
626
- <span class="logo">E</span><h1>Plan Canvas sessions</h1>
627
- ${sessions.length ? `<table><thead><tr><th>Artifact</th><th>Path</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>` : '<p class="empty">No sessions yet. Ask your agent to open a plan with the plan-canvas skill.</p>'}
711
+ <main>
712
+ <header><span class="mark"></span><h1>Plan Canvas</h1></header>
713
+ <p class="sub">Review plans and HTML artifacts with your agent. Start new ones from the terminal; this page only lists what exists.</p>
714
+ <h2>Open reviews</h2>
715
+ ${sorted.length ? `<div class="grid">${sorted.map(sessionCard).join('\n')}</div>` : '<p class="empty">No sessions yet. Ask your agent to open a plan with the plan-canvas skill.</p>'}
716
+ <h2>Plan templates</h2>
717
+ ${templates.length ? `<div class="grid">${templates.map(templateCard).join('\n')}</div>` : '<p class="empty">No templates found.</p>'}
718
+ <h2>Visual skills</h2>
719
+ <div class="grid">${skills.map(skillCard).join('\n')}</div>
720
+ </main>
628
721
  </body>
629
722
  </html>`;
630
723
  }
@@ -632,7 +725,10 @@ ${sessions.length ? `<table><thead><tr><th>Artifact</th><th>Path</th><th>Status<
632
725
  module.exports = {
633
726
  canvasCss,
634
727
  canvasClientJs,
728
+ mermaidLoaderScript,
729
+ mermaidUrl,
635
730
  renderCanvasHtml,
636
731
  renderMarkdownArtifactHtml,
637
- renderSessionListHtml
732
+ renderHomeHtml,
733
+ VISUAL_SKILLS
638
734
  };