@jabworks/condux 0.2.0 → 0.3.1

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -29
  3. package/index.js +21 -0
  4. package/package.json +6 -4
  5. package/skills/code-review/SKILL.md +136 -0
  6. package/skills/code-review/evals/trigger_eval.json +22 -0
  7. package/skills/discovery/SKILL.md +242 -0
  8. package/skills/discovery/evals/trigger_eval.json +24 -0
  9. package/skills/discovery/references/choice-server.js +216 -0
  10. package/skills/discovery/references/mockup-picker.md +90 -0
  11. package/skills/discovery/references/package.json +3 -0
  12. package/skills/draft-plan/SKILL.md +229 -0
  13. package/skills/draft-plan/evals/trigger_eval.json +22 -0
  14. package/skills/draft-plan/references/plan-template.md +96 -0
  15. package/skills/finalize/SKILL.md +123 -0
  16. package/skills/finalize/evals/trigger_eval.json +22 -0
  17. package/skills/finalize/references/rationalization-table.md +15 -0
  18. package/skills/plan-review/SKILL.md +215 -0
  19. package/skills/plan-review/evals/trigger_eval.json +22 -0
  20. package/skills/plan-review/references/annotate-server.js +465 -0
  21. package/skills/plan-review/references/install-codex-hook.sh +112 -0
  22. package/skills/plan-review/references/package.json +3 -0
  23. package/skills/plan-review/references/plan-review-template.html +730 -0
  24. package/skills/preflight/SKILL.md +128 -0
  25. package/skills/preflight/evals/trigger_eval.json +26 -0
  26. package/skills/root-cause-analysis/SKILL.md +82 -0
  27. package/skills/root-cause-analysis/evals/trigger_eval.json +22 -0
  28. package/skills/subagent-deployment/SKILL.md +105 -0
  29. package/skills/subagent-deployment/evals/trigger_eval.json +22 -0
  30. package/skills/subagent-deployment/references/safety-checklist.md +54 -0
  31. package/skills/subagent-execution/SKILL.md +182 -0
  32. package/skills/subagent-execution/agents/coder.md +55 -0
  33. package/skills/subagent-execution/agents/explorer.md +77 -0
  34. package/skills/subagent-execution/agents/planner.md +101 -0
  35. package/skills/subagent-execution/agents/researcher.md +78 -0
  36. package/skills/subagent-execution/evals/trigger_eval.json +22 -0
  37. package/skills/subagent-execution/references/install-codex-agents.mjs +98 -0
  38. package/skills/subagent-execution/references/review-package.sh +29 -0
  39. package/skills/subagent-execution/references/spawn-rules.md +88 -0
  40. package/skills/subagent-execution/references/task-brief.sh +39 -0
  41. package/skills/technical-spec/SKILL.md +190 -0
  42. package/skills/technical-spec/evals/trigger_eval.json +23 -0
  43. package/skills/technical-spec/references/scaffold.sh +98 -0
  44. package/skills/technical-spec/references/templates.md +159 -0
  45. package/skills/test-first-development/SKILL.md +121 -0
  46. package/skills/test-first-development/evals/trigger_eval.json +22 -0
  47. package/skills/test-first-development/references/rationalization-table.md +16 -0
  48. package/skills/workflow/SKILL.md +241 -0
  49. package/skills/workflow/evals/trigger_eval.json +30 -0
