@rsaglobaltech/specgate 0.22.3 → 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.
- package/CHANGELOG.md +60 -0
- package/dist/packages/core/src/domain/DoneVerification.js +21 -2
- package/dist/packages/core/src/domain/DoneVerification.js.map +1 -1
- package/dist/packages/core/test/unit/done-verification.test.js +18 -0
- package/dist/packages/core/test/unit/done-verification.test.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +50 -5
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +6 -0
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js +2 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StudioCommand.js +31 -0
- package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
- package/dist/scripts/harness/events.js +57 -0
- package/dist/scripts/harness/events.js.map +1 -1
- package/dist/scripts/lib/harness-events.js +32 -1
- package/dist/scripts/lib/harness-events.js.map +1 -1
- package/dist/scripts/lib/harness-guard.js +186 -0
- package/dist/scripts/lib/harness-guard.js.map +1 -0
- package/dist/scripts/lib/in-flight.js +17 -4
- package/dist/scripts/lib/in-flight.js.map +1 -1
- package/dist/scripts/lib/live-detail.js +345 -0
- package/dist/scripts/lib/live-detail.js.map +1 -0
- package/dist/scripts/lib/live-model.js +158 -9
- package/dist/scripts/lib/live-model.js.map +1 -1
- package/dist/scripts/lib/live-page.js +599 -67
- package/dist/scripts/lib/live-page.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,84 +5,616 @@ exports.livePage = livePage;
|
|
|
5
5
|
* The live view's page (ADR-0037): one self-contained document, no external
|
|
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
|
+
*
|
|
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`.
|
|
24
|
+
*
|
|
25
|
+
* The script lives inside this template literal: it uses no backticks, no
|
|
26
|
+
* dollar-brace, and no backslash escapes.
|
|
8
27
|
*/
|
|
9
28
|
function livePage(projectName) {
|
|
10
|
-
const title = String(projectName).replace(/[&<>"]/g, "");
|
|
29
|
+
const title = String(projectName).replace(/[&<>"']/g, "");
|
|
11
30
|
return `<!doctype html>
|
|
12
31
|
<html lang="en"><head><meta charset="utf-8"/>
|
|
13
32
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
14
33
|
<title>Harness live — ${title}</title>
|
|
15
34
|
<style>
|
|
16
|
-
:root{--bg:#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
.
|
|
35
|
+
:root{--bg:#f6f8fa;--panel:#ffffff;--line:#d1d9e0;--soft:#eff2f5;--ink:#1f2328;--muted:#59636e;--faint:#8c959f;
|
|
36
|
+
--pass:#1a7f37;--fail:#cf222e;--run:#9a6700;--run-bg:#fff8c5;--accent:#0969da;--sel:#ddf4ff;
|
|
37
|
+
--log:#1f2328;--log-ink:#d1d9e0;--log-muted:#8c959f;--top:56px;
|
|
38
|
+
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
|
|
39
|
+
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;color-scheme:light}
|
|
40
|
+
@media (prefers-color-scheme: dark){:root{--bg:#0d1117;--panel:#151b23;--line:#3d444d;--soft:#212830;--ink:#f0f6fc;
|
|
41
|
+
--muted:#9198a1;--faint:#656c76;--pass:#3fb950;--fail:#f85149;--run:#d29922;--run-bg:#2c2513;--accent:#4493f8;
|
|
42
|
+
--sel:#16263d;--log:#010409;--log-ink:#d1d7e0;--log-muted:#656c76;color-scheme:dark}}
|
|
43
|
+
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 var(--sans)}
|
|
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}
|
|
51
|
+
.pill{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
|
52
|
+
.pill i{width:8px;height:8px;border-radius:50%;background:var(--faint)} .pill.on i{background:var(--pass)}
|
|
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}
|
|
62
|
+
.job:hover{background:var(--soft)} .job.sel{background:var(--sel);font-weight:600}
|
|
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}
|
|
64
|
+
.job .d{font-size:12px;color:var(--muted);font-weight:400;font-variant-numeric:tabular-nums}
|
|
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}
|
|
77
|
+
.head{display:flex;align-items:center;gap:12px;flex-wrap:wrap} .head h1{margin:0;font-size:20px;font-weight:600}
|
|
78
|
+
.badge{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600}
|
|
79
|
+
.badge.running{background:var(--run-bg);color:var(--run)} .badge.pass{background:rgba(31,136,61,.12);color:var(--pass)}
|
|
80
|
+
.badge.fail{background:rgba(207,34,46,.12);color:var(--fail)} .badge i{width:8px;height:8px;border-radius:50%;background:currentColor}
|
|
81
|
+
.box{background:var(--panel);border:1px solid var(--line);border-radius:6px}
|
|
82
|
+
#progress{display:flex;flex-wrap:wrap;gap:8px 40px;padding:12px 18px}
|
|
83
|
+
#progress div span{display:block;font-size:12px;color:var(--muted)} #progress div b{font-weight:400;font-variant-numeric:tabular-nums}
|
|
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}
|
|
98
|
+
#focus{display:flex;flex-direction:column;overflow:hidden}
|
|
99
|
+
#focus .fh{padding:12px 16px 0;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
|
|
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}
|
|
105
|
+
#pipeline{list-style:none;margin:0;padding:0}
|
|
106
|
+
#pipeline>li{display:flex;align-items:center;gap:10px;padding:8px 16px;border-bottom:1px solid var(--soft);font-size:13px}
|
|
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}
|
|
134
|
+
.ic{flex:none;width:16px;height:16px;display:inline-block}
|
|
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}}
|
|
42
140
|
</style></head>
|
|
43
141
|
<body>
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
142
|
+
<div class="top">
|
|
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>
|
|
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>
|
|
149
|
+
<span id="conn" class="pill"><i></i><span id="connText">connecting</span></span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="layout">
|
|
152
|
+
<aside aria-label="Work queue"><div id="jobs"></div></aside>
|
|
153
|
+
<main>
|
|
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>
|
|
184
|
+
</section>
|
|
185
|
+
</main>
|
|
186
|
+
</div>
|
|
48
187
|
<script>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
188
|
+
(function(){
|
|
189
|
+
var NS = "http:" + "//www.w3.org/2000/svg";
|
|
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) {} }
|
|
195
|
+
function $(id){ return document.getElementById(id); }
|
|
196
|
+
function el(tag, cls, text){ var e = document.createElement(tag); if (cls) e.className = cls; if (text !== undefined) e.textContent = text; return e; }
|
|
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; }
|
|
198
|
+
function now(){ return Date.now() / 1000; }
|
|
199
|
+
function two(n){ return (n < 10 ? "0" : "") + n; }
|
|
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;
|
|
201
|
+
return h ? h + "h " + m + "m" : m ? m + "m " + x + "s" : x + "s"; }
|
|
202
|
+
function stamp(t){ if (!t) return ""; var d = new Date(t * 1000); return two(d.getHours()) + ":" + two(d.getMinutes()) + ":" + two(d.getSeconds()); }
|
|
203
|
+
function elapsed(r){ return r.state === "running" ? Math.max(r.elapsed, now() - r.started) : r.elapsed; }
|
|
204
|
+
function clip(t, n){ t = String(t || ""); return t.length > n ? t.slice(0, n - 1) + "…" : t; }
|
|
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
|
+
}
|
|
212
|
+
function icon(state, size){
|
|
213
|
+
var v = s("svg", {viewBox:"0 0 16 16", width:size || 16, height:size || 16, "class":"ic", "aria-hidden":"true"});
|
|
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); }
|
|
215
|
+
else if (state === "fail") { s("circle", {cx:8, cy:8, r:7, fill:"var(--fail)"}, v); s("path", {d:"M5.5 5.5l5 5M10.5 5.5l-5 5", stroke:"#fff", "stroke-width":"1.8", "stroke-linecap":"round"}, v); }
|
|
216
|
+
else if (state === "running") { s("circle", {cx:8, cy:8, r:6, fill:"none", stroke:"var(--run)", "stroke-width":"2", opacity:".25"}, v);
|
|
217
|
+
s("path", {d:"M8 2a6 6 0 0 1 6 6", fill:"none", stroke:"var(--run)", "stroke-width":"2", "stroke-linecap":"round", "class":"spin"}, v); }
|
|
218
|
+
else s("circle", {cx:8, cy:8, r:6.25, fill:"none", stroke:"var(--faint)", "stroke-width":"1.5"}, v);
|
|
219
|
+
return v;
|
|
220
|
+
}
|
|
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;
|
|
226
|
+
}
|
|
227
|
+
function ordered(reqs){ return reqs.slice().sort(function(a, b){ return a.started - b.started; }); }
|
|
228
|
+
function groups(list){
|
|
229
|
+
var out = [], at = {};
|
|
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); });
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
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
|
+
}
|
|
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
|
+
*/
|
|
269
|
+
function sidebar(list, sel){
|
|
270
|
+
var box = $("jobs"), keepScroll = box.parentNode.scrollTop; box.replaceChildren();
|
|
271
|
+
var sum = el("button", "job summary-btn" + (pinned ? "" : " sel"), "Summary"); sum.type = "button";
|
|
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;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function summary(list){
|
|
322
|
+
var p = 0, f = 0, run = 0, first = 0, last = 0;
|
|
323
|
+
list.forEach(function(r){ if (r.state === "pass") p++; else if (r.state === "fail") f++; else run++;
|
|
324
|
+
if (!first || r.started < first) first = r.started; last = Math.max(last, r.state === "running" ? now() : r.updated); });
|
|
325
|
+
var state = run ? "running" : f ? "fail" : list.length ? "pass" : "running";
|
|
326
|
+
$("runBadge").className = "badge " + state;
|
|
327
|
+
$("runState").textContent = run ? "In progress" : f ? "Failed" : list.length ? "Completed" : "Waiting";
|
|
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";
|
|
330
|
+
var box = $("progress"); box.replaceChildren();
|
|
331
|
+
[["Started", first ? stamp(first) : "—"], ["Duration", first ? dur(last - first) : "—"],
|
|
332
|
+
["Requirements", p + " passed · " + run + " running · " + f + " failed"],
|
|
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){
|
|
336
|
+
var d = el("div"); d.append(el("span", "", x[0]), el("b", "", x[1])); box.appendChild(d);
|
|
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);
|
|
341
|
+
}
|
|
342
|
+
|
|
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);
|
|
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);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function steps(r){
|
|
392
|
+
var ol = $("pipeline");
|
|
393
|
+
if (!r) { ol.replaceChildren(el("li", "empty", "Each requirement opens here as the harness works on it.")); return; }
|
|
394
|
+
var events = r.steps.slice().reverse(), firstGate = events.filter(function(e){ return e.type === "gate"; })[0];
|
|
395
|
+
var rows = [], seen = {Specification:false, Architecture:false, Tests:false, Merge:false};
|
|
396
|
+
var agentEnd = firstGate ? Number(firstGate.t) : (r.state === "running" ? now() : r.updated);
|
|
397
|
+
rows.push({name:"Agent", state: firstGate ? "pass" : r.state === "running" ? "running" : r.state, d: dur(agentEnd - r.started), log: !firstGate});
|
|
398
|
+
var prevT = agentEnd;
|
|
399
|
+
events.forEach(function(e){
|
|
400
|
+
if (e.type !== "gate" && e.type !== "merge") return;
|
|
401
|
+
var stage = e.type === "merge" ? "merge into " + (e.into || "HEAD") : String(e.stage || "");
|
|
402
|
+
var kind = e.type === "merge" || stage.indexOf("post-merge") === 0 ? "Merge" : stage.indexOf("validate") >= 0 ? "Specification" : stage.indexOf("architecture") >= 0 ? "Architecture" : "Tests";
|
|
403
|
+
seen[kind] = true;
|
|
404
|
+
rows.push({name:stage, mono:true, state: e.type === "merge" || e.result === "pass" ? "pass" : "fail", d: dur(Number(e.t) - prevT)});
|
|
405
|
+
prevT = Number(e.t);
|
|
406
|
+
});
|
|
407
|
+
var lastGate = events.filter(function(e){ return e.type === "gate"; }).slice(-1)[0];
|
|
408
|
+
if (r.state === "running" && firstGate && !(lastGate && lastGate.result !== "pass")) {
|
|
409
|
+
var next = ["Specification","Architecture","Tests","Merge"].filter(function(k){ return !seen[k]; })[0];
|
|
410
|
+
if (next) { rows.push({name:next, state:"running", d: dur(now() - prevT), log:true}); seen[next] = true; }
|
|
76
411
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
412
|
+
["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k, state:"idle", d:""}); });
|
|
413
|
+
ol.replaceChildren();
|
|
414
|
+
rows.forEach(function(x){
|
|
415
|
+
var li = el("li", x.state === "running" ? "running" : x.state === "idle" ? "idle" : "");
|
|
416
|
+
li.append(icon(x.state), el("span", "t" + (x.mono ? " mono" : ""), x.name), el("span", "d", x.d));
|
|
417
|
+
ol.appendChild(li);
|
|
418
|
+
if (x.log) { var lr = el("li", "logrow"); lr.appendChild(log(r)); ol.appendChild(lr); }
|
|
419
|
+
});
|
|
420
|
+
if (!rows.some(function(x){ return x.log; }) && r.steps.length) { var lr2 = el("li", "logrow"); lr2.appendChild(log(r)); ol.appendChild(lr2); }
|
|
421
|
+
}
|
|
422
|
+
function log(r){
|
|
423
|
+
var ul = el("ol", "feed"); ul.id = "feed";
|
|
424
|
+
r.steps.slice(0, 40).slice().reverse().forEach(function(e){
|
|
425
|
+
var li = el("li"), t = Number(e.ended || e.t) || 0, kind = e.type === "step" ? String(e.kind || "") : String(e.type || "");
|
|
426
|
+
var what = e.type === "message" ? e.text : e.type === "gate" ? (e.result === "pass" ? "passed " : "failed ") + e.stage
|
|
427
|
+
: e.type === "merge" ? "merged into " + (e.into || "HEAD") : (e.target || e.tool || "");
|
|
428
|
+
if (e.added !== undefined || e.removed !== undefined) what += " +" + (Number(e.added) || 0) + " −" + (Number(e.removed) || 0);
|
|
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)));
|
|
431
|
+
ul.appendChild(li);
|
|
432
|
+
});
|
|
433
|
+
if (!ul.childNodes.length) ul.appendChild(el("li", "", "The agent's steps appear here as it works."));
|
|
434
|
+
requestAnimationFrame(function(){ ul.scrollTop = ul.scrollHeight; });
|
|
435
|
+
return ul;
|
|
436
|
+
}
|
|
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
|
+
|
|
593
|
+
function draw(){
|
|
594
|
+
var list = ordered(model.requirements || []), sel = selected(list);
|
|
595
|
+
sidebar(list, sel);
|
|
596
|
+
if (view === "run") { summary(list); modules(list, sel); focus(sel); }
|
|
597
|
+
}
|
|
598
|
+
function tick(){
|
|
599
|
+
var list = ordered(model.requirements || []), sel = selected(list);
|
|
600
|
+
if (view === "run") { summary(list); if (sel && sel.state === "running" && tab === "steps") { stepsKey = ""; focus(sel); } }
|
|
601
|
+
}
|
|
602
|
+
function accept(m){ model = m && m.requirements ? m : {requirements:[]}; if (m && m.projectDir) projectDir = m.projectDir; matrixDirty = true; draw(); refreshMatrix(); }
|
|
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);
|
|
610
|
+
fetch("live.json").then(function(x){ return x.json(); }).then(accept).catch(function(){});
|
|
611
|
+
var es = new EventSource("live/stream");
|
|
612
|
+
es.addEventListener("live", function(e){ try { accept(JSON.parse(e.data)); } catch (err) {} });
|
|
613
|
+
es.onopen = function(){ $("conn").classList.add("on"); $("connText").textContent = "Live"; };
|
|
614
|
+
es.onerror = function(){ $("conn").classList.remove("on"); $("connText").textContent = "Reconnecting"; };
|
|
615
|
+
setInterval(tick, 1000);
|
|
616
|
+
var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){ stepsKey = ""; draw(); }, 120); });
|
|
617
|
+
})();
|
|
86
618
|
</script>
|
|
87
619
|
</body></html>`;
|
|
88
620
|
}
|