@rsaglobaltech/specgate 0.23.0 → 0.24.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 (28) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/packages/core/src/domain/DoneVerification.js +21 -2
  3. package/dist/packages/core/src/domain/DoneVerification.js.map +1 -1
  4. package/dist/packages/core/test/unit/done-verification.test.js +18 -0
  5. package/dist/packages/core/test/unit/done-verification.test.js.map +1 -1
  6. package/dist/scripts/cli/commands/harness/RunCommand.js +50 -5
  7. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  8. package/dist/scripts/cli/commands/quality/CheckCommand.js +6 -0
  9. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
  10. package/dist/scripts/cli/commands/spec/DoneCommand.js +2 -1
  11. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
  12. package/dist/scripts/cli/commands/spec/StudioCommand.js +31 -0
  13. package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
  14. package/dist/scripts/harness/events.js +57 -0
  15. package/dist/scripts/harness/events.js.map +1 -1
  16. package/dist/scripts/lib/harness-events.js +32 -1
  17. package/dist/scripts/lib/harness-events.js.map +1 -1
  18. package/dist/scripts/lib/harness-guard.js +186 -0
  19. package/dist/scripts/lib/harness-guard.js.map +1 -0
  20. package/dist/scripts/lib/in-flight.js +17 -4
  21. package/dist/scripts/lib/in-flight.js.map +1 -1
  22. package/dist/scripts/lib/live-detail.js +345 -0
  23. package/dist/scripts/lib/live-detail.js.map +1 -0
  24. package/dist/scripts/lib/live-model.js +98 -7
  25. package/dist/scripts/lib/live-model.js.map +1 -1
  26. package/dist/scripts/lib/live-page.js +460 -118
  27. package/dist/scripts/lib/live-page.js.map +1 -1
  28. package/package.json +1 -1
@@ -6,12 +6,21 @@ exports.livePage = livePage;
6
6
  * resources, filled from `/live.json` and kept current by `/live/stream`.
7
7
  * Every value from an event is set with textContent, never as HTML.
8
8
  *
9
- * REQ-179: laid out the way CI runs are read. A sidebar lists the run's
10
- * requirements by module with their status and duration; the main pane sums
11
- * the run up, draws its requirements as a graph of jobs, and opens the
12
- * selected one (the requirement at work, unless another is clicked) into its
13
- * steps — Agent, then each stage of the gate, up to the merge — with the step
14
- * in progress showing the agent's own stream as its log.
9
+ * REQ-179: laid out the way CI runs are read — a sidebar of requirements by
10
+ * module, the run summed up, its requirements as a graph of jobs, and the
11
+ * selected one's steps with the agent's stream as the log of the step in
12
+ * progress.
13
+ *
14
+ * REQ-182: the sidebar is the run's work queue (now, needs attention, up next,
15
+ * finished recently) with the agent's model, context and tokens; an Agent tab
16
+ * shows the selected attempt's session and the prompt it was given.
17
+ *
18
+ * REQ-181: the header stays in view while the page scrolls; modules fold, in
19
+ * the sidebar as an accordion the viewer opens and closes, and in the graph
20
+ * once all their requirements have passed; the selected requirement opens into
21
+ * tabs (Steps, Requirement, Tests and files, Attempts) read from
22
+ * `/live/req/<id>.json`; and a second view shows the traceability matrix as it
23
+ * stands, from `/live/matrix.json`.
15
24
  *
16
25
  * The script lives inside this template literal: it uses no backticks, no
17
26
  * dollar-brace, and no backslash escapes.
@@ -25,89 +34,181 @@ function livePage(projectName) {
25
34
  <style>
26
35
  :root{--bg:#f6f8fa;--panel:#ffffff;--line:#d1d9e0;--soft:#eff2f5;--ink:#1f2328;--muted:#59636e;--faint:#8c959f;
27
36
  --pass:#1a7f37;--fail:#cf222e;--run:#9a6700;--run-bg:#fff8c5;--accent:#0969da;--sel:#ddf4ff;
28
- --log:#1f2328;--log-ink:#d1d9e0;--log-muted:#8c959f;
37
+ --log:#1f2328;--log-ink:#d1d9e0;--log-muted:#8c959f;--top:56px;
29
38
  --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
30
39
  --sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;color-scheme:light}
31
40
  @media (prefers-color-scheme: dark){:root{--bg:#0d1117;--panel:#151b23;--line:#3d444d;--soft:#212830;--ink:#f0f6fc;
32
41
  --muted:#9198a1;--faint:#656c76;--pass:#3fb950;--fail:#f85149;--run:#d29922;--run-bg:#2c2513;--accent:#4493f8;
33
42
  --sel:#16263d;--log:#010409;--log-ink:#d1d7e0;--log-muted:#656c76;color-scheme:dark}}
34
43
  *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--sans)}
35
- button{font:inherit;color:inherit}
36
- .top{height:56px;display:flex;align-items:center;gap:10px;padding:0 24px;background:var(--panel);border-bottom:1px solid var(--line)}
37
- .top svg{width:22px;height:22px} .crumb{color:var(--accent)} .sep{color:var(--faint)}
44
+ button{font:inherit;color:inherit} a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline}
45
+ .top{position:sticky;top:0;z-index:20;height:var(--top);display:flex;align-items:center;gap:10px;padding:0 24px;
46
+ background:var(--panel);border-bottom:1px solid var(--line)}
47
+ .top svg{width:22px;height:22px;flex:none} .sep{color:var(--faint)}
48
+ .views{display:flex;gap:4px;margin-left:24px}
49
+ .views button{border:1px solid transparent;background:none;border-radius:6px;padding:4px 12px;cursor:pointer;color:var(--muted)}
50
+ .views button[aria-pressed="true"]{border-color:var(--line);background:var(--soft);color:var(--ink);font-weight:600}
38
51
  .pill{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
39
52
  .pill i{width:8px;height:8px;border-radius:50%;background:var(--faint)} .pill.on i{background:var(--pass)}
40
- .layout{display:flex;min-height:calc(100vh - 56px)}
41
- aside{width:300px;flex:none;background:var(--panel);border-right:1px solid var(--line);padding:16px 12px;overflow:auto}
42
- aside h3{margin:16px 10px 6px;font-size:12px;font-weight:600;color:var(--muted)}
43
- .job{display:flex;width:100%;align-items:center;gap:8px;padding:6px 10px;border:0;border-radius:6px;background:none;text-align:left;cursor:pointer}
53
+ .layout{display:flex;align-items:flex-start}
54
+ aside{position:sticky;top:var(--top);width:300px;flex:none;height:calc(100vh - var(--top));overflow:auto;
55
+ background:var(--panel);border-right:1px solid var(--line);padding:12px}
56
+ aside details{border-radius:6px} aside summary{list-style:none;display:flex;align-items:center;gap:8px;padding:7px 10px;
57
+ border-radius:6px;cursor:pointer;font-weight:600;font-size:13px}
58
+ aside summary::-webkit-details-marker{display:none} aside summary:hover{background:var(--soft)}
59
+ aside summary .chev{width:12px;height:12px;transition:transform .15s} aside details[open]>summary .chev{transform:rotate(90deg)}
60
+ aside summary .count{margin-left:auto;font-weight:400;color:var(--muted);font-size:12px;font-variant-numeric:tabular-nums}
61
+ .job{display:flex;width:100%;align-items:center;gap:8px;padding:5px 10px 5px 30px;border:0;border-radius:6px;background:none;text-align:left;cursor:pointer;font-size:13px}
44
62
  .job:hover{background:var(--soft)} .job.sel{background:var(--sel);font-weight:600}
45
- .job .name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
63
+ .job .name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .job .name>span{display:block;overflow:hidden;text-overflow:ellipsis}
46
64
  .job .d{font-size:12px;color:var(--muted);font-weight:400;font-variant-numeric:tabular-nums}
47
- .summary-btn{margin-bottom:4px}
48
- main{flex:1;min-width:0;padding:24px 28px;display:flex;flex-direction:column;gap:16px}
65
+ .summary-btn{padding-left:10px;margin-bottom:6px;font-weight:600}
66
+ .qsec{margin:10px 0 14px} .qsec h2{display:flex;align-items:center;gap:8px;margin:0 0 4px;padding:0 10px;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
67
+ .qsec h2 .count{margin-left:auto;font-weight:400;font-variant-numeric:tabular-nums} .qsec .job{padding-left:10px;align-items:flex-start}
68
+ .qsec .job .ic{margin-top:3px} .job small{display:block;font-size:11.5px;color:var(--muted);font-weight:400}
69
+ .qsec p{margin:2px 10px 6px;font-size:12.5px} .muted{color:var(--muted)}
70
+ .now{border:1px solid var(--run);border-radius:6px;padding:4px 0 8px;background:var(--panel)}
71
+ .kv{display:grid;grid-template-columns:62px minmax(0,1fr);gap:4px 10px;margin:4px 10px 0;font-size:12.5px} .kv dt{color:var(--muted)} .kv dd{margin:0;min-width:0;overflow-wrap:anywhere}
72
+ .ctx{display:flex;flex-direction:column;gap:3px} .ctx .d{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
73
+ details.prompt{margin-top:10px} details.prompt summary{cursor:pointer;color:var(--accent);font-size:13px}
74
+ details.prompt pre{max-height:420px;overflow:auto;background:var(--log);color:var(--log-ink);padding:12px;border-radius:6px;font:12px/1.55 var(--mono);white-space:pre-wrap}
75
+ main{flex:1;min-width:0;padding:0 28px 28px}
76
+ .stick{position:sticky;top:var(--top);z-index:10;background:var(--bg);padding:20px 0 12px;display:flex;flex-direction:column;gap:12px}
49
77
  .head{display:flex;align-items:center;gap:12px;flex-wrap:wrap} .head h1{margin:0;font-size:20px;font-weight:600}
50
78
  .badge{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
51
79
  .badge.running{background:var(--run-bg);color:var(--run)} .badge.pass{background:rgba(31,136,61,.12);color:var(--pass)}
52
80
  .badge.fail{background:rgba(207,34,46,.12);color:var(--fail)} .badge i{width:8px;height:8px;border-radius:50%;background:currentColor}
53
81
  .box{background:var(--panel);border:1px solid var(--line);border-radius:6px}
54
- #progress{display:flex;flex-wrap:wrap;gap:12px 40px;padding:14px 18px}
82
+ #progress{display:flex;flex-wrap:wrap;gap:8px 40px;padding:12px 18px}
55
83
  #progress div span{display:block;font-size:12px;color:var(--muted)} #progress div b{font-weight:400;font-variant-numeric:tabular-nums}
56
- .graph{padding:8px 4px} #chain{display:block;width:100%}
84
+ .bar{display:flex;height:6px;border-radius:999px;overflow:hidden;background:var(--soft)} .bar i{display:block}
85
+ .gap{display:flex;flex-direction:column;gap:16px}
86
+ .mm-row{display:grid;grid-template-columns:28px 150px minmax(0,1fr) 70px 90px;gap:12px;align-items:center;padding:11px 16px;border-bottom:1px solid var(--soft)}
87
+ .mm-row:last-of-type{border-bottom:0} .mm-head{font-size:12px;font-weight:600;color:var(--muted);border-bottom-color:var(--line)}
88
+ .mm-row b{font-weight:600} .mm-row .num{text-align:right;font-variant-numeric:tabular-nums} .mm-row .muted{color:var(--muted)}
89
+ .mm-row.has-fail{background:rgba(207,34,46,.05)} .mm-row.has-run{background:var(--run-bg)} .mm-row.queued{color:var(--muted)}
90
+ .mm-bar{display:flex;gap:3px}
91
+ .seg{flex:1;min-width:6px;height:16px;border:0;border-radius:3px;padding:0;cursor:pointer;background:var(--soft)}
92
+ .seg.pass{background:var(--pass)} .seg.fail{background:var(--fail)} .seg.queued{background:var(--soft);box-shadow:inset 0 0 0 1px var(--line)}
93
+ .seg.running{background:repeating-linear-gradient(135deg,#bf8700 0 5px,#d4a72c 5px 10px)}
94
+ .seg.blocked{background:none;box-shadow:inset 0 0 0 1.5px var(--faint)}
95
+ .seg.on{outline:2px solid var(--accent);outline-offset:2px} .seg:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
96
+ .mm-legend{display:flex;gap:16px;justify-content:flex-end;padding:10px 16px;font-size:12px;color:var(--muted);border-top:1px solid var(--line)}
97
+ .mm-legend span{display:inline-flex;align-items:center;gap:6px} .mm-legend .seg{width:14px;height:10px;flex:none;cursor:default}
57
98
  #focus{display:flex;flex-direction:column;overflow:hidden}
58
- #focus .fh{padding:12px 16px;border-bottom:1px solid var(--line);display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
99
+ #focus .fh{padding:12px 16px 0;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
59
100
  #focus .fh b{font-weight:600} #focus .fh span{font-size:12px;color:var(--muted)}
101
+ .tabs{display:flex;gap:2px;padding:8px 12px 0;border-bottom:1px solid var(--line);overflow-x:auto}
102
+ .tabs button{border:0;background:none;padding:8px 12px;cursor:pointer;color:var(--muted);border-bottom:2px solid transparent;white-space:nowrap}
103
+ .tabs button[aria-selected="true"]{color:var(--ink);font-weight:600;border-bottom-color:#fd8c73}
104
+ .pane{padding:0}
60
105
  #pipeline{list-style:none;margin:0;padding:0}
61
106
  #pipeline>li{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid var(--soft);font-size:13px}
62
- #pipeline>li:last-child{border-bottom:0} #pipeline .t{flex:1;min-width:0;overflow-wrap:anywhere}
63
- #pipeline .mono{font-family:var(--mono);font-size:12.5px} #pipeline .d{color:var(--muted);font-variant-numeric:tabular-nums}
64
- #pipeline li.running{background:var(--run-bg)} #pipeline li.running .d{color:var(--run)}
65
- #pipeline li.idle{color:var(--faint)} #pipeline li.logrow{display:block;padding:0;background:var(--log)}
66
- #feed{list-style:none;margin:0;padding:10px 16px 10px 42px;max-height:300px;overflow:auto;color:var(--log-ink);font:12px/1.65 var(--mono)}
67
- #feed li{display:grid;grid-template-columns:64px 56px minmax(0,1fr);gap:10px;animation:in .3s ease both}
68
- #feed .ts{color:var(--log-muted)} #feed .k{color:var(--log-muted)} #feed .ok{color:#3fb950} #feed .bad{color:#f85149}
69
- #feed .say{font-family:var(--sans);font-size:12.5px} #feed .x{overflow-wrap:anywhere}
107
+ #pipeline .t{flex:1;min-width:0;overflow-wrap:anywhere} #pipeline .mono{font-family:var(--mono);font-size:12.5px}
108
+ #pipeline .d{color:var(--muted);font-variant-numeric:tabular-nums}
109
+ #pipeline li.running{background:var(--run-bg)} #pipeline li.running .d{color:var(--run)} #pipeline li.idle{color:var(--faint)}
110
+ #pipeline li.logrow{display:block;padding:0;background:var(--log)}
111
+ .feed{list-style:none;margin:0;padding:10px 16px 10px 42px;max-height:340px;overflow:auto;color:var(--log-ink);font:12px/1.65 var(--mono)}
112
+ .feed li{display:grid;grid-template-columns:64px 56px minmax(0,1fr);gap:10px}
113
+ .feed .ts,.feed .k{color:var(--log-muted)} .feed .ok{color:#3fb950} .feed .bad{color:#f85149}
114
+ .feed .say{font-family:var(--sans);font-size:12.5px} .feed .x{overflow-wrap:anywhere}
115
+ .sect{padding:14px 16px;border-bottom:1px solid var(--soft)} .sect:last-child{border-bottom:0}
116
+ .sect h3{margin:0 0 8px;font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
117
+ .stmt{font-size:14.5px;line-height:1.55} .meta{display:flex;flex-wrap:wrap;gap:6px 18px;font-size:12.5px;color:var(--muted)}
118
+ .scn{border:1px solid var(--line);border-radius:6px;padding:10px 12px;margin-bottom:8px}
119
+ .scn b{font-weight:600} .scn ol{margin:6px 0 0;padding-left:18px;font-size:13px}
120
+ .scn .kw{font-weight:600;color:var(--accent)}
121
+ .list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px;font:12.5px var(--mono)}
122
+ .list li{display:flex;gap:10px;align-items:baseline} .add{color:var(--pass)} .rem{color:var(--fail)}
123
+ table{width:100%;border-collapse:collapse;font-size:12.5px}
124
+ th{text-align:left;font-weight:600;color:var(--muted);padding:7px 10px;border-bottom:1px solid var(--line);white-space:nowrap}
125
+ td{padding:7px 10px;border-bottom:1px solid var(--soft);vertical-align:top}
126
+ td.mono{font-family:var(--mono);white-space:nowrap} td.tests{font-family:var(--mono);font-size:11.5px;overflow-wrap:anywhere}
127
+ .st{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
128
+ .st.Implemented,.st.Verified,.st.Released{color:var(--pass)} .st.Draft{color:var(--muted)} .st.running{color:var(--run)}
129
+ .mtools{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
130
+ .mtools input{height:32px;padding:0 10px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--ink);min-width:260px}
131
+ .msum{font-size:12.5px;color:var(--muted)}
132
+ .mgroup>summary{list-style:none;display:flex;gap:10px;align-items:center;padding:10px 14px;cursor:pointer;font-weight:600}
133
+ .mgroup>summary::-webkit-details-marker{display:none} .mgroup>summary .count{font-weight:400;color:var(--muted);font-size:12.5px}
70
134
  .ic{flex:none;width:16px;height:16px;display:inline-block}
71
- .spin{animation:spin 1s linear infinite;transform-origin:50% 50%}
72
- @keyframes spin{to{transform:rotate(360deg)}} @keyframes in{from{opacity:0}to{opacity:1}}
73
- .empty{padding:48px 16px;text-align:center;color:var(--muted)}
74
- @media (max-width:900px){.layout{flex-direction:column} aside{width:auto;border-right:0;border-bottom:1px solid var(--line);max-height:280px}}
75
- @media (prefers-reduced-motion: reduce){*{animation:none!important}}
135
+ .spin{animation:spin 1s linear infinite;transform-origin:50% 50%} @keyframes spin{to{transform:rotate(360deg)}}
136
+ .empty{padding:40px 16px;text-align:center;color:var(--muted)}
137
+ [hidden]{display:none!important}
138
+ @media (max-width:900px){.layout{flex-direction:column} aside{position:static;width:auto;height:auto;max-height:300px;border-right:0;border-bottom:1px solid var(--line)} main{padding:0 14px 20px}}
139
+ @media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}}
76
140
  </style></head>
77
141
  <body>
78
142
  <div class="top">
79
143
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg>
80
- <span class="crumb">${title}</span><span class="sep">/</span><span class="crumb">Harness</span><span class="sep">/</span><b>Live</b>
144
+ <a href="#">${title}</a><span class="sep">/</span><b>Harness live</b>
145
+ <div class="views" role="group" aria-label="View">
146
+ <button type="button" id="vRun" aria-pressed="true">Run</button>
147
+ <button type="button" id="vMatrix" aria-pressed="false">Traceability</button>
148
+ </div>
81
149
  <span id="conn" class="pill"><i></i><span id="connText">connecting</span></span>
82
150
  </div>
83
151
  <div class="layout">
84
- <aside aria-label="Requirements of the run"><div id="jobs"></div></aside>
152
+ <aside aria-label="Work queue"><div id="jobs"></div></aside>
85
153
  <main>
86
- <div class="head"><span id="runBadge" class="badge running"><i></i><span id="runState">Waiting</span></span><h1 id="runTitle">Harness run</h1></div>
87
- <div id="progress" class="box" role="status"></div>
88
- <div class="box graph"><svg id="chain" role="img" aria-label="The requirements of the run as a graph of jobs"></svg></div>
89
- <section id="focus" class="box" aria-live="polite">
90
- <div class="fh"><b id="fName">—</b><span id="fMeta"></span></div>
91
- <ol id="pipeline" aria-label="Steps: Agent, Specification, Architecture, Tests, Merge"></ol>
154
+ <section id="runView">
155
+ <div class="stick">
156
+ <div class="head"><span id="runBadge" class="badge running"><i></i><span id="runState">Waiting</span></span><h1 id="runTitle">Harness run</h1></div>
157
+ <div id="progress" class="box" role="status"></div>
158
+ </div>
159
+ <div class="gap">
160
+ <div class="box" id="chain" role="group" aria-label="The run by module: one segment per requirement"></div>
161
+ <section id="focus" class="box" aria-live="polite">
162
+ <div class="fh"><b id="fName">—</b><span id="fMeta"></span></div>
163
+ <div class="tabs" role="tablist" aria-label="About this requirement">
164
+ <button type="button" role="tab" data-tab="steps" aria-selected="true">Steps</button>
165
+ <button type="button" role="tab" data-tab="req" aria-selected="false">Requirement</button>
166
+ <button type="button" role="tab" data-tab="files" aria-selected="false">Tests and files</button>
167
+ <button type="button" role="tab" data-tab="attempts" aria-selected="false">Attempts</button>
168
+ <button type="button" role="tab" data-tab="agent" aria-selected="false">Agent</button>
169
+ </div>
170
+ <div class="pane" data-pane="steps"><ol id="pipeline" aria-label="Steps: Agent, Specification, Architecture, Tests, Merge"></ol></div>
171
+ <div class="pane" data-pane="req" hidden></div>
172
+ <div class="pane" data-pane="files" hidden></div>
173
+ <div class="pane" data-pane="attempts" hidden></div>
174
+ <div class="pane" data-pane="agent" hidden></div>
175
+ </section>
176
+ </div>
177
+ </section>
178
+ <section id="matrixView" hidden>
179
+ <div class="stick">
180
+ <div class="head"><h1>Traceability matrix</h1><span class="msum" id="mSum"></span></div>
181
+ <div class="mtools"><input id="mFilter" type="search" placeholder="Filter by REQ, SCN, title, test or status" aria-label="Filter the matrix"/></div>
182
+ </div>
183
+ <div id="matrix" class="gap"></div>
92
184
  </section>
93
185
  </main>
94
186
  </div>
95
187
  <script>
96
188
  (function(){
97
189
  var NS = "http:" + "//www.w3.org/2000/svg";
98
- var model = {requirements:[]}, pinned = null, logKey = "";
190
+ var model = {requirements:[]}, projectDir = "", pinned = null, tab = "steps", stepsKey = "";
191
+ var expanded = {}, details = {}, matrix = [], matrixDirty = true, view = "run";
192
+ var open = load("sg-live-open") || {};
193
+ function load(k){ try { return JSON.parse(localStorage.getItem(k) || "null"); } catch (e) { return null; } }
194
+ function save(k, v){ try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
99
195
  function $(id){ return document.getElementById(id); }
100
196
  function el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; }
101
197
  function s(tag, attrs, parent){ var e = document.createElementNS(NS, tag); for (var k in attrs) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; }
102
198
  function now(){ return Date.now() / 1000; }
199
+ function two(n){ return (n < 10 ? "0" : "") + n; }
103
200
  function dur(sec){ sec = Math.max(0, Math.round(sec)); var h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), x = sec % 60;
104
201
  return h ? h + "h " + m + "m" : m ? m + "m " + x + "s" : x + "s"; }
105
- function two(n){ return (n < 10 ? "0" : "") + n; }
106
202
  function stamp(t){ if (!t) return ""; var d = new Date(t * 1000); return two(d.getHours()) + ":" + two(d.getMinutes()) + ":" + two(d.getSeconds()); }
107
203
  function elapsed(r){ return r.state === "running" ? Math.max(r.elapsed, now() - r.started) : r.elapsed; }
108
204
  function clip(t, n){ t = String(t || ""); return t.length > n ? t.slice(0, n - 1) + "…" : t; }
109
-
110
- /** A status icon: a check, a cross, a turning ring, or an empty circle. */
205
+ function cap(g){ return g ? g.charAt(0).toUpperCase() + g.slice(1) : "Requirements"; }
206
+ /** A link that opens the file in VS Code, at its line when known. */
207
+ function fileLink(rel, line){
208
+ var a = el("a", "", rel + (line ? ":" + line : ""));
209
+ if (projectDir) a.setAttribute("href", "vscode://file/" + (projectDir.split(String.fromCharCode(92)).join("/") + "/" + rel) + (line ? ":" + line : ""));
210
+ a.title = "Open in VS Code"; return a;
211
+ }
111
212
  function icon(state, size){
112
213
  var v = s("svg", {viewBox:"0 0 16 16", width:size || 16, height:size || 16, "class":"ic", "aria-hidden":"true"});
113
214
  if (state === "pass") { s("circle", {cx:8, cy:8, r:7, fill:"var(--pass)"}, v); s("path", {d:"M4.5 8.2l2.3 2.3 4.7-4.9", fill:"none", stroke:"#fff", "stroke-width":"1.8", "stroke-linecap":"round", "stroke-linejoin":"round"}, v); }
@@ -117,32 +218,104 @@ function livePage(projectName) {
117
218
  else s("circle", {cx:8, cy:8, r:6.25, fill:"none", stroke:"var(--faint)", "stroke-width":"1.5"}, v);
118
219
  return v;
119
220
  }
120
- function selected(reqs){
121
- if (pinned) { var p = reqs.filter(function(r){ return r.req === pinned; })[0]; if (p) return p; }
122
- return reqs.filter(function(r){ return r.state === "running"; })[0] || reqs[0] || null;
221
+ function selected(list){
222
+ if (pinned) { var p = list.filter(function(r){ return r.req === pinned; })[0]; if (p) return p;
223
+ var m = matrix.filter(function(x){ return x.requirement === pinned; })[0];
224
+ if (m) return {req:m.requirement, title:m.title, group:m.group, state:"queued", attempt:0, steps:[], started:now(), updated:now(), elapsed:0, agent:null, usage:null, prompt:null}; }
225
+ return list.filter(function(r){ return r.state === "running"; })[0] || list[0] || null;
123
226
  }
124
227
  function ordered(reqs){ return reqs.slice().sort(function(a, b){ return a.started - b.started; }); }
125
228
  function groups(list){
126
229
  var out = [], at = {};
127
230
  list.forEach(function(r){ var g = r.group || ""; if (!(g in at)) { at[g] = out.length; out.push({name:g, reqs:[]}); } out[at[g]].reqs.push(r); });
231
+ out.forEach(function(g){ g.pass = g.reqs.filter(function(r){ return r.state === "pass"; }).length;
232
+ g.fail = g.reqs.filter(function(r){ return r.state === "fail"; }).length; g.done = g.pass === g.reqs.length;
233
+ g.time = g.reqs.reduce(function(a, r){ return a + elapsed(r); }, 0); });
128
234
  return out;
129
235
  }
130
- function pick(id){ pinned = pinned === id ? null : id; logKey = ""; draw(); }
236
+ function pick(id){ pinned = pinned === id ? null : id; stepsKey = ""; draw(); }
237
+
238
+ function kt(n){ n = Number(n) || 0; return n >= 1e6 ? (n / 1e6).toFixed(2) + "M" : n >= 1e4 ? Math.round(n / 1e3) + "k" : n >= 1e3 ? (n / 1e3).toFixed(1) + "k" : String(n); }
239
+ function usd(n){ return n === null || n === undefined ? "—" : "$" + Number(n).toFixed(2); }
240
+ function shortModel(m){ return String(m || "").replace(/^claude-/, ""); }
241
+ /** How full the context is, as a bar: green, amber past 60%, red past 85%. */
242
+ function ctxBar(u){
243
+ var w = el("div", "ctx"), pct = u && u.window ? Math.min(100, u.context / u.window * 100) : 0;
244
+ var bar = el("div", "bar"), i = el("i"); i.style.width = pct + "%";
245
+ i.style.background = pct > 85 ? "var(--fail)" : pct > 60 ? "var(--run)" : "var(--pass)"; bar.appendChild(i);
246
+ w.append(bar, el("span", "d", u && u.window ? kt(u.context) + " of " + kt(u.window) + " · " + Math.round(pct) + "%" : "—"));
247
+ return w;
248
+ }
249
+ function jobBtn(r, sel, note, state){
250
+ var b = el("button", "job" + (sel && sel.req === r.req ? " sel" : "")); b.type = "button";
251
+ b.setAttribute("aria-current", sel && sel.req === r.req ? "true" : "false");
252
+ var txt = el("span", "name"); txt.append(el("span", "", r.req + " " + (r.title || "")));
253
+ if (note) txt.appendChild(el("small", "", note));
254
+ b.append(icon(state || r.state), txt);
255
+ b.addEventListener("click", function(){ pick(r.req); });
256
+ return b;
257
+ }
258
+ function block(title, count){
259
+ var d = el("section", "qsec"), h = el("h2"); h.append(el("span", "", title));
260
+ if (count !== undefined) h.appendChild(el("span", "count", String(count)));
261
+ d.appendChild(h); return d;
262
+ }
131
263
 
264
+ /**
265
+ * The sidebar is the run's work queue (REQ-182), not a second map of it: what
266
+ * runs now and with which model and context, what needs a person, what comes
267
+ * next, and what finished last. The map is the matrix in the centre.
268
+ */
132
269
  function sidebar(list, sel){
133
- var box = $("jobs"); box.replaceChildren();
270
+ var box = $("jobs"), keepScroll = box.parentNode.scrollTop; box.replaceChildren();
134
271
  var sum = el("button", "job summary-btn" + (pinned ? "" : " sel"), "Summary"); sum.type = "button";
135
- sum.addEventListener("click", function(){ pinned = null; logKey = ""; draw(); }); box.appendChild(sum);
136
- groups(list).forEach(function(g){
137
- box.appendChild(el("h3", "", g.name ? g.name.charAt(0).toUpperCase() + g.name.slice(1) : "Requirements"));
138
- g.reqs.forEach(function(r){
139
- var b = el("button", "job" + (sel && sel.req === r.req ? " sel" : "")); b.type = "button";
140
- b.setAttribute("aria-current", sel && sel.req === r.req ? "true" : "false");
141
- b.append(icon(r.state), el("span", "name", r.req + " " + (r.title || "")), el("span", "d", dur(elapsed(r))));
142
- b.addEventListener("click", function(){ pick(r.req); });
143
- box.appendChild(b);
144
- });
145
- });
272
+ sum.addEventListener("click", function(){ pinned = null; stepsKey = ""; draw(); }); box.appendChild(sum);
273
+ var since = model.queueStarted || 0, byReq = {}, titles = {};
274
+ list.forEach(function(r){ byReq[r.req] = r; });
275
+ matrix.forEach(function(m){ titles[m.requirement] = m.title; });
276
+
277
+ var cur = list.filter(function(r){ return r.state === "running"; })[0];
278
+ var now0 = block("Now");
279
+ if (cur) {
280
+ var card = el("div", "now"); card.appendChild(jobBtn(cur, sel, "attempt " + cur.attempt + " · " + cur.phase + " · " + dur(elapsed(cur))));
281
+ var kv = el("dl", "kv");
282
+ var add = function(k, v){ var dd = el("dd"); if (typeof v === "string") dd.textContent = v; else dd.appendChild(v); kv.append(el("dt", "", k), dd); };
283
+ add("Model", cur.agent ? shortModel(cur.agent.model) : "not reported");
284
+ add("Context", ctxBar(cur.usage));
285
+ add("Output", cur.usage ? kt(cur.usage.output) + " tokens" : "—");
286
+ if (cur.prompt) add("Prompt", "~" + kt(cur.prompt.tokens) + " tokens · " + cur.prompt.lines + " lines");
287
+ card.appendChild(kv); now0.appendChild(card);
288
+ } else now0.appendChild(el("p", "muted", "Nothing running."));
289
+ box.appendChild(now0);
290
+
291
+ var need = list.filter(function(r){ return r.state === "fail"; }).map(function(r){
292
+ var g = r.steps.filter(function(e){ return e.type === "gate" && e.result !== "pass"; })[0];
293
+ return {r:r, note:"failed · attempt " + r.attempt + (g ? " · " + clip(String(g.stage).split(":")[0], 40) : ""), st:"fail"}; });
294
+ var seenB = {};
295
+ matrix.forEach(function(m){ if (m.status === "Needs Clarification" && !byReq[m.requirement] && !seenB[m.requirement]) { seenB[m.requirement] = 1;
296
+ need.push({r:{req:m.requirement, title:m.title, state:"idle"}, note:"blocked · needs clarification", st:"idle"}); } });
297
+ var n1 = block("Needs attention", need.length);
298
+ need.forEach(function(x){ n1.appendChild(jobBtn(x.r, sel, x.note, x.st)); });
299
+ if (!need.length) n1.appendChild(el("p", "muted", "Nothing failed or blocked."));
300
+ box.appendChild(n1);
301
+
302
+ var done = list.filter(function(r){ return r.state === "pass" && r.started >= since; });
303
+ var next = (model.queue || []).filter(function(id){ var r = byReq[id]; return !(r && r.started >= since); });
304
+ var times = done.map(function(r){ return r.elapsed; }).sort(function(a, b){ return a - b; });
305
+ var median = times.length ? times[Math.floor(times.length / 2)] : 0;
306
+ var n2 = block("Up next", next.length);
307
+ if (next.length && median) n2.appendChild(el("p", "muted", "about " + dur(next.length * median) + " at " + dur(median) + " each"));
308
+ next.slice(0, 6).forEach(function(id){ n2.appendChild(jobBtn({req:id, title:titles[id] || "", state:"idle"}, sel, "", "idle")); });
309
+ if (next.length > 6) n2.appendChild(el("p", "muted", "+ " + (next.length - 6) + " more"));
310
+ if (!next.length) n2.appendChild(el("p", "muted", (model.queue || []).length ? "The queue is done." : "No queue recorded for this run."));
311
+ box.appendChild(n2);
312
+
313
+ var recent = list.filter(function(r){ return r.state === "pass"; }).sort(function(a, b){ return b.updated - a.updated; }).slice(0, 5);
314
+ var n3 = block("Finished recently");
315
+ recent.forEach(function(r){ n3.appendChild(jobBtn(r, sel, dur(r.elapsed) + (r.usage && r.usage.cost !== null ? " · " + usd(r.usage.cost) : ""), "pass")); });
316
+ if (!recent.length) n3.appendChild(el("p", "muted", "None yet."));
317
+ box.appendChild(n3);
318
+ box.parentNode.scrollTop = keepScroll;
146
319
  }
147
320
 
148
321
  function summary(list){
@@ -152,65 +325,76 @@ function livePage(projectName) {
152
325
  var state = run ? "running" : f ? "fail" : list.length ? "pass" : "running";
153
326
  $("runBadge").className = "badge " + state;
154
327
  $("runState").textContent = run ? "In progress" : f ? "Failed" : list.length ? "Completed" : "Waiting";
155
- var mods = groups(list).map(function(g){ return g.name; }).filter(Boolean);
156
- $("runTitle").textContent = mods.length ? mods.map(function(m){ return m.charAt(0).toUpperCase() + m.slice(1); }).join(", ") : "Harness run";
328
+ var cur = list.filter(function(r){ return r.state === "running"; })[0];
329
+ $("runTitle").textContent = cur ? cap(cur.group) + " · " + cur.req + " " + (cur.title || "") : groups(list).map(function(g){ return cap(g.name); }).join(", ") || "Harness run";
157
330
  var box = $("progress"); box.replaceChildren();
158
331
  [["Started", first ? stamp(first) : "—"], ["Duration", first ? dur(last - first) : "—"],
159
332
  ["Requirements", p + " passed · " + run + " running · " + f + " failed"],
160
- ["Agent time", dur(list.reduce(function(a, r){ return a + elapsed(r); }, 0))]].forEach(function(x){
333
+ ["Agent time", dur(list.reduce(function(a, r){ return a + elapsed(r); }, 0))]].concat(model.totals && (model.totals.input || model.totals.output) ? [
334
+ ["Tokens", kt(model.totals.input + model.totals.cacheRead + model.totals.cacheWrite) + " in · " + kt(model.totals.output) + " out"],
335
+ ["Cost", usd(model.totals.cost)]] : []).forEach(function(x){
161
336
  var d = el("div"); d.append(el("span", "", x[0]), el("b", "", x[1])); box.appendChild(d);
162
337
  });
338
+ var bar = el("div", "bar"); bar.style.flexBasis = "100%"; var total = Math.max(1, list.length);
339
+ [["var(--pass)", p], ["var(--run)", run], ["var(--fail)", f]].forEach(function(x){ var i = el("i"); i.style.width = (x[1] / total * 100) + "%"; i.style.background = x[0]; bar.appendChild(i); });
340
+ box.appendChild(bar);
163
341
  }
164
342
 
165
- /** The graph of jobs: one row per module, boxes joined in the order they ran, a bend between rows. */
166
- function graph(list, sel){
167
- var svg = $("chain"), W = Math.max(svg.clientWidth || 900, 360), bw = 168, bh = 40, gx = 24, gy = 34, padX = 18, padY = 30;
168
- var perRow = Math.max(1, Math.floor((W - padX * 2 + gx) / (bw + gx)));
169
- var rows = [];
170
- groups(list).forEach(function(g){ for (var i = 0; i < g.reqs.length; i += perRow) rows.push({name: i ? "" : g.name, reqs: g.reqs.slice(i, i + perRow)}); });
171
- var H = Math.max(80, padY + rows.length * (bh + gy) + 8);
172
- svg.setAttribute("viewBox", "0 0 " + W + " " + H); svg.setAttribute("height", String(H)); svg.replaceChildren();
173
- if (!list.length) { var t = s("text", {x:W / 2, y:H / 2, "text-anchor":"middle", fill:"var(--muted)", "font-size":"13"}, svg); t.textContent = "No harness events yet. Run specgate harness run: each requirement appears here as it starts."; return; }
174
- var pos = {}, prev = null;
175
- rows.forEach(function(row, ri){
176
- var y = padY + ri * (bh + gy);
177
- if (row.name) { var l = s("text", {x:padX, y:y - 10, "font-size":"12", "font-weight":"600", fill:"var(--muted)"}, svg); l.textContent = row.name.charAt(0).toUpperCase() + row.name.slice(1); }
178
- row.reqs.forEach(function(r, i){ pos[r.req] = {x:padX + i * (bw + gx), y:y}; });
179
- });
180
- list.forEach(function(r){
181
- var a = prev && pos[prev.req], b = pos[r.req];
182
- if (a) {
183
- var c = r.state === "running" ? "var(--run)" : prev.state === "pass" ? "var(--faint)" : "var(--line)";
184
- if (a.y === b.y) s("line", {x1:a.x + bw, y1:a.y + bh / 2, x2:b.x, y2:b.y + bh / 2, stroke:c, "stroke-width":"1.5"}, svg);
185
- else s("path", {d:"M" + (a.x + bw) + " " + (a.y + bh / 2) + " h" + (gx / 2) + " V" + (a.y + bh + gy / 2) + " H" + (b.x - gx / 2) + " V" + (b.y + bh / 2) + " H" + b.x,
186
- fill:"none", stroke:c, "stroke-width":"1.5"}, svg);
187
- }
188
- prev = r;
189
- });
190
- list.forEach(function(r){
191
- var p = pos[r.req], on = sel && sel.req === r.req;
192
- var g = s("g", {transform:"translate(" + p.x + "," + p.y + ")", tabindex:"0", role:"button", style:"cursor:pointer",
193
- "aria-label": r.req + " " + (r.title || "") + ", " + r.state}, svg);
194
- s("rect", {width:bw, height:bh, rx:6, fill: on ? "var(--sel)" : "var(--panel)", stroke: on ? "var(--accent)" : "var(--line)", "stroke-width": on ? 2 : 1}, g);
195
- var ic = icon(r.state, 16); ic.setAttribute("x", "12"); ic.setAttribute("y", String(bh / 2 - 8)); g.appendChild(ic);
196
- var n = s("text", {x:36, y:bh / 2 + 4.5, "font-size":"13", "font-weight": on ? "600" : "400", fill:"var(--ink)"}, g); n.textContent = r.req;
197
- var d = s("text", {x:bw - 12, y:bh / 2 + 4.5, "text-anchor":"end", "font-size":"12", fill:"var(--muted)"}, g); d.textContent = dur(elapsed(r));
198
- var tip = s("title", {}, g); tip.textContent = r.req + " — " + (r.title || "");
199
- g.addEventListener("click", function(){ pick(r.req); });
200
- g.addEventListener("keydown", function(ev){ if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); pick(r.req); } });
343
+ /**
344
+ * The run as a matrix of modules (REQ-181): one row per module, a segment
345
+ * per requirement in id order, coloured by what happened to it in this run,
346
+ * or by its status when the run has not reached it; the passed count and
347
+ * the agent time. A segment opens its requirement below.
348
+ */
349
+ function modules(list, sel){
350
+ var box = $("chain"); box.replaceChildren();
351
+ var byReq = {}; list.forEach(function(r){ byReq[r.req] = r; });
352
+ var reqs = [], seen = {};
353
+ matrix.forEach(function(m){ if (seen[m.requirement] || m.requirement === "REQ-001") return; seen[m.requirement] = 1;
354
+ reqs.push({req:m.requirement, title:m.title, group:m.group, status:m.status}); });
355
+ list.forEach(function(r){ if (!seen[r.req]) { seen[r.req] = 1; reqs.push({req:r.req, title:r.title, group:r.group, status:"Draft"}); } });
356
+ if (!reqs.length) { box.appendChild(el("div", "empty", "No harness events yet. Run specgate harness run: each requirement appears here as it starts.")); return; }
357
+ var order = [], rowsBy = {};
358
+ reqs.sort(function(x, y){ return x.req.localeCompare(y.req); }).forEach(function(q){
359
+ var g = q.group || "spec"; if (!rowsBy[g]) { rowsBy[g] = []; order.push(g); } rowsBy[g].push(q); });
360
+ var rank = function(g){ var rs = rowsBy[g], ev = rs.map(function(q){ return byReq[q.req]; }).filter(Boolean);
361
+ return ev.length ? Math.min.apply(null, ev.map(function(r){ return r.started; })) : Infinity; };
362
+ order.sort(function(x, y){ return rank(x) - rank(y); });
363
+ var head = el("div", "mm-row mm-head");
364
+ ["", "Module", "Requirements", "Passed", "Time"].forEach(function(h, i){ var c = el("span", i >= 3 ? "num" : "", h); head.appendChild(c); });
365
+ box.appendChild(head);
366
+ order.forEach(function(g){
367
+ var rs = rowsBy[g], pass = 0, fail = 0, run = 0, time = 0;
368
+ var row = el("div", "mm-row"), bar = el("span", "mm-bar");
369
+ rs.forEach(function(q){
370
+ var ev = byReq[q.req], st = ev ? ev.state : (q.status === "Implemented" || q.status === "Verified" || q.status === "Released") ? "pass"
371
+ : q.status === "Needs Clarification" ? "blocked" : "queued";
372
+ if (st === "pass") pass++; else if (st === "fail") fail++; else if (st === "running") run++;
373
+ if (ev) time += elapsed(ev);
374
+ var seg = el("button", "seg " + st + (sel && sel.req === q.req ? " on" : "")); seg.type = "button";
375
+ seg.title = q.req + " — " + (q.title || "") + " · " + (ev ? st + " · " + dur(elapsed(ev)) : st);
376
+ seg.setAttribute("aria-label", seg.title);
377
+ seg.addEventListener("click", function(){ pick(q.req); });
378
+ bar.appendChild(seg);
379
+ });
380
+ var state = fail ? "fail" : run ? "running" : pass === rs.length ? "pass" : "idle";
381
+ if (fail) row.classList.add("has-fail"); else if (run) row.classList.add("has-run"); else if (state === "idle" && !pass) row.classList.add("queued");
382
+ row.append(icon(state), el("b", "", cap(g === "spec" ? "" : g)), bar, el("span", "num", pass + "/" + rs.length), el("span", "num muted", time ? dur(time) : pass === rs.length ? "earlier" : "queued"));
383
+ box.appendChild(row);
201
384
  });
385
+ var legend = el("div", "mm-legend");
386
+ [["pass", "passed"], ["running", "running"], ["fail", "failed"], ["blocked", "blocked"], ["queued", "queued"]].forEach(function(x){
387
+ var s0 = el("span"), sw = el("i", "seg " + x[0]); s0.append(sw, document.createTextNode(x[1])); legend.appendChild(s0); });
388
+ box.appendChild(legend);
202
389
  }
203
390
 
204
- /** The selected requirement's steps: the agent, then each stage of the gate as it ran, the rest still to come. */
205
391
  function steps(r){
206
392
  var ol = $("pipeline");
207
- if (!r) { $("fName").textContent = "—"; $("fMeta").textContent = ""; ol.replaceChildren(el("li", "empty", "Each requirement opens here as the harness works on it.")); return; }
208
- $("fName").textContent = r.req + " · " + (r.title || "");
209
- $("fMeta").textContent = "attempt " + r.attempt + (r.group ? " · " + r.group : "") + " · harness/" + r.req;
393
+ if (!r) { ol.replaceChildren(el("li", "empty", "Each requirement opens here as the harness works on it.")); return; }
210
394
  var events = r.steps.slice().reverse(), firstGate = events.filter(function(e){ return e.type === "gate"; })[0];
211
395
  var rows = [], seen = {Specification:false, Architecture:false, Tests:false, Merge:false};
212
396
  var agentEnd = firstGate ? Number(firstGate.t) : (r.state === "running" ? now() : r.updated);
213
- rows.push({name:"Agent", mono:false, state: firstGate ? "pass" : r.state === "running" ? "running" : r.state, d: dur(agentEnd - r.started), log: !firstGate});
397
+ rows.push({name:"Agent", state: firstGate ? "pass" : r.state === "running" ? "running" : r.state, d: dur(agentEnd - r.started), log: !firstGate});
214
398
  var prevT = agentEnd;
215
399
  events.forEach(function(e){
216
400
  if (e.type !== "gate" && e.type !== "merge") return;
@@ -223,9 +407,9 @@ function livePage(projectName) {
223
407
  var lastGate = events.filter(function(e){ return e.type === "gate"; }).slice(-1)[0];
224
408
  if (r.state === "running" && firstGate && !(lastGate && lastGate.result !== "pass")) {
225
409
  var next = ["Specification","Architecture","Tests","Merge"].filter(function(k){ return !seen[k]; })[0];
226
- if (next) { rows.push({name: next, mono:false, state:"running", d: dur(now() - prevT), log:true}); seen[next] = true; }
410
+ if (next) { rows.push({name:next, state:"running", d: dur(now() - prevT), log:true}); seen[next] = true; }
227
411
  }
228
- ["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k, mono:false, state:"idle", d:""}); });
412
+ ["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k, state:"idle", d:""}); });
229
413
  ol.replaceChildren();
230
414
  rows.forEach(function(x){
231
415
  var li = el("li", x.state === "running" ? "running" : x.state === "idle" ? "idle" : "");
@@ -236,14 +420,14 @@ function livePage(projectName) {
236
420
  if (!rows.some(function(x){ return x.log; }) && r.steps.length) { var lr2 = el("li", "logrow"); lr2.appendChild(log(r)); ol.appendChild(lr2); }
237
421
  }
238
422
  function log(r){
239
- var ul = el("ol"); ul.id = "feed";
423
+ var ul = el("ol", "feed"); ul.id = "feed";
240
424
  r.steps.slice(0, 40).slice().reverse().forEach(function(e){
241
425
  var li = el("li"), t = Number(e.ended || e.t) || 0, kind = e.type === "step" ? String(e.kind || "") : String(e.type || "");
242
426
  var what = e.type === "message" ? e.text : e.type === "gate" ? (e.result === "pass" ? "passed " : "failed ") + e.stage
243
427
  : e.type === "merge" ? "merged into " + (e.into || "HEAD") : (e.target || e.tool || "");
244
428
  if (e.added !== undefined || e.removed !== undefined) what += " +" + (Number(e.added) || 0) + " −" + (Number(e.removed) || 0);
245
- var x = el("span", "x" + (e.type === "message" ? " say" : e.type === "gate" ? (e.result === "pass" ? " ok" : " bad") : ""), clip(what, 400));
246
- li.append(el("span", "ts", stamp(t)), el("span", "k", e.type === "message" ? "agent" : kind), x);
429
+ li.append(el("span", "ts", stamp(t)), el("span", "k", e.type === "message" ? "agent" : kind),
430
+ el("span", "x" + (e.type === "message" ? " say" : e.type === "gate" ? (e.result === "pass" ? " ok" : " bad") : ""), clip(what, 400)));
247
431
  ul.appendChild(li);
248
432
  });
249
433
  if (!ul.childNodes.length) ul.appendChild(el("li", "", "The agent's steps appear here as it works."));
@@ -251,27 +435,185 @@ function livePage(projectName) {
251
435
  return ul;
252
436
  }
253
437
 
438
+ /** The requirement's details, fetched once per change of its events. */
439
+ function detail(r, then){
440
+ var key = r.req + ":" + r.steps.length + ":" + r.state;
441
+ if (details[r.req] && details[r.req].key === key) return then(details[r.req].d);
442
+ fetch("live/req/" + encodeURIComponent(r.req) + ".json").then(function(x){ return x.json(); })
443
+ .then(function(d){ details[r.req] = {key:key, d:d}; then(d); }).catch(function(){});
444
+ }
445
+ function sect(title){ var d = el("div", "sect"); d.appendChild(el("h3", "", title)); return d; }
446
+ function paneReq(d){
447
+ var p = document.querySelector('[data-pane="req"]'); p.replaceChildren();
448
+ var a = sect("Requirement");
449
+ a.appendChild(el("div", "stmt", d.statement || "No requirement text found in the specification."));
450
+ var m = el("div", "meta"); m.style.marginTop = "8px";
451
+ m.append(el("span", "", "Status: " + d.status), el("span", "", "Module: " + (d.group || "—")));
452
+ if (d.source) { var sp = el("span", "", "Spec: "); sp.appendChild(fileLink(d.source.file, d.source.line)); m.appendChild(sp); }
453
+ a.appendChild(m); p.appendChild(a);
454
+ var b = sect("Scenarios · " + d.scenarios.length);
455
+ d.scenarios.forEach(function(sc){
456
+ var box = el("div", "scn"), h = el("div"); h.append(el("b", "", sc.id), document.createTextNode(" — " + sc.title)); box.appendChild(h);
457
+ var ol = el("ol"); sc.steps.forEach(function(st){ var li = el("li"), sp2 = st.indexOf(" "), kw = sp2 > 0 ? st.slice(0, sp2) : st;
458
+ li.append(el("span", "kw", kw), document.createTextNode(sp2 > 0 ? st.slice(sp2) : "")); ol.appendChild(li); });
459
+ box.appendChild(ol); b.appendChild(box);
460
+ });
461
+ if (!d.scenarios.length) b.appendChild(el("div", "meta", "No scenario."));
462
+ p.appendChild(b);
463
+ if (d.assumptions.length) {
464
+ var c = sect("Assumptions · " + d.assumptions.length), t = el("table"), hr = el("tr");
465
+ ["#", "Assumption", "Status"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
466
+ d.assumptions.forEach(function(x){ var tr = el("tr"); tr.append(el("td", "mono", x.id), el("td", "", x.text), el("td", "", x.status)); t.appendChild(tr); });
467
+ c.appendChild(t); p.appendChild(c);
468
+ }
469
+ }
470
+ function paneFiles(d){
471
+ var p = document.querySelector('[data-pane="files"]'); p.replaceChildren();
472
+ var a = sect("Tests that name it · " + d.tests.length), ul = el("ul", "list");
473
+ d.tests.forEach(function(f){ var li = el("li"); li.appendChild(fileLink(f)); ul.appendChild(li); });
474
+ if (!d.tests.length) ul.appendChild(el("li", "", "No test names it yet."));
475
+ a.appendChild(ul); p.appendChild(a);
476
+ var b = sect("Feature"), ul2 = el("ul", "list");
477
+ d.features.forEach(function(f){ var li = el("li"); li.appendChild(fileLink(f)); ul2.appendChild(li); });
478
+ if (!d.features.length) ul2.appendChild(el("li", "", "Written when its change is archived."));
479
+ b.appendChild(ul2); p.appendChild(b);
480
+ var c = sect("Files the agent changed (last attempt) · " + d.files.length), ul3 = el("ul", "list");
481
+ d.files.forEach(function(f){ var li = el("li"); li.append(fileLink(f.path), el("span", "add", "+" + f.added), el("span", "rem", "−" + f.removed)); ul3.appendChild(li); });
482
+ if (!d.files.length) ul3.appendChild(el("li", "", "No file changed yet."));
483
+ c.appendChild(ul3); p.appendChild(c);
484
+ }
485
+ function paneAttempts(d){
486
+ var p = document.querySelector('[data-pane="attempts"]'); p.replaceChildren();
487
+ var a = sect("Attempts · " + d.attempts.length + " · branch harness/" + d.id);
488
+ var t = el("table"), hr = el("tr"); ["#", "Result", "Started", "Duration", "Failed at", "Gate"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
489
+ d.attempts.forEach(function(x){
490
+ var tr = el("tr"), res = el("td"), st = el("span", "st");
491
+ st.append(icon(x.result), document.createTextNode(x.result)); res.appendChild(st);
492
+ tr.append(el("td", "mono", String(x.attempt)), res, el("td", "mono", stamp(x.started)), el("td", "", dur(x.duration)),
493
+ el("td", "", x.failedAt || "—"), el("td", "tests", x.gates.map(function(g){ return (g.result === "pass" ? "✓ " : "✗ ") + g.stage; }).join(" · ")));
494
+ t.appendChild(tr);
495
+ });
496
+ if (!d.attempts.length) { var tr0 = el("tr"), td0 = el("td", "", "No attempt recorded."); td0.colSpan = 6; tr0.appendChild(td0); t.appendChild(tr0); }
497
+ a.appendChild(t); p.appendChild(a);
498
+ }
499
+ /** The agent's session for the selected attempt: model, context, tokens, cost and the prompt it was given (REQ-182). */
500
+ function paneAgent(r){
501
+ var p = document.querySelector('[data-pane="agent"]'); p.replaceChildren();
502
+ if (!r.agent && !r.usage && !r.prompt) {
503
+ p.appendChild(el("div", "empty", "No session reported for this attempt. Agents piped through specgate harness events --from claude report their model, tokens and prompt."));
504
+ return;
505
+ }
506
+ var a = sect("Session"), m = el("div", "meta");
507
+ m.append(el("span", "", "Model: " + (r.agent ? r.agent.model || "—" : "—")));
508
+ if (r.agent && r.agent.version) m.appendChild(el("span", "", "Claude Code " + r.agent.version));
509
+ if (r.agent && r.agent.permission) m.appendChild(el("span", "", "Permissions: " + r.agent.permission));
510
+ if (r.agent && r.agent.tools) m.appendChild(el("span", "", r.agent.tools + " tools"));
511
+ if (r.usage && r.usage.turns !== null) m.appendChild(el("span", "", r.usage.turns + " turns"));
512
+ a.appendChild(m); p.appendChild(a);
513
+ if (r.usage) {
514
+ var c = sect("Context window"); c.appendChild(ctxBar(r.usage));
515
+ c.appendChild(el("div", "meta", "Peak " + kt(r.usage.peak) + (r.usage.window ? " · " + Math.round(r.usage.peak / r.usage.window * 100) + "% of the window" : "")));
516
+ p.appendChild(c);
517
+ var t = sect("Tokens"), tb = el("table"), hr = el("tr");
518
+ ["Input", "Cache read", "Cache write", "Output", "Cost"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); tb.appendChild(hr);
519
+ var tr = el("tr"), fin = r.usage.input || r.usage.cacheRead || r.usage.cacheWrite;
520
+ [fin ? kt(r.usage.input) : "—", fin ? kt(r.usage.cacheRead) : "—", fin ? kt(r.usage.cacheWrite) : "—", kt(r.usage.output), usd(r.usage.cost)].forEach(function(x){ tr.appendChild(el("td", "mono", x)); });
521
+ tb.appendChild(tr); t.appendChild(tb);
522
+ if (!fin) t.appendChild(el("div", "meta", "Totals and cost arrive when the session ends."));
523
+ p.appendChild(t);
524
+ }
525
+ if (r.prompt) {
526
+ var q = sect("Prompt"), qm = el("div", "meta");
527
+ qm.append(el("span", "", "For: " + r.prompt.role), el("span", "", r.prompt.lines + " lines"), el("span", "", kt(r.prompt.chars) + " characters"), el("span", "", "~" + kt(r.prompt.tokens) + " tokens (estimated)"));
528
+ var url = "live/req/" + encodeURIComponent(r.req) + "/prompt/" + r.attempt + ".txt", open1 = el("a", "", "Open in a new tab"); open1.href = url; open1.target = "_blank"; open1.rel = "noopener";
529
+ qm.appendChild(open1); q.appendChild(qm);
530
+ var dt = el("details", "prompt"), ds = el("summary", "", "Show the prompt"), pre = el("pre");
531
+ dt.append(ds, pre);
532
+ dt.addEventListener("toggle", function(){ if (dt.open && !pre.textContent) fetch(url).then(function(x){ return x.text(); }).then(function(x){ pre.textContent = x; }).catch(function(){ pre.textContent = "Not kept."; }); });
533
+ q.appendChild(dt); p.appendChild(q);
534
+ }
535
+ }
536
+ function focus(sel){
537
+ $("fName").textContent = sel ? sel.req + " · " + (sel.title || "") : "—";
538
+ $("fMeta").textContent = sel ? "attempt " + sel.attempt + (sel.group ? " · " + sel.group : "") + " · harness/" + sel.req + (sel.agent ? " · " + shortModel(sel.agent.model) : "") : "";
539
+ Array.prototype.forEach.call(document.querySelectorAll(".tabs button"), function(b){ b.setAttribute("aria-selected", String(b.dataset.tab === tab)); });
540
+ Array.prototype.forEach.call(document.querySelectorAll("[data-pane]"), function(p){ p.hidden = p.dataset.pane !== tab; });
541
+ if (!sel) return steps(null);
542
+ var key = sel.req + ":" + sel.attempt + ":" + sel.steps.length + ":" + sel.state + ":" + tab + ":" + (sel.usage ? sel.usage.context + "/" + sel.usage.output : "");
543
+ if (key === stepsKey) return;
544
+ stepsKey = key;
545
+ if (tab === "steps") steps(sel);
546
+ else if (tab === "agent") paneAgent(sel);
547
+ else detail(sel, function(d){ if (tab === "req") paneReq(d); else if (tab === "files") paneFiles(d); else paneAttempts(d); });
548
+ }
549
+
550
+ /** The traceability matrix, one accordion per module, filtered as the viewer types. */
551
+ function drawMatrix(){
552
+ var q = String($("mFilter").value || "").toLowerCase(), box = $("matrix"); box.replaceChildren();
553
+ var rows = matrix.filter(function(r){ return !q || [r.requirement, r.scenario, r.title, r.status, r.group, r.feature].concat(r.tests).join(" ").toLowerCase().indexOf(q) >= 0; });
554
+ var reqs = {}, impl = {}; matrix.forEach(function(r){ reqs[r.requirement] = 1; if (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") impl[r.requirement] = 1; });
555
+ var tested = matrix.filter(function(r){ return r.tests.length; }).length;
556
+ $("mSum").textContent = Object.keys(impl).length + " of " + Object.keys(reqs).length + " requirements delivered · " + tested + " of " + matrix.length + " scenario rows have a test";
557
+ var running = {}; (model.requirements || []).forEach(function(r){ if (r.state === "running") running[r.req] = 1; });
558
+ var byGroup = {}, order = [];
559
+ rows.forEach(function(r){ var g = r.group || "spec.md"; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(r); });
560
+ order.forEach(function(g){
561
+ var list = byGroup[g], d = el("details", "box mgroup"), mk = "m:" + g; d.open = mk in open ? open[mk] : true;
562
+ var done = {}; list.forEach(function(r){ if (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") done[r.requirement] = 1; });
563
+ var all = {}; list.forEach(function(r){ all[r.requirement] = 1; });
564
+ var sm = el("summary"); sm.append(el("span", "", cap(g)), el("span", "count", Object.keys(done).length + " / " + Object.keys(all).length + " delivered")); d.appendChild(sm);
565
+ d.addEventListener("toggle", function(){ open[mk] = d.open; save("sg-live-open", open); });
566
+ var wrap = el("div"); wrap.style.overflowX = "auto";
567
+ var t = el("table"), hr = el("tr"); ["Requirement", "Title", "Scenario", "Feature", "Tests", "Status"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
568
+ list.forEach(function(r){
569
+ var tr = el("tr"), reqTd = el("td", "mono"), a = el("a", "", r.requirement); a.href = "#"; a.addEventListener("click", function(ev){ ev.preventDefault(); setView("run"); pinned = r.requirement; tab = "req"; stepsKey = ""; draw(); });
570
+ reqTd.appendChild(a);
571
+ var feat = el("td", "tests"); if (r.feature) feat.appendChild(fileLink(r.feature)); else feat.textContent = "—";
572
+ var tests = el("td", "tests"); if (r.tests.length) r.tests.forEach(function(f, i){ if (i) tests.appendChild(document.createElement("br")); tests.appendChild(fileLink(f)); }); else tests.textContent = "—";
573
+ var stTd = el("td"), state = running[r.requirement] ? "running" : r.status, sp = el("span", "st " + state.split(" ").join(""));
574
+ sp.append(icon(running[r.requirement] ? "running" : (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") ? "pass" : "idle", 14),
575
+ document.createTextNode(running[r.requirement] ? "in progress" : r.status + (r.source === "change" ? " · in change" : "")));
576
+ stTd.appendChild(sp);
577
+ tr.append(reqTd, el("td", "", r.title), el("td", "mono", r.scenario), feat, tests, stTd); t.appendChild(tr);
578
+ });
579
+ wrap.appendChild(t); d.appendChild(wrap); box.appendChild(d);
580
+ });
581
+ if (!order.length) box.appendChild(el("div", "empty box", matrix.length ? "No row matches the filter." : "Reading the matrix…"));
582
+ }
583
+ function refreshMatrix(){
584
+ if (!matrixDirty) return;
585
+ matrixDirty = false;
586
+ fetch("live/matrix.json").then(function(x){ return x.json(); }).then(function(m){ matrix = m.rows || []; if (view === "matrix") drawMatrix(); else draw(); }).catch(function(){ matrixDirty = true; });
587
+ }
588
+ function setView(v){
589
+ view = v; $("vRun").setAttribute("aria-pressed", String(v === "run")); $("vMatrix").setAttribute("aria-pressed", String(v === "matrix"));
590
+ $("runView").hidden = v !== "run"; $("matrixView").hidden = v !== "matrix"; if (v === "matrix") { drawMatrix(); refreshMatrix(); } else { stepsKey = ""; draw(); }
591
+ }
592
+
254
593
  function draw(){
255
594
  var list = ordered(model.requirements || []), sel = selected(list);
256
- sidebar(list, sel); summary(list); graph(list, sel);
257
- var key = sel ? sel.req + ":" + sel.attempt + ":" + sel.steps.length + ":" + sel.state : "";
258
- if (key !== logKey) { logKey = key; steps(sel); }
595
+ sidebar(list, sel);
596
+ if (view === "run") { summary(list); modules(list, sel); focus(sel); }
259
597
  }
260
598
  function tick(){
261
599
  var list = ordered(model.requirements || []), sel = selected(list);
262
- summary(list); sidebar(list, sel);
263
- var run = document.querySelector("#pipeline li.running .d");
264
- if (run && sel && sel.state === "running") draw();
600
+ if (view === "run") { summary(list); if (sel && sel.state === "running" && tab === "steps") { stepsKey = ""; focus(sel); } }
265
601
  }
266
- function accept(m){ model = m && m.requirements ? m : {requirements:[]}; draw(); }
602
+ function accept(m){ model = m && m.requirements ? m : {requirements:[]}; if (m && m.projectDir) projectDir = m.projectDir; matrixDirty = true; draw(); refreshMatrix(); }
267
603
 
604
+ Array.prototype.forEach.call(document.querySelectorAll(".tabs button"), function(b){
605
+ b.addEventListener("click", function(){ tab = b.dataset.tab; stepsKey = ""; draw(); });
606
+ });
607
+ $("vRun").addEventListener("click", function(){ setView("run"); });
608
+ $("vMatrix").addEventListener("click", function(){ setView("matrix"); });
609
+ $("mFilter").addEventListener("input", drawMatrix);
268
610
  fetch("live.json").then(function(x){ return x.json(); }).then(accept).catch(function(){});
269
611
  var es = new EventSource("live/stream");
270
612
  es.addEventListener("live", function(e){ try { accept(JSON.parse(e.data)); } catch (err) {} });
271
613
  es.onopen = function(){ $("conn").classList.add("on"); $("connText").textContent = "Live"; };
272
614
  es.onerror = function(){ $("conn").classList.remove("on"); $("connText").textContent = "Reconnecting"; };
273
615
  setInterval(tick, 1000);
274
- var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){ logKey = ""; draw(); }, 120); });
616
+ var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){ stepsKey = ""; draw(); }, 120); });
275
617
  })();
276
618
  </script>
277
619
  </body></html>`;