@@ -0,0 +1,730 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>review: {{PLAN_NAME}}</title>
7
+ <!-- Self-contained: no external fonts, scripts, or styles. Zero network egress.
8
+ UI components styled after the Terminus UI design system (Button, Badge,
9
+ Card, Textarea, Popover, Separator) — adapted as vanilla CSS, no React. -->
10
+ <style>
11
+ :root {
12
+ --background: #111110; --card: #191918; --card-foreground: #eeeeec;
13
+ --popover: #222221; --foreground: #eeeeec;
14
+ --muted: #222221; --muted-foreground: #b5b3ad; --border: #3b3a37; --input: #3b3a37;
15
+ --primary: #978365; --primary-foreground: #ffffff; --primary-hover: #a39073;
16
+ --primary-muted: #24231f; --primary-text: #cbb99f; --ring: #978365; --accent: #2a2a28;
17
+ --success: #71d083; --success-muted: #1b2a1e; --success-border: #2d5736;
18
+ --warning: #ffca16; --warning-muted: #302008; --warning-border: #5c3d05;
19
+ --destructive: #ff977d; --destructive-muted: #391714; --destructive-border: #6e2920;
20
+ --info: #70b8ff; --info-muted: #0d2847; --info-border: #104d87;
21
+ --hl: rgba(255, 202, 22, 0.30); --hl-active: rgba(255, 202, 22, 0.55);
22
+ --outline: rgba(237, 237, 236, 0.08); --subtle: #686560;
23
+ --radius: 0.25rem; --radius-md: 0.25rem;
24
+ --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Code", monospace;
25
+ }
26
+ @media (prefers-color-scheme: light) {
27
+ :root {
28
+ /* Terminus UI — light, sampled from the design system */
29
+ --background: #f6f5ef; --card: #fcfbf6; --card-foreground: #36302a;
30
+ --popover: #fcfbf6; --foreground: #36302a;
31
+ --muted: #f0efe3; --muted-foreground: #8c8678; --border: #ddd6c6; --input: #ddd6c6;
32
+ --primary: #967e60; --primary-foreground: #ffffff; --primary-hover: #897254;
33
+ --primary-muted: #efece0; --primary-text: #6f5e44; --ring: #967e60; --accent: #eae7da;
34
+ --success: #3f7a34; --success-muted: #e6f5e2; --success-border: #c2e1b3;
35
+ --warning: #93761d; --warning-muted: #fbf4c4; --warning-border: #ebd87f;
36
+ --destructive: #bf4f3b; --destructive-muted: #fbe8e2; --destructive-border: #f0c9ba;
37
+ --info: #3f6fa6; --info-muted: #e6f1fc; --info-border: #bed7ef;
38
+ --hl: rgba(234, 179, 8, 0.42); --hl-active: rgba(202, 138, 4, 0.62);
39
+ --outline: rgba(54, 48, 42, 0.08); --subtle: #a8a294;
40
+ }
41
+ }
42
+ * { box-sizing: border-box; }
43
+ html, body { height: 100%; }
44
+ body { margin: 0; background: var(--background); color: var(--foreground); font: 13px/1.6 var(--mono); font-variant-numeric: tabular-nums; display: flex; flex-direction: column; height: 100vh; }
45
+ .app { display: grid; grid-template-columns: 248px minmax(0, 1fr) 372px; flex: 1 1 auto; min-height: 0; }
46
+ ::-webkit-scrollbar { width: 9px; height: 9px; }
47
+ ::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 9999px; border: 2px solid transparent; background-clip: padding-box; }
48
+ ::-webkit-scrollbar-track { background: transparent; }
49
+
50
+ /* ---- Terminus: Button ---- */
51
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;
52
+ font: 500 13px var(--mono); border-radius: var(--radius-md); padding: 8px 14px; border: 1px solid transparent;
53
+ background: transparent; color: var(--foreground); cursor: pointer; transition: background-color .12s, border-color .12s, color .12s; }
54
+ .btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--background), 0 0 0 4px color-mix(in oklch, var(--ring) 45%, transparent); }
55
+ .btn:disabled { pointer-events: none; opacity: .4; }
56
+ .btn-sm { padding: 6px 10px; font-size: 12px; }
57
+ .btn-block { width: 100%; }
58
+ .btn-solid { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
59
+ .btn-solid:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
60
+ .btn-soft { background: var(--primary-muted); color: var(--primary-text); }
61
+ .btn-soft:hover { background: var(--accent); }
62
+ .btn-surface { background: var(--card); border-color: var(--border); color: var(--foreground); }
63
+ .btn-surface:hover { border-color: var(--primary); }
64
+ .btn-outline { background: transparent; border-color: var(--border); color: var(--foreground); }
65
+ .btn-outline:hover { background: var(--accent); border-color: var(--border); }
66
+ .btn-ghost { background: transparent; color: var(--muted-foreground); }
67
+ .btn-ghost:hover { background: var(--accent); color: var(--foreground); }
68
+ .btn-danger { background: var(--destructive-muted); color: var(--destructive); border-color: var(--destructive-border); }
69
+ .btn-danger:hover { background: color-mix(in oklch, var(--destructive-muted) 70%, var(--destructive)); }
70
+
71
+ /* ---- Terminus: Badge ---- */
72
+ .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--radius-md); border: 1px solid transparent;
73
+ font: 500 11px var(--mono); padding: 1px 6px; min-width: 18px; justify-content: center; }
74
+ .badge-warning { background: var(--warning-muted); color: var(--warning); border-color: var(--warning-border); }
75
+ .badge-soft { background: var(--primary-muted); color: var(--primary-text); }
76
+
77
+ /* Left: navigation — Contents (docs + headings) | Files (paths touched) */
78
+ .nav { border-right: 1px solid var(--border); background: var(--card); overflow-y: auto; padding: 18px 14px 40px; }
79
+ .nav .tabs { display: flex; margin: 0 0 12px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
80
+ .nav .tab { flex: 1; font: 500 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; padding: 6px 4px;
81
+ background: transparent; border: none; border-right: 1px solid var(--border); color: var(--subtle); cursor: pointer; }
82
+ .nav .tab:last-child { border-right: none; }
83
+ .nav .tab:hover { color: var(--foreground); }
84
+ .nav .tab.active { background: var(--accent); color: var(--foreground); }
85
+ .nav .group { font: 600 10px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--subtle); margin: 14px 6px 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
86
+ .nav a.doc { font-weight: 600; color: var(--foreground); margin-top: 10px; }
87
+ .nav a.doc.active { border-left-color: var(--primary); background: var(--accent); }
88
+ .nav a.file .t { font-size: 12px; }
89
+ .nav .none { color: var(--subtle); font-size: 12px; padding: 4px 6px; }
90
+ code.mention-flash { outline: 2px solid var(--warning); outline-offset: 1px; }
91
+ .nav .label, .chat .label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin: 0 0 12px; padding: 0 6px; }
92
+ .nav a { display: flex; align-items: center; gap: 6px; color: var(--muted-foreground); text-decoration: none; padding: 4px 6px; border-radius: 4px; border-left: 2px solid transparent; }
93
+ .nav a span.t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
94
+ .nav a:hover { background: var(--accent); color: var(--foreground); }
95
+ .nav a.active { color: var(--foreground); border-left-color: var(--primary); background: var(--accent); }
96
+ .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: 12px; color: var(--subtle); }
97
+
98
+ /* Center: plan */
99
+ .main { overflow-y: auto; padding: 28px 44px 120px; position: relative;
100
+ background-image:
101
+ linear-gradient(to right, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px),
102
+ linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px);
103
+ background-size: 32px 32px; }
104
+ .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: 12px; max-width: 760px; }
105
+ .doc { max-width: 760px; margin: 0 auto; }
106
+
107
+ /* Revision diff — on a revised plan, show what changed instead of a silent swap. */
108
+ .revbar { display: none; align-items: center; gap: 10px; max-width: 760px; margin: 0 auto 20px;
109
+ padding: 9px 12px; border: 1px solid var(--warning-border); background: var(--warning-muted); border-radius: var(--radius); }
110
+ .revbar.show { display: flex; }
111
+ .revbar .rb-text { color: var(--warning); font: 600 12px var(--mono); }
112
+ .revbar .rb-stat { color: var(--muted-foreground); font-size: 12px; }
113
+ .revbar .rb-actions { margin-left: auto; display: flex; gap: 6px; }
114
+ .diffwrap { display: none; max-width: 760px; margin: 0 auto; }
115
+ .diffwrap.show { display: block; }
116
+ .diffview { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius);
117
+ padding: 8px 0; overflow-x: auto; font: 12px/1.55 var(--mono); }
118
+ .diffline { display: flex; white-space: pre; padding: 0 12px; }
119
+ .diffline .g { width: 14px; flex: none; color: var(--subtle); user-select: none; }
120
+ .diffline.ctx { color: var(--muted-foreground); }
121
+ .diffline.add { background: var(--success-muted); color: var(--success); }
122
+ .diffline.add .g { color: var(--success); }
123
+ .diffline.del { background: var(--destructive-muted); color: var(--destructive); }
124
+ .diffline.del .g { color: var(--destructive); }
125
+ .diffline.fold { justify-content: center; color: var(--subtle); font-style: italic; padding: 3px 12px; }
126
+ h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1.5em 0 0.6em; scroll-margin-top: 24px; }
127
+ h1 { font-size: 21px; } h2 { font-size: 16px; } h3 { font-size: 14px; color: var(--muted-foreground); }
128
+ h4 { font-size: 13px; } h5, h6 { font-size: 12px; color: var(--muted-foreground); }
129
+ p { margin: 0.6em 0; }
130
+ ul, ol { margin: 0.6em 0; padding-left: 22px; } li { margin: 0.25em 0; }
131
+ li.task { list-style: none; margin-left: -18px; }
132
+ del { color: var(--subtle); }
133
+ a { color: var(--info); }
134
+ code { background: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
135
+ pre { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; overflow-x: auto; }
136
+ pre code { background: none; border: none; padding: 0; }
137
+ blockquote { margin: 0.6em 0; padding: 2px 14px; color: var(--muted-foreground); border-left: 3px solid var(--primary); }
138
+ hr { border: none; border-top: 1px solid var(--border); margin: 1.6em 0; }
139
+ table { border-collapse: collapse; margin: 0.9em 0; width: 100%; display: block; overflow-x: auto; font-size: 13px; }
140
+ th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; vertical-align: top; }
141
+ th { background: var(--muted); font-weight: 600; }
142
+ tbody tr:nth-child(even) { background: color-mix(in oklch, var(--muted) 40%, transparent); }
143
+ .blk { border-radius: 4px; padding: 1px 8px; margin: 0 -8px; }
144
+ mark.hl { background: var(--hl); color: inherit; border-radius: 2px; cursor: pointer; padding: 0 1px; }
145
+ mark.hl.active { background: var(--hl-active); box-shadow: 0 0 0 1px var(--warning-border); }
146
+
147
+ /* Floating "comment" button (Button surface) + comment Popover */
148
+ /* Step 1: menubar only. Step 2 (.expanded): reveal the input + actions. */
149
+ .popover:not(.expanded) .ta, .popover:not(.expanded) .row { display: none; }
150
+ .popover:not(.expanded) .cats { border-bottom: none; }
151
+ .popover:not(.expanded) .chip:first-child { border-bottom-left-radius: var(--radius); }
152
+ .popover:not(.expanded) .chip:last-child { border-bottom-right-radius: var(--radius); }
153
+ /* Floating annotation toolbar — a menubar of category items over an input. */
154
+ .popover { position: absolute; z-index: 31; display: none; width: 340px; background: var(--popover); border: 1px solid var(--border);
155
+ border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0,0,0,0.45); padding: 0; }
156
+ .popover .q { color: var(--muted-foreground); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
157
+ padding: 6px 10px; margin: 0; background: var(--muted); border-bottom: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; }
158
+ /* Menubar strip: category items, evenly split, divider-separated */
159
+ .popover .cats { display: flex; margin: 0; border-bottom: 1px solid var(--border); }
160
+ .chip { flex: 1; font: 500 11px var(--mono); padding: 6px 8px; text-align: center; border: none; border-right: 1px solid var(--border);
161
+ background: transparent; color: var(--muted-foreground); cursor: pointer; transition: background-color .12s, color .12s; }
162
+ .chip:last-child { border-right: none; }
163
+ .chip:hover { background: var(--accent); color: var(--foreground); }
164
+ .chip.active { background: var(--primary-muted); color: var(--primary-text); }
165
+ .popover .row { display: flex; justify-content: flex-end; gap: 8px; align-items: center; padding: 6px 8px; border-top: 1px solid var(--border); }
166
+ .popover .hintk { color: var(--subtle); font-size: 11px; margin-right: auto; }
167
+ /* Caret pointing at the selection (down when the toolbar sits above it) */
168
+ .popover::after { content: ''; position: absolute; left: 18px; width: 9px; height: 9px; background: var(--popover);
169
+ border: 1px solid var(--border); transform: rotate(45deg); display: none; }
170
+ .popover.above::after { display: block; bottom: -5px; border-top: none; border-left: none; }
171
+ .popover.below::after { display: block; top: -5px; background: var(--muted); border-bottom: none; border-right: none; }
172
+
173
+ /* ---- Terminus: Textarea ---- */
174
+ .ta { width: 100%; resize: vertical; font: 13px/1.5 var(--mono); color: var(--foreground); background: transparent;
175
+ border: 1px solid var(--input); border-radius: var(--radius-md); padding: 8px 10px; }
176
+ .ta::placeholder { color: var(--subtle); }
177
+ .ta:focus-visible { outline: none; border-color: var(--ring); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 28%, transparent); }
178
+ .popover .ta { min-height: 132px; border: none; border-radius: 0; padding: 8px 10px; } .popover .ta:focus-visible { outline: none; box-shadow: none; }
179
+ .composer .ta { min-height: 52px; }
180
+
181
+ /* Right: annotations (Cards) + composer + decisions */
182
+ .chat { border-left: 1px solid var(--border); background: var(--card); display: flex; flex-direction: column; }
183
+ .chat .label { padding: 16px 16px 0; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
184
+ .thread { flex: 1; overflow-y: auto; padding: 6px 14px; display: flex; flex-direction: column; gap: 8px; }
185
+ .thread .empty { color: var(--subtle); padding: 8px 4px; }
186
+ /* Terminus: Card */
187
+ .msg { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--card); box-shadow: 0 1px 2px rgba(0,0,0,0.16); cursor: default; }
188
+ .msg.note { border-color: var(--warning-border); cursor: pointer; }
189
+ .msg.note.active { box-shadow: 0 0 0 2px var(--warning); }
190
+ .msg .meta { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; align-items: center; }
191
+ .msg .who { color: var(--muted-foreground); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; } .msg.note .who { color: var(--warning); }
192
+ .msg .del { cursor: pointer; border: none; background: none; color: var(--subtle); font-size: 11px; padding: 0 2px; } .msg .del:hover { color: var(--destructive); }
193
+ .msg .quote { color: var(--muted-foreground); font-size: 11px; border-left: 2px solid var(--warning); padding: 1px 8px; margin-bottom: 5px; max-height: 42px; overflow: hidden; }
194
+ .msg .body { white-space: pre-wrap; word-break: break-word; color: var(--card-foreground); }
195
+
196
+ /* Terminus: Separator + composer */
197
+ .composer { border-top: 1px solid var(--border); padding: 12px 14px; }
198
+ .actions { padding: 0 14px 14px; }
199
+ .count { color: var(--subtle); font-size: 11px; }
200
+
201
+ /* Bottom decision bar — decisions read as the end of the review. Reject sits
202
+ isolated on the left; the two "continue" actions group on the right. The
203
+ summary in the middle says what rides along with the decision. */
204
+ .dbar { flex: none; display: flex; align-items: center; border-top: 1px solid var(--border); background: var(--card); padding: 10px 16px; min-height: 58px; }
205
+ .dbar-live { display: flex; flex: 1; align-items: center; gap: 12px; }
206
+ .dbar-summary { flex: 1; text-align: center; color: var(--subtle); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
207
+ .dbar-group { display: flex; gap: 8px; }
208
+
209
+ /* Post-submit confirmation — replaces the decision bar content once a decision
210
+ is sent. Per-decision copy tells you exactly what the agent does next. */
211
+ .sent { display: none; flex: 1; align-items: center; gap: 14px; border: 1px solid var(--border); border-radius: var(--radius); padding: 9px 14px; }
212
+ .sent.show { display: flex; }
213
+ .sent.approve { border-color: var(--success-border); background: var(--success-muted); }
214
+ .sent.revise { border-color: var(--warning-border); background: var(--warning-muted); }
215
+ .sent.deny { border-color: var(--destructive-border); background: var(--destructive-muted); }
216
+ .sent .h { display: flex; align-items: center; gap: 8px; font: 600 13px var(--mono); }
217
+ .sent.approve .h { color: var(--success); }
218
+ .sent.revise .h { color: var(--warning); }
219
+ .sent.deny .h { color: var(--destructive); }
220
+ .sent .h { flex: none; }
221
+ .sent .sub { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
222
+ .sent .ico { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }
223
+ .sent .again { margin-top: 10px; }
224
+ .dot { width: 7px; height: 7px; border-radius: 9999px; background: currentColor; animation: pulse 1.3s ease-in-out infinite; }
225
+ @keyframes pulse { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
226
+ </style>
227
+ </head>
228
+ <body>
229
+ <div class="app">
230
+ <aside class="nav">
231
+ <div class="tabs">
232
+ <button type="button" class="tab active" id="tab-contents">Contents</button>
233
+ <button type="button" class="tab" id="tab-files">Files</button>
234
+ </div>
235
+ <div id="pane-contents"><nav id="toc"></nav></div>
236
+ <div id="pane-files" style="display:none"><nav id="filelist"></nav></div>
237
+ </aside>
238
+
239
+ <main class="main">
240
+ <p class="hint">Select any text to comment on it. Or chat with the agent on the right. Then decide.</p>
241
+ <div class="revbar" id="revbar">
242
+ <span class="rb-text">Plan revised</span>
243
+ <span class="rb-stat" id="rb-stat"></span>
244
+ <span class="rb-actions">
245
+ <button type="button" class="btn btn-surface btn-sm" id="rb-toggle">View changes</button>
246
+ <button type="button" class="btn btn-ghost btn-sm" id="rb-dismiss">Dismiss</button>
247
+ </span>
248
+ </div>
249
+ <div class="doc" id="plan">Loading plan…</div>
250
+ <div class="diffwrap" id="diffwrap"><div class="diffview" id="diffview"></div></div>
251
+ <div class="popover" id="popover">
252
+ <div class="q" id="pop-quote"></div>
253
+ <div class="cats" id="pop-cats">
254
+ <button type="button" class="chip active" data-cat="Comment">Comment</button>
255
+ <button type="button" class="chip" data-cat="Issue">Issue</button>
256
+ <button type="button" class="chip" data-cat="Question">Question</button>
257
+ <button type="button" class="chip" data-cat="Suggestion">Suggestion</button>
258
+ <button type="button" class="chip" data-cat="Nitpick">Nitpick</button>
259
+ </div>
260
+ <textarea class="ta" id="pop-text" rows="6" placeholder="Your note on this selection…"></textarea>
261
+ <div class="row"><span class="hintk">⌘↵ to save</span>
262
+ <button class="btn btn-ghost btn-sm" id="pop-cancel">Cancel</button>
263
+ <button class="btn btn-solid btn-sm" id="pop-save">Save</button></div>
264
+ </div>
265
+ </main>
266
+
267
+ <aside class="chat">
268
+ <p class="label">Review <span class="count" id="count"></span></p>
269
+ <div class="thread" id="thread"><div class="empty">No notes yet. Select text to comment, or send a message.</div></div>
270
+ <div class="composer">
271
+ <textarea class="ta" id="input" placeholder="Message the agent about this plan…"></textarea>
272
+ </div>
273
+ <div class="actions">
274
+ <button class="btn btn-soft btn-block" id="send">Send message</button>
275
+ </div>
276
+ </aside>
277
+ </div>
278
+
279
+ <!-- Decisions live in a fixed bottom bar — the rail above is purely
280
+ conversational (notes + messages). Reject is isolated on the left. -->
281
+ <footer class="dbar">
282
+ <div class="dbar-live" id="dbar-live">
283
+ <button class="btn btn-danger" id="btn-reject" data-decision="Reject">Reject</button>
284
+ <span class="dbar-summary" id="dbar-summary"></span>
285
+ <span class="dbar-group">
286
+ <button class="btn btn-outline" data-decision="Request Revisions">Request revisions</button>
287
+ <button class="btn btn-solid" data-decision="Approve">Approve</button>
288
+ </span>
289
+ </div>
290
+ <div class="sent" id="sent">
291
+ <div class="h"><span class="ico" id="sent-ico"></span><span id="sent-title"></span></div>
292
+ <div class="sub" id="sent-sub"></div>
293
+ </div>
294
+ </footer>
295
+
296
+ <script>
297
+ // ---- Self-contained Markdown renderer (no external deps) ----
298
+ function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
299
+ function safeHref(s){const h=String(s).trim();const compact=h.replace(/[\u0000-\u0020]+/g,'');return /^(javascript|data|vbscript):/i.test(compact)?'#':h;}
300
+ function inline(s){
301
+ const codes=[];
302
+ s=esc(s).replace(/`([^`]+)`/g,function(_,c){codes.push('<code>'+c+'</code>');return '\u0000'+(codes.length-1)+'\u0000';});
303
+ s=s.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
304
+ .replace(/__([^_]+)__/g,'<strong>$1</strong>')
305
+ .replace(/(^|[^*])\*([^*]+)\*/g,'$1<em>$2</em>')
306
+ .replace(/(^|[^\w_])_([^_]+)_(?![\w_])/g,'$1<em>$2</em>')
307
+ .replace(/~~([^~]+)~~/g,'<del>$1</del>')
308
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g,function(_,label,href){return '<a href="'+safeHref(href)+'" rel="noopener noreferrer">'+label+'</a>';});
309
+ return s.replace(/\u0000(\d+)\u0000/g,function(_,n){return codes[+n];});
310
+ }
311
+ function renderBlocks(md){
312
+ const lines=md.replace(/\r\n/g,'\n').split('\n'); const out=[]; let i=0;
313
+ function tcells(row){ return row.trim().replace(/^\|/,'').replace(/\|\s*$/,'').split('|').map(function(c){return c.trim();}); }
314
+ function isDelim(row){ if(row.indexOf('|')<0) return false; const cs=tcells(row); return cs.length>0 && cs.every(function(c){return /^:?-{1,}:?$/.test(c);}); }
315
+ while(i<lines.length){let line=lines[i];
316
+ if(/^```/.test(line)){const buf=[];i++;while(i<lines.length&&!/^```/.test(lines[i]))buf.push(lines[i++]);i++;out.push({html:'<pre><code>'+esc(buf.join('\n'))+'</code></pre>',heading:null});continue;}
317
+ if(/^\s*$/.test(line)){i++;continue;}
318
+ if(/^#{1,6}\s/.test(line)){const lvl=line.match(/^#+/)[0].length;const text=line.replace(/^#+\s/,'');out.push({html:'<h'+lvl+'>'+inline(text)+'</h'+lvl+'>',heading:{level:lvl,text:text}});i++;continue;}
319
+ if(/^---+\s*$/.test(line)){out.push({html:'<hr>',heading:null});i++;continue;}
320
+ if(/^\s*>/.test(line)){const buf=[];while(i<lines.length&&/^\s*>/.test(lines[i]))buf.push(lines[i++].replace(/^\s*>\s?/,''));out.push({html:'<blockquote>'+inline(buf.join(' '))+'</blockquote>',heading:null});continue;}
321
+ if(/^\s*([-*+]|\d+\.)\s/.test(line)){const ordered=/^\s*\d+\./.test(line);const buf=[];while(i<lines.length&&/^\s*([-*+]|\d+\.)\s/.test(lines[i])){let it=lines[i].replace(/^\s*([-*+]|\d+\.)\s/,'');const cb=it.match(/^\[([ xX])\]\s+/);if(cb){it=it.slice(cb[0].length);buf.push('<li class="task">'+(/[xX]/.test(cb[1])?'☑':'☐')+' '+inline(it)+'</li>');}else{buf.push('<li>'+inline(it)+'</li>');}i++;}out.push({html:'<'+(ordered?'ol':'ul')+'>'+buf.join('')+'</'+(ordered?'ol':'ul')+'>',heading:null});continue;}
322
+ if(line.indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1])){const header=tcells(line);const aligns=tcells(lines[i+1]).map(function(c){const l=/^:/.test(c),r=/:$/.test(c);return l&&r?'center':r?'right':l?'left':'';});i+=2;const rows=[];while(i<lines.length&&lines[i].indexOf('|')>=0&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|---+\s*$)/.test(lines[i])){rows.push(tcells(lines[i]));i++;}let h='<table><thead><tr>';header.forEach(function(c,ci){h+='<th'+(aligns[ci]?' style="text-align:'+aligns[ci]+'"':'')+'>'+inline(c)+'</th>';});h+='</tr></thead><tbody>';rows.forEach(function(cs){h+='<tr>';header.forEach(function(_,ci){h+='<td'+(aligns[ci]?' style="text-align:'+aligns[ci]+'"':'')+'>'+inline(cs[ci]!==undefined?cs[ci]:'')+'</td>';});h+='</tr>';});h+='</tbody></table>';out.push({html:h,heading:null});continue;}
323
+ const buf=[];while(i<lines.length&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|\s*>|\s*([-*+]|\d+\.)\s|---+\s*$)/.test(lines[i])&&!(lines[i].indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1])))buf.push(lines[i++]);
324
+ out.push({html:'<p>'+inline(buf.join(' '))+'</p>',heading:null});}
325
+ return out;
326
+ }
327
+
328
+ // ---- State ----
329
+ const annotations = []; let nextId = 1;
330
+ const planEl=document.getElementById('plan');
331
+ const tocEl=document.getElementById('toc');
332
+ const pop=document.getElementById('popover');
333
+ const popQuote=document.getElementById('pop-quote');
334
+ const popText=document.getElementById('pop-text');
335
+ const popCats=document.getElementById('pop-cats');
336
+ const main=document.querySelector('.main');
337
+ let pendingRange=null, pendingQuote='', pendingSection='', pendingCat=null, selDoc=null;
338
+ function setCat(cat){ pendingCat=cat; popCats.querySelectorAll('.chip').forEach(function(x){x.classList.toggle('active',x.dataset.cat===cat);}); }
339
+ // Step 1 shows just the quote + category menubar. Choosing a category expands
340
+ // the toolbar to reveal the note input and action buttons (step 2).
341
+ popCats.querySelectorAll('.chip').forEach(function(c){
342
+ c.onmousedown=function(e){ e.preventDefault(); }; // keep the text selection alive
343
+ c.onclick=function(){ setCat(c.dataset.cat); pop.classList.add('expanded'); placePopover(); popText.focus(); };
344
+ });
345
+
346
+ // ---- Documents: one pane per doc (a single plan file, or a spec dir) ----
347
+ // Panes stay in the DOM when hidden, so highlights and note anchors on an
348
+ // inactive doc survive switching back and forth.
349
+ let DIRMODE=false, docs=[], activeDoc=null;
350
+ const docState={}; // name -> { md, pane, scroll, key, headings }
351
+
352
+ function renderDocPane(doc, md){
353
+ const st=docState[doc];
354
+ st.md=md; st.pane.innerHTML=''; st.headings=[];
355
+ let section='(top)';
356
+ renderBlocks(md).forEach(function(b,idx){
357
+ const id=st.key+'-blk-'+idx;
358
+ const wrap=document.createElement('div'); wrap.className='blk'; wrap.id=id; wrap.innerHTML=b.html;
359
+ if(b.heading){ section=id; st.headings.push({id:id,level:b.heading.level,text:b.heading.text}); }
360
+ wrap.dataset.section=section; st.pane.appendChild(wrap);
361
+ });
362
+ }
363
+
364
+ function buildToc(){
365
+ tocEl.innerHTML='';
366
+ var lastGroup=null;
367
+ docs.forEach(function(doc){
368
+ const st=docState[doc];
369
+ if(DIRMODE){
370
+ const slash=doc.lastIndexOf('/');
371
+ const group=slash>=0?doc.slice(0,slash):'';
372
+ if(group && group!==lastGroup){ const g=document.createElement('div'); g.className='group'; g.title=group; g.textContent=group; tocEl.appendChild(g); }
373
+ lastGroup=group;
374
+ const d=document.createElement('a'); d.href='#'; d.className='doc'; d.dataset.doc=doc;
375
+ const t=document.createElement('span'); t.className='t'; t.textContent=slash>=0?doc.slice(slash+1):doc; d.appendChild(t);
376
+ const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.fordoc=doc; d.appendChild(bd);
377
+ d.onclick=function(e){ e.preventDefault(); switchDoc(doc); main.scrollTop=0; };
378
+ tocEl.appendChild(d);
379
+ }
380
+ st.headings.forEach(function(h){
381
+ const a=document.createElement('a'); a.href='#'+h.id;
382
+ a.className='lvl'+(DIRMODE?Math.min(h.level+1,3):h.level); a.dataset.target=h.id; a.dataset.doc=doc;
383
+ const t=document.createElement('span'); t.className='t'; t.textContent=h.text; a.appendChild(t);
384
+ const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.for=h.id; a.appendChild(bd);
385
+ a.onclick=function(e){ e.preventDefault(); if(doc!==activeDoc)switchDoc(doc);
386
+ document.getElementById(h.id).scrollIntoView({behavior:'smooth',block:'start'}); };
387
+ tocEl.appendChild(a);
388
+ });
389
+ });
390
+ observeHeadings(); updateBadges();
391
+ }
392
+
393
+ function switchDoc(doc){
394
+ if(activeDoc&&docState[activeDoc]) docState[activeDoc].scroll=main.scrollTop;
395
+ activeDoc=doc;
396
+ docs.forEach(function(d){ docState[d].pane.style.display=d===doc?'':'none'; });
397
+ tocEl.querySelectorAll('a.doc').forEach(function(a){ a.classList.toggle('active',a.dataset.doc===doc); });
398
+ main.scrollTop=docState[doc].scroll||0;
399
+ observeHeadings();
400
+ }
401
+
402
+ // ---- Selection -> highlight ----
403
+ function blockOf(node){ while(node&&node!==planEl){ if(node.classList&&node.classList.contains('blk')) return node; node=node.parentNode; } return null; }
404
+
405
+ // Anchor the toolbar to the stored selection; sits above it, flips below
406
+ // only when the selection is too near the top of the scroll area.
407
+ function placePopover(){
408
+ if(!selDoc) return;
409
+ const pw=pop.offsetWidth, ph=pop.offsetHeight;
410
+ const left=Math.max(4, Math.min(selDoc.left, main.scrollWidth-pw-8));
411
+ let top=selDoc.top-ph-10;
412
+ if(top < main.scrollTop+4){ top=selDoc.bottom+10; pop.classList.add('below'); pop.classList.remove('above'); }
413
+ else { pop.classList.add('above'); pop.classList.remove('below'); }
414
+ pop.style.left=left+'px'; pop.style.top=top+'px';
415
+ }
416
+ function openToolbar(){
417
+ popQuote.textContent='“'+pendingQuote+'”'; popText.value='';
418
+ pendingCat=null; popCats.querySelectorAll('.chip').forEach(function(x){x.classList.remove('active');});
419
+ pop.classList.remove('expanded');
420
+ pop.style.display='block'; placePopover();
421
+ }
422
+ // Selecting text in the plan opens the menubar above the selection.
423
+ document.addEventListener('mouseup', function(e){
424
+ if(pop.contains(e.target)) return;
425
+ setTimeout(function(){
426
+ const sel=window.getSelection();
427
+ if(!sel.rangeCount||sel.isCollapsed) return;
428
+ const r=sel.getRangeAt(0);
429
+ if(!planEl.contains(r.commonAncestorContainer)||!sel.toString().trim()) return;
430
+ pendingRange=r.cloneRange(); pendingQuote=sel.toString().trim();
431
+ const blk=blockOf(pendingRange.startContainer); pendingSection=blk?blk.dataset.section:'(top)';
432
+ const rect=pendingRange.getBoundingClientRect(); const host=main.getBoundingClientRect();
433
+ selDoc={ left: rect.left-host.left+main.scrollLeft, top: rect.top-host.top+main.scrollTop, bottom: rect.bottom-host.top+main.scrollTop };
434
+ openToolbar();
435
+ },0);
436
+ });
437
+
438
+ function highlight(range, aid){
439
+ const root=range.commonAncestorContainer; const base=root.nodeType===3?root.parentNode:root;
440
+ const walker=document.createTreeWalker(base,NodeFilter.SHOW_TEXT,null); const nodes=[]; let n;
441
+ while(n=walker.nextNode()){ if(range.intersectsNode(n)&&n.nodeValue.trim()!=='') nodes.push(n); }
442
+ nodes.forEach(function(node){
443
+ let s=0,e=node.nodeValue.length;
444
+ if(node===range.startContainer) s=range.startOffset;
445
+ if(node===range.endContainer) e=range.endOffset;
446
+ if(s>=e) return;
447
+ const rng=document.createRange(); rng.setStart(node,s); rng.setEnd(node,e);
448
+ const m=document.createElement('mark'); m.className='hl'; m.dataset.aid=String(aid);
449
+ try{ rng.surroundContents(m); }catch(err){}
450
+ });
451
+ }
452
+ function unhighlight(aid){
453
+ document.querySelectorAll('mark.hl[data-aid="'+aid+'"]').forEach(function(m){
454
+ const p=m.parentNode; while(m.firstChild) p.insertBefore(m.firstChild,m); p.removeChild(m); p.normalize();
455
+ });
456
+ }
457
+
458
+ function saveNote(){
459
+ const text=popText.value.trim(); if(!text||!pendingRange) { closePop(); return; }
460
+ const id=nextId++;
461
+ try{ highlight(pendingRange,id); }catch(e){}
462
+ annotations.push({id:id,kind:'note',cat:pendingCat,quote:pendingQuote,text:text,section:pendingSection,doc:activeDoc});
463
+ closePop(); window.getSelection().removeAllRanges(); paint();
464
+ }
465
+ function closePop(){ pop.style.display='none'; pop.classList.remove('expanded'); pendingRange=null; }
466
+ document.getElementById('pop-save').onclick=saveNote;
467
+ document.getElementById('pop-cancel').onclick=function(){closePop();window.getSelection().removeAllRanges();};
468
+ popText.addEventListener('keydown',function(e){ if((e.metaKey||e.ctrlKey)&&e.key==='Enter')saveNote(); if(e.key==='Escape')closePop(); });
469
+ document.addEventListener('mousedown',function(e){ if(!pop.contains(e.target)&&pop.style.display==='block') closePop(); });
470
+
471
+ // Intra-tree markdown links (e.g. the spec catalog's [x](wan-config/index.md))
472
+ // point at real relative paths so they stay correct for agents/GitHub. In the
473
+ // viewer there is no such URL — resolve the href against the active doc's folder
474
+ // and, if it names a known doc, switch panes instead of hard-navigating to a 404.
475
+ function resolveDocHref(href){
476
+ if(!href||/^(https?:|mailto:|tel:|#|\/\/)/i.test(href)) return null;
477
+ let frag=''; const hash=href.indexOf('#'); if(hash>=0){ frag=href.slice(hash+1); href=href.slice(0,hash); }
478
+ if(!href) return {doc:activeDoc,frag:frag};
479
+ const baseDir=activeDoc&&activeDoc.indexOf('/')>=0?activeDoc.slice(0,activeDoc.lastIndexOf('/')):'';
480
+ const stack=[];
481
+ (baseDir?baseDir.split('/'):[]).concat(href.split('/')).forEach(function(p){
482
+ if(p===''||p==='.')return; if(p==='..')stack.pop(); else stack.push(p);
483
+ });
484
+ const cand=stack.join('/');
485
+ return docs.indexOf(cand)>=0?{doc:cand,frag:frag}:null;
486
+ }
487
+ planEl.addEventListener('click',function(e){
488
+ const a=e.target.closest&&e.target.closest('a');
489
+ if(a&&planEl.contains(a)){
490
+ const t=resolveDocHref(a.getAttribute('href'));
491
+ if(t){ e.preventDefault(); if(t.doc!==activeDoc)switchDoc(t.doc); main.scrollTop=0;
492
+ if(t.frag){ const el=document.getElementById(t.frag); if(el)el.scrollIntoView({behavior:'smooth',block:'start'}); }
493
+ return; }
494
+ }
495
+ const m=e.target.closest&&e.target.closest('mark.hl'); if(!m) return;
496
+ focusNote(m.dataset.aid,true);
497
+ });
498
+ function focusNote(aid,scroll){
499
+ document.querySelectorAll('mark.hl').forEach(function(x){x.classList.toggle('active',x.dataset.aid===String(aid));});
500
+ document.querySelectorAll('.msg.note').forEach(function(x){x.classList.toggle('active',x.dataset.aid===String(aid));});
501
+ if(scroll){const el=document.querySelector('.msg.note[data-aid="'+aid+'"]'); if(el)el.scrollIntoView({block:'nearest'});}
502
+ }
503
+
504
+ function sendChat(){ const ta=document.getElementById('input'); const text=ta.value.trim(); if(!text)return;
505
+ annotations.push({id:nextId++,kind:'chat',quote:'',text:text,section:''}); ta.value=''; paint(); }
506
+
507
+ function paint(){
508
+ const box=document.getElementById('thread'); box.innerHTML='';
509
+ document.getElementById('count').textContent=annotations.length?'('+annotations.length+')':'';
510
+ if(!annotations.length){ box.innerHTML='<div class="empty">No notes yet. Select text to comment, or send a message.</div>'; updateBadges(); updateSummary(); return; }
511
+ annotations.forEach(function(a){
512
+ const m=document.createElement('div'); m.className='msg'+(a.kind==='note'?' note':''); if(a.kind==='note')m.dataset.aid=String(a.id);
513
+ const meta=document.createElement('div'); meta.className='meta';
514
+ const who=document.createElement('span'); who.className='who'; who.textContent=a.kind==='note'?(a.cat||'Note'):'message';
515
+ const del=document.createElement('button'); del.className='del'; del.textContent='×';
516
+ del.onclick=function(ev){ev.stopPropagation(); if(a.kind==='note')unhighlight(a.id); const i=annotations.indexOf(a); annotations.splice(i,1); paint();};
517
+ meta.appendChild(who); meta.appendChild(del); m.appendChild(meta);
518
+ if(a.quote){ const q=document.createElement('div'); q.className='quote'; q.textContent='“'+a.quote+'”'; m.appendChild(q); }
519
+ const body=document.createElement('div'); body.className='body'; body.textContent=a.text; m.appendChild(body);
520
+ if(a.kind==='note') m.onclick=function(){ if(a.doc&&a.doc!==activeDoc)switchDoc(a.doc);
521
+ focusNote(a.id,false); const el=document.getElementById(a.section); if(el)el.scrollIntoView({behavior:'smooth',block:'center'}); };
522
+ box.appendChild(m);
523
+ });
524
+ updateBadges(); updateSummary();
525
+ }
526
+ // Live summary in the decision bar — what rides along with the decision.
527
+ function updateSummary(){
528
+ const n=annotations.filter(function(a){return a.kind==='note';}).length;
529
+ const m=annotations.filter(function(a){return a.kind==='chat';}).length;
530
+ const parts=[];
531
+ if(n)parts.push(n+' note'+(n>1?'s':'')); if(m)parts.push(m+' message'+(m>1?'s':''));
532
+ document.getElementById('dbar-summary').textContent=
533
+ parts.length?parts.join(' · ')+' will be sent with your decision':'No notes or messages yet — a decision sends on its own';
534
+ }
535
+ function updateBadges(){
536
+ const counts={}, docCounts={};
537
+ annotations.forEach(function(a){ if(a.kind!=='note')return;
538
+ if(a.section)counts[a.section]=(counts[a.section]||0)+1;
539
+ if(a.doc)docCounts[a.doc]=(docCounts[a.doc]||0)+1; });
540
+ document.querySelectorAll('.nav .badge[data-for]').forEach(function(b){ const c=counts[b.dataset.for]||0; b.textContent=c; b.style.display=c?'inline-flex':'none'; });
541
+ document.querySelectorAll('.nav .badge[data-fordoc]').forEach(function(b){ const c=docCounts[b.dataset.fordoc]||0; b.textContent=c; b.style.display=c?'inline-flex':'none'; });
542
+ }
543
+
544
+ let spy;
545
+ function observeHeadings(){ if(spy)spy.disconnect();
546
+ spy=new IntersectionObserver(function(es){es.forEach(function(en){ if(en.isIntersecting){document.querySelectorAll('#toc a[data-target]').forEach(function(a){a.classList.toggle('active',a.dataset.target===en.target.id);});} });},{root:document.querySelector('.main'),rootMargin:'0px 0px -75% 0px',threshold:0});
547
+ document.querySelectorAll('#toc a[data-target]').forEach(function(a){ if(a.dataset.doc!==activeDoc)return; const el=document.getElementById(a.dataset.target); if(el)spy.observe(el);});
548
+ }
549
+
550
+ // Per-decision confirmation copy — what the agent does with each verdict.
551
+ const SENT={
552
+ 'Approve': {cls:'approve',ico:'✓',wait:false,title:'Approved',sub:'Sent to the agent — it will proceed and implement the plan.'},
553
+ 'Request Revisions':{cls:'revise', ico:'',wait:true, title:'Revisions requested',sub:'Your notes are on their way. The agent will revise the plan; the updated version reloads here when it’s ready.'},
554
+ 'Reject': {cls:'deny', ico:'✕',wait:false,title:'Rejected',sub:'Sent to the agent — it will not implement this. It steps back to reconsider whether the feature should be built at all.'},
555
+ };
556
+ let submitting=false;
557
+ function clearAllNotes(){
558
+ annotations.slice().forEach(function(a){ if(a.kind==='note') unhighlight(a.id); });
559
+ annotations.length=0; nextId=1; paint();
560
+ }
561
+ function showSent(decision){
562
+ const cfg=SENT[decision]||SENT['Request Revisions'];
563
+ document.getElementById('dbar-live').style.display='none';
564
+ const ico=document.getElementById('sent-ico');
565
+ ico.className='ico'+(cfg.wait?' dot':''); ico.textContent=cfg.wait?'':cfg.ico;
566
+ document.getElementById('sent-title').textContent=cfg.title;
567
+ document.getElementById('sent-sub').textContent=cfg.sub;
568
+ document.getElementById('sent').className='sent show '+cfg.cls;
569
+ }
570
+ function resetDecisionUI(){
571
+ document.getElementById('sent').className='sent';
572
+ const a=document.getElementById('dbar-live'); a.style.display='';
573
+ a.querySelectorAll('button').forEach(function(b){b.disabled=false;});
574
+ }
575
+ async function submit(decision){
576
+ if(submitting) return; submitting=true;
577
+ document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=true;});
578
+ try{ await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:annotations})}); }
579
+ catch(e){}
580
+ clearAllNotes(); // notes clear the moment the decision is sent
581
+ showSent(decision);
582
+ submitting=false;
583
+ }
584
+ document.getElementById('send').onclick=sendChat;
585
+ document.getElementById('input').addEventListener('keydown',function(e){if((e.metaKey||e.ctrlKey)&&e.key==='Enter')sendChat();});
586
+ document.querySelectorAll('.dbar button[data-decision]').forEach(function(b){b.onclick=function(){submit(b.dataset.decision);};});
587
+
588
+ // ---- Revision diff (self-contained line-level LCS, no deps) ----
589
+ function lineDiff(a,b){
590
+ const A=(a||'').replace(/\r\n/g,'\n').split('\n'), B=(b||'').replace(/\r\n/g,'\n').split('\n');
591
+ const n=A.length,m=B.length; const dp=[]; for(let i=0;i<=n;i++)dp.push(new Int32Array(m+1));
592
+ for(let i=n-1;i>=0;i--)for(let j=m-1;j>=0;j--)
593
+ dp[i][j]=A[i]===B[j]?dp[i+1][j+1]+1:(dp[i+1][j]>=dp[i][j+1]?dp[i+1][j]:dp[i][j+1]);
594
+ const out=[]; let i=0,j=0;
595
+ while(i<n&&j<m){ if(A[i]===B[j]){out.push({t:'ctx',s:A[i]});i++;j++;}
596
+ else if(dp[i+1][j]>=dp[i][j+1]){out.push({t:'del',s:A[i]});i++;}
597
+ else{out.push({t:'add',s:B[j]});j++;} }
598
+ while(i<n)out.push({t:'del',s:A[i++]}); while(j<m)out.push({t:'add',s:B[j++]});
599
+ return out;
600
+ }
601
+ function renderDiff(diff){
602
+ const view=document.getElementById('diffview'); view.innerHTML=''; const FOLD=6; let run=[];
603
+ function line(t,s){ const el=document.createElement('div'); el.className='diffline '+t;
604
+ const g=document.createElement('span'); g.className='g'; g.textContent=t==='add'?'+':t==='del'?'-':' ';
605
+ el.appendChild(g); el.appendChild(document.createTextNode(s||' ')); view.appendChild(el); }
606
+ function flush(){ if(!run.length)return;
607
+ if(run.length>FOLD){ line('ctx',run[0].s); line('ctx',run[1].s);
608
+ const f=document.createElement('div'); f.className='diffline fold'; f.textContent='⋯ '+(run.length-4)+' unchanged lines'; view.appendChild(f);
609
+ line('ctx',run[run.length-2].s); line('ctx',run[run.length-1].s); }
610
+ else run.forEach(function(r){line('ctx',r.s);}); run=[]; }
611
+ diff.forEach(function(d){ if(d.t==='ctx')run.push(d); else{ flush(); line(d.t,d.s); } }); flush();
612
+ }
613
+ const diffwrap=document.getElementById('diffwrap');
614
+ function setDiffView(on){
615
+ diffwrap.classList.toggle('show',on);
616
+ planEl.style.display=on?'none':'';
617
+ document.getElementById('rb-toggle').textContent=on?'View rendered':'View changes';
618
+ }
619
+ function showRevBar(diff,doc){
620
+ const adds=diff.filter(function(d){return d.t==='add';}).length;
621
+ const dels=diff.filter(function(d){return d.t==='del';}).length;
622
+ document.querySelector('#revbar .rb-text').textContent=(DIRMODE&&doc)?doc+' revised':'Plan revised';
623
+ document.getElementById('rb-stat').textContent='+'+adds+' / -'+dels+' lines';
624
+ renderDiff(diff); document.getElementById('revbar').classList.add('show'); setDiffView(false);
625
+ }
626
+ function hideRevBar(){ document.getElementById('revbar').classList.remove('show'); setDiffView(false); }
627
+ document.getElementById('rb-toggle').onclick=function(){ setDiffView(!diffwrap.classList.contains('show')); };
628
+ document.getElementById('rb-dismiss').onclick=hideRevBar;
629
+
630
+ // ---- Files tab: paths the plan/spec touches ----
631
+ // Heuristic scan of inline code + fence contents. Conservative on purpose:
632
+ // false negatives are fine, noisy false positives are not.
633
+ function considerPath(tok,set){
634
+ tok=tok.replace(/^['"(<]+|[)>'".,;:!?]+$/g,'').replace(/^\.\//,'');
635
+ if(!tok||tok.length>200||/\s/.test(tok)||tok.indexOf('://')>=0||tok[0]==='#'||tok[0]==='-') return;
636
+ const segs=tok.split('/'); const last=segs[segs.length-1];
637
+ const hasExt=/^[\w@-][\w@.-]*\.[A-Za-z][A-Za-z0-9]{0,5}$/.test(last);
638
+ if(segs.length>=2&&/^[\w@.-]+(\/[\w@.-]+)+$/.test(tok)&&(hasExt||segs.length>=3)) set.add(tok);
639
+ }
640
+ function scanDocPaths(md,set){
641
+ const lines=(md||'').replace(/\r\n/g,'\n').split('\n'); let inFence=false;
642
+ lines.forEach(function(line){
643
+ if(/^```/.test(line)){ inFence=!inFence; return; }
644
+ if(inFence){ line.split(/\s+/).forEach(function(w){considerPath(w,set);}); return; }
645
+ const re=/`([^`]+)`/g; let m;
646
+ while((m=re.exec(line))){ m[1].split(/\s+/).forEach(function(w){considerPath(w,set);}); }
647
+ });
648
+ }
649
+ const mentionCycle={};
650
+ function jumpToMention(pth){
651
+ const hits=[];
652
+ docs.forEach(function(d){
653
+ docState[d].pane.querySelectorAll('code').forEach(function(c){ if(c.textContent.indexOf(pth)>=0) hits.push({doc:d,el:c}); });
654
+ });
655
+ if(!hits.length) return;
656
+ const i=(mentionCycle[pth]||0)%hits.length; mentionCycle[pth]=i+1;
657
+ const h=hits[i];
658
+ if(h.doc!==activeDoc) switchDoc(h.doc);
659
+ h.el.scrollIntoView({behavior:'smooth',block:'center'});
660
+ h.el.classList.add('mention-flash'); setTimeout(function(){h.el.classList.remove('mention-flash');},1200);
661
+ }
662
+ async function buildFilesIndex(){
663
+ const set=new Set();
664
+ docs.forEach(function(d){ scanDocPaths(docState[d].md,set); });
665
+ const paths=Array.from(set).sort();
666
+ let results={};
667
+ try{ const r=await (await fetch('/api/verify-paths',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({paths:paths})})).json(); results=r.results||{}; }catch(e){}
668
+ const box=document.getElementById('filelist'); box.innerHTML='';
669
+ if(!paths.length){ box.innerHTML='<div class="none">No file paths mentioned.</div>'; return; }
670
+ paths.forEach(function(pth){
671
+ const a=document.createElement('a'); a.href='#'; a.className='file'; a.title=pth;
672
+ const t=document.createElement('span'); t.className='t'; t.textContent=pth; a.appendChild(t);
673
+ if(pth in results){
674
+ const bd=document.createElement('span'); bd.className='badge '+(results[pth]?'badge-soft':'badge-warning');
675
+ bd.textContent=results[pth]?'exists':'new'; a.appendChild(bd);
676
+ }
677
+ a.onclick=function(e){ e.preventDefault(); jumpToMention(pth); };
678
+ box.appendChild(a);
679
+ });
680
+ }
681
+ const tabC=document.getElementById('tab-contents'), tabF=document.getElementById('tab-files');
682
+ function setTab(files){ tabC.classList.toggle('active',!files); tabF.classList.toggle('active',files);
683
+ document.getElementById('pane-contents').style.display=files?'none':'';
684
+ document.getElementById('pane-files').style.display=files?'':'none'; }
685
+ tabC.onclick=function(){setTab(false);}; tabF.onclick=function(){setTab(true);};
686
+
687
+ // ---- Load + live reload ----
688
+ function fetchDoc(doc){ return fetch(DIRMODE?'/api/plan?doc='+encodeURIComponent(doc):'/api/plan').then(function(r){return r.text();}); }
689
+ async function reloadDoc(doc){
690
+ if(!docState[doc]) return;
691
+ const md=await fetchDoc(doc);
692
+ const st=docState[doc];
693
+ if(md===st.md) return; // spurious watch event, no content change
694
+ const prev=st.md, revised=prev!==null;
695
+ if(revised){
696
+ // A revised doc invalidates its anchors. Drop that doc's notes only,
697
+ // restore the decision bar, and diff old vs new for this round.
698
+ annotations.slice().forEach(function(a){ if(a.doc===doc){ if(a.kind==='note')unhighlight(a.id); annotations.splice(annotations.indexOf(a),1); } });
699
+ resetDecisionUI();
700
+ }
701
+ renderDocPane(doc,md); buildToc(); paint(); buildFilesIndex();
702
+ if(revised){ if(doc!==activeDoc)switchDoc(doc); showRevBar(lineDiff(prev,md),doc); }
703
+ else hideRevBar();
704
+ }
705
+ async function boot(){
706
+ let meta={dir:false,docs:['plan']};
707
+ try{ meta=await (await fetch('/api/docs')).json(); }catch(e){}
708
+ DIRMODE=!!meta.dir; docs=meta.docs||[];
709
+ // Directory (spec) review is accept-or-fix — a spec documents work already
710
+ // decided, so the plan-only "Reject / don't build it" verdict doesn't apply.
711
+ if(DIRMODE){ const rj=document.getElementById('btn-reject'); if(rj)rj.style.display='none'; }
712
+ planEl.innerHTML='';
713
+ docs.forEach(function(d,i){
714
+ const pane=document.createElement('div'); pane.className='docpane'; pane.style.display='none';
715
+ planEl.appendChild(pane);
716
+ docState[d]={md:null,pane:pane,scroll:0,key:'d'+i,headings:[]};
717
+ });
718
+ for(let i=0;i<docs.length;i++){ const d=docs[i]; renderDocPane(d,await fetchDoc(d)); }
719
+ buildToc(); paint();
720
+ if(docs.length) switchDoc(docs[0]);
721
+ buildFilesIndex();
722
+ }
723
+ try{const es=new EventSource('/api/events');es.onmessage=function(ev){
724
+ const m=String((ev&&ev.data)||''); const doc=m.indexOf('change:')===0?m.slice(7):docs[0];
725
+ reloadDoc(doc);
726
+ };}catch(e){}
727
+ boot();
728
+ </script>
729
+ </body>
730
+ </html>