@rsaglobaltech/specgate 0.23.0 → 0.25.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 (57) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/packages/core/src/domain/Clarification.js +173 -0
  3. package/dist/packages/core/src/domain/Clarification.js.map +1 -0
  4. package/dist/packages/core/src/domain/DoneVerification.js +21 -2
  5. package/dist/packages/core/src/domain/DoneVerification.js.map +1 -1
  6. package/dist/packages/core/src/domain/DraftChecklist.js +16 -16
  7. package/dist/packages/core/src/domain/DraftChecklist.js.map +1 -1
  8. package/dist/packages/core/src/domain/HarnessRun.js.map +1 -1
  9. package/dist/packages/core/test/unit/clarification.test.js +161 -0
  10. package/dist/packages/core/test/unit/clarification.test.js.map +1 -0
  11. package/dist/packages/core/test/unit/done-verification.test.js +18 -0
  12. package/dist/packages/core/test/unit/done-verification.test.js.map +1 -1
  13. package/dist/scripts/answer.js +10 -0
  14. package/dist/scripts/answer.js.map +1 -0
  15. package/dist/scripts/ask.js +10 -0
  16. package/dist/scripts/ask.js.map +1 -0
  17. package/dist/scripts/brief_add.js +10 -0
  18. package/dist/scripts/brief_add.js.map +1 -0
  19. package/dist/scripts/cli/commands/change/ChangeCommand.js +87 -1
  20. package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
  21. package/dist/scripts/cli/commands/harness/RunCommand.js +128 -8
  22. package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
  23. package/dist/scripts/cli/commands/quality/CheckCommand.js +6 -0
  24. package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -1
  25. package/dist/scripts/cli/commands/spec/ClarifyCommands.js +275 -0
  26. package/dist/scripts/cli/commands/spec/ClarifyCommands.js.map +1 -0
  27. package/dist/scripts/cli/commands/spec/DoneCommand.js +2 -1
  28. package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
  29. package/dist/scripts/cli/commands/spec/StudioCommand.js +31 -0
  30. package/dist/scripts/cli/commands/spec/StudioCommand.js.map +1 -1
  31. package/dist/scripts/harness/events.js +57 -0
  32. package/dist/scripts/harness/events.js.map +1 -1
  33. package/dist/scripts/harness/prompt.js +2 -0
  34. package/dist/scripts/harness/prompt.js.map +1 -1
  35. package/dist/scripts/lib/brief-add.js +197 -0
  36. package/dist/scripts/lib/brief-add.js.map +1 -0
  37. package/dist/scripts/lib/change-impact.js +156 -0
  38. package/dist/scripts/lib/change-impact.js.map +1 -0
  39. package/dist/scripts/lib/clarification.js +300 -0
  40. package/dist/scripts/lib/clarification.js.map +1 -0
  41. package/dist/scripts/lib/harness-events.js +32 -1
  42. package/dist/scripts/lib/harness-events.js.map +1 -1
  43. package/dist/scripts/lib/harness-guard.js +186 -0
  44. package/dist/scripts/lib/harness-guard.js.map +1 -0
  45. package/dist/scripts/lib/in-flight.js +17 -4
  46. package/dist/scripts/lib/in-flight.js.map +1 -1
  47. package/dist/scripts/lib/live-detail.js +346 -0
  48. package/dist/scripts/lib/live-detail.js.map +1 -0
  49. package/dist/scripts/lib/live-model.js +125 -9
  50. package/dist/scripts/lib/live-model.js.map +1 -1
  51. package/dist/scripts/lib/live-page.js +465 -119
  52. package/dist/scripts/lib/live-page.js.map +1 -1
  53. package/dist/scripts/lib/surface.js +52 -0
  54. package/dist/scripts/lib/surface.js.map +1 -1
  55. package/dist/scripts/questions.js +10 -0
  56. package/dist/scripts/questions.js.map +1 -0
  57. 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,192 +34,371 @@ 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)} .seg.question{background:var(--run)}
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); }
114
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); }
115
216
  else if (state === "running") { s("circle", {cx:8, cy:8, r:6, fill:"none", stroke:"var(--run)", "stroke-width":"2", opacity:".25"}, v);
116
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 if (state === "question") { s("circle", {cx:8, cy:8, r:7, fill:"var(--run)"}, v); var tq = s("text", {x:8, y:11.5, "text-anchor":"middle", "font-size":"10", "font-weight":"700", fill:"#fff"}, v); tq.textContent = "?"; }
117
219
  else s("circle", {cx:8, cy:8, r:6.25, fill:"none", stroke:"var(--faint)", "stroke-width":"1.5"}, v);
118
220
  return v;
119
221
  }
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;
222
+ function selected(list){
223
+ if (pinned) { var p = list.filter(function(r){ return r.req === pinned; })[0]; if (p) return p;
224
+ var m = matrix.filter(function(x){ return x.requirement === pinned; })[0];
225
+ 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}; }
226
+ return list.filter(function(r){ return r.state === "running"; })[0] || list[0] || null;
123
227
  }
124
228
  function ordered(reqs){ return reqs.slice().sort(function(a, b){ return a.started - b.started; }); }
125
229
  function groups(list){
126
230
  var out = [], at = {};
127
231
  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); });
232
+ out.forEach(function(g){ g.pass = g.reqs.filter(function(r){ return r.state === "pass"; }).length;
233
+ g.fail = g.reqs.filter(function(r){ return r.state === "fail"; }).length; g.done = g.pass === g.reqs.length;
234
+ g.time = g.reqs.reduce(function(a, r){ return a + elapsed(r); }, 0); });
128
235
  return out;
129
236
  }
130
- function pick(id){ pinned = pinned === id ? null : id; logKey = ""; draw(); }
237
+ function pick(id){ pinned = pinned === id ? null : id; stepsKey = ""; draw(); }
238
+
239
+ 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); }
240
+ function usd(n){ return n === null || n === undefined ? "—" : "$" + Number(n).toFixed(2); }
241
+ function shortModel(m){ return String(m || "").replace(/^claude-/, ""); }
242
+ /** How full the context is, as a bar: green, amber past 60%, red past 85%. */
243
+ function ctxBar(u){
244
+ var w = el("div", "ctx"), pct = u && u.window ? Math.min(100, u.context / u.window * 100) : 0;
245
+ var bar = el("div", "bar"), i = el("i"); i.style.width = pct + "%";
246
+ i.style.background = pct > 85 ? "var(--fail)" : pct > 60 ? "var(--run)" : "var(--pass)"; bar.appendChild(i);
247
+ w.append(bar, el("span", "d", u && u.window ? kt(u.context) + " of " + kt(u.window) + " · " + Math.round(pct) + "%" : "—"));
248
+ return w;
249
+ }
250
+ function jobBtn(r, sel, note, state){
251
+ var b = el("button", "job" + (sel && sel.req === r.req ? " sel" : "")); b.type = "button";
252
+ b.setAttribute("aria-current", sel && sel.req === r.req ? "true" : "false");
253
+ var txt = el("span", "name"); txt.append(el("span", "", r.req + " " + (r.title || "")));
254
+ if (note) txt.appendChild(el("small", "", note));
255
+ b.append(icon(state || r.state), txt);
256
+ b.addEventListener("click", function(){ pick(r.req); });
257
+ return b;
258
+ }
259
+ function block(title, count){
260
+ var d = el("section", "qsec"), h = el("h2"); h.append(el("span", "", title));
261
+ if (count !== undefined) h.appendChild(el("span", "count", String(count)));
262
+ d.appendChild(h); return d;
263
+ }
131
264
 
265
+ /**
266
+ * The sidebar is the run's work queue (REQ-182), not a second map of it: what
267
+ * runs now and with which model and context, what needs a person, what comes
268
+ * next, and what finished last. The map is the matrix in the centre.
269
+ */
132
270
  function sidebar(list, sel){
133
- var box = $("jobs"); box.replaceChildren();
271
+ var box = $("jobs"), keepScroll = box.parentNode.scrollTop; box.replaceChildren();
134
272
  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
- });
273
+ sum.addEventListener("click", function(){ pinned = null; stepsKey = ""; draw(); }); box.appendChild(sum);
274
+ var since = model.queueStarted || 0, byReq = {}, titles = {};
275
+ list.forEach(function(r){ byReq[r.req] = r; });
276
+ matrix.forEach(function(m){ titles[m.requirement] = m.title; });
277
+
278
+ var cur = list.filter(function(r){ return r.state === "running"; })[0];
279
+ var now0 = block("Now");
280
+ if (cur) {
281
+ var card = el("div", "now"); card.appendChild(jobBtn(cur, sel, "attempt " + cur.attempt + " · " + cur.phase + " · " + dur(elapsed(cur))));
282
+ var kv = el("dl", "kv");
283
+ 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); };
284
+ add("Model", cur.agent ? shortModel(cur.agent.model) : "not reported");
285
+ add("Context", ctxBar(cur.usage));
286
+ add("Output", cur.usage ? kt(cur.usage.output) + " tokens" : "—");
287
+ if (cur.prompt) add("Prompt", "~" + kt(cur.prompt.tokens) + " tokens · " + cur.prompt.lines + " lines");
288
+ card.appendChild(kv); now0.appendChild(card);
289
+ } else now0.appendChild(el("p", "muted", "Nothing running."));
290
+ box.appendChild(now0);
291
+
292
+ // What the agent asked waits for a person first (REQ-189).
293
+ var need = list.filter(function(r){ return r.state === "question" && r.question; }).map(function(r){
294
+ return {r:r, note:"asks: " + clip(r.question.text, 90) + " · " + r.question.answer, st:"question"}; });
295
+ need = need.concat(list.filter(function(r){ return r.state === "fail"; }).map(function(r){
296
+ var g = r.steps.filter(function(e){ return e.type === "gate" && e.result !== "pass"; })[0];
297
+ return {r:r, note:"failed · attempt " + r.attempt + (g ? " · " + clip(String(g.stage).split(":")[0], 40) : ""), st:"fail"}; }));
298
+ var seenB = {};
299
+ matrix.forEach(function(m){ if (m.status === "Needs Clarification" && !byReq[m.requirement] && !seenB[m.requirement]) { seenB[m.requirement] = 1;
300
+ need.push({r:{req:m.requirement, title:m.title, state:"idle"}, note:"blocked · needs clarification", st:"idle"}); } });
301
+ var n1 = block("Needs attention", need.length);
302
+ need.forEach(function(x){ n1.appendChild(jobBtn(x.r, sel, x.note, x.st)); });
303
+ if (!need.length) n1.appendChild(el("p", "muted", "Nothing failed or blocked."));
304
+ box.appendChild(n1);
305
+
306
+ var done = list.filter(function(r){ return r.state === "pass" && r.started >= since; });
307
+ var next = (model.queue || []).filter(function(id){ var r = byReq[id]; return !(r && r.started >= since); });
308
+ var times = done.map(function(r){ return r.elapsed; }).sort(function(a, b){ return a - b; });
309
+ var median = times.length ? times[Math.floor(times.length / 2)] : 0;
310
+ var n2 = block("Up next", next.length);
311
+ if (next.length && median) n2.appendChild(el("p", "muted", "about " + dur(next.length * median) + " at " + dur(median) + " each"));
312
+ next.slice(0, 6).forEach(function(id){ n2.appendChild(jobBtn({req:id, title:titles[id] || "", state:"idle"}, sel, "", "idle")); });
313
+ if (next.length > 6) n2.appendChild(el("p", "muted", "+ " + (next.length - 6) + " more"));
314
+ if (!next.length) n2.appendChild(el("p", "muted", (model.queue || []).length ? "The queue is done." : "No queue recorded for this run."));
315
+ box.appendChild(n2);
316
+
317
+ var recent = list.filter(function(r){ return r.state === "pass"; }).sort(function(a, b){ return b.updated - a.updated; }).slice(0, 5);
318
+ var n3 = block("Finished recently");
319
+ recent.forEach(function(r){ n3.appendChild(jobBtn(r, sel, dur(r.elapsed) + (r.usage && r.usage.cost !== null ? " · " + usd(r.usage.cost) : ""), "pass")); });
320
+ if (!recent.length) n3.appendChild(el("p", "muted", "None yet."));
321
+ box.appendChild(n3);
322
+ box.parentNode.scrollTop = keepScroll;
146
323
  }
147
324
 
148
325
  function summary(list){
149
326
  var p = 0, f = 0, run = 0, first = 0, last = 0;
150
- list.forEach(function(r){ if (r.state === "pass") p++; else if (r.state === "fail") f++; else run++;
327
+ list.forEach(function(r){ if (r.state === "pass") p++; else if (r.state === "fail" || r.state === "question") f++; else run++;
151
328
  if (!first || r.started < first) first = r.started; last = Math.max(last, r.state === "running" ? now() : r.updated); });
152
329
  var state = run ? "running" : f ? "fail" : list.length ? "pass" : "running";
153
330
  $("runBadge").className = "badge " + state;
154
331
  $("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";
332
+ var cur = list.filter(function(r){ return r.state === "running"; })[0];
333
+ $("runTitle").textContent = cur ? cap(cur.group) + " · " + cur.req + " " + (cur.title || "") : groups(list).map(function(g){ return cap(g.name); }).join(", ") || "Harness run";
157
334
  var box = $("progress"); box.replaceChildren();
158
335
  [["Started", first ? stamp(first) : "—"], ["Duration", first ? dur(last - first) : "—"],
159
336
  ["Requirements", p + " passed · " + run + " running · " + f + " failed"],
160
- ["Agent time", dur(list.reduce(function(a, r){ return a + elapsed(r); }, 0))]].forEach(function(x){
337
+ ["Agent time", dur(list.reduce(function(a, r){ return a + elapsed(r); }, 0))]].concat(model.totals && (model.totals.input || model.totals.output) ? [
338
+ ["Tokens", kt(model.totals.input + model.totals.cacheRead + model.totals.cacheWrite) + " in · " + kt(model.totals.output) + " out"],
339
+ ["Cost", usd(model.totals.cost)]] : []).forEach(function(x){
161
340
  var d = el("div"); d.append(el("span", "", x[0]), el("b", "", x[1])); box.appendChild(d);
162
341
  });
342
+ var bar = el("div", "bar"); bar.style.flexBasis = "100%"; var total = Math.max(1, list.length);
343
+ [["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); });
344
+ box.appendChild(bar);
163
345
  }
164
346
 
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); } });
347
+ /**
348
+ * The run as a matrix of modules (REQ-181): one row per module, a segment
349
+ * per requirement in id order, coloured by what happened to it in this run,
350
+ * or by its status when the run has not reached it; the passed count and
351
+ * the agent time. A segment opens its requirement below.
352
+ */
353
+ function modules(list, sel){
354
+ var box = $("chain"); box.replaceChildren();
355
+ var byReq = {}; list.forEach(function(r){ byReq[r.req] = r; });
356
+ var reqs = [], seen = {};
357
+ matrix.forEach(function(m){ if (seen[m.requirement] || m.requirement === "REQ-001") return; seen[m.requirement] = 1;
358
+ reqs.push({req:m.requirement, title:m.title, group:m.group, status:m.status}); });
359
+ 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"}); } });
360
+ if (!reqs.length) { box.appendChild(el("div", "empty", "No harness events yet. Run specgate harness run: each requirement appears here as it starts.")); return; }
361
+ var order = [], rowsBy = {};
362
+ reqs.sort(function(x, y){ return x.req.localeCompare(y.req); }).forEach(function(q){
363
+ var g = q.group || "spec"; if (!rowsBy[g]) { rowsBy[g] = []; order.push(g); } rowsBy[g].push(q); });
364
+ var rank = function(g){ var rs = rowsBy[g], ev = rs.map(function(q){ return byReq[q.req]; }).filter(Boolean);
365
+ return ev.length ? Math.min.apply(null, ev.map(function(r){ return r.started; })) : Infinity; };
366
+ order.sort(function(x, y){ return rank(x) - rank(y); });
367
+ var head = el("div", "mm-row mm-head");
368
+ ["", "Module", "Requirements", "Passed", "Time"].forEach(function(h, i){ var c = el("span", i >= 3 ? "num" : "", h); head.appendChild(c); });
369
+ box.appendChild(head);
370
+ order.forEach(function(g){
371
+ var rs = rowsBy[g], pass = 0, fail = 0, run = 0, time = 0;
372
+ var row = el("div", "mm-row"), bar = el("span", "mm-bar");
373
+ rs.forEach(function(q){
374
+ var ev = byReq[q.req], st = ev ? ev.state : (q.status === "Implemented" || q.status === "Verified" || q.status === "Released") ? "pass"
375
+ : q.status === "Needs Clarification" ? "blocked" : "queued";
376
+ if (st === "pass") pass++; else if (st === "fail" || st === "question") fail++; else if (st === "running") run++;
377
+ if (ev) time += elapsed(ev);
378
+ var seg = el("button", "seg " + st + (sel && sel.req === q.req ? " on" : "")); seg.type = "button";
379
+ seg.title = q.req + " — " + (q.title || "") + " · " + (ev ? st + " · " + dur(elapsed(ev)) : st);
380
+ seg.setAttribute("aria-label", seg.title);
381
+ seg.addEventListener("click", function(){ pick(q.req); });
382
+ bar.appendChild(seg);
383
+ });
384
+ var state = fail ? "fail" : run ? "running" : pass === rs.length ? "pass" : "idle";
385
+ if (fail) row.classList.add("has-fail"); else if (run) row.classList.add("has-run"); else if (state === "idle" && !pass) row.classList.add("queued");
386
+ 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"));
387
+ box.appendChild(row);
201
388
  });
389
+ var legend = el("div", "mm-legend");
390
+ [["pass", "passed"], ["running", "running"], ["fail", "failed"], ["question", "asks"], ["blocked", "blocked"], ["queued", "queued"]].forEach(function(x){
391
+ var s0 = el("span"), sw = el("i", "seg " + x[0]); s0.append(sw, document.createTextNode(x[1])); legend.appendChild(s0); });
392
+ box.appendChild(legend);
202
393
  }
203
394
 
204
- /** The selected requirement's steps: the agent, then each stage of the gate as it ran, the rest still to come. */
205
395
  function steps(r){
206
396
  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;
397
+ if (!r) { ol.replaceChildren(el("li", "empty", "Each requirement opens here as the harness works on it.")); return; }
210
398
  var events = r.steps.slice().reverse(), firstGate = events.filter(function(e){ return e.type === "gate"; })[0];
211
399
  var rows = [], seen = {Specification:false, Architecture:false, Tests:false, Merge:false};
212
400
  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});
401
+ rows.push({name:"Agent", state: firstGate ? "pass" : r.state === "running" ? "running" : r.state, d: dur(agentEnd - r.started), log: !firstGate});
214
402
  var prevT = agentEnd;
215
403
  events.forEach(function(e){
216
404
  if (e.type !== "gate" && e.type !== "merge") return;
@@ -223,9 +411,9 @@ function livePage(projectName) {
223
411
  var lastGate = events.filter(function(e){ return e.type === "gate"; }).slice(-1)[0];
224
412
  if (r.state === "running" && firstGate && !(lastGate && lastGate.result !== "pass")) {
225
413
  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; }
414
+ if (next) { rows.push({name:next, state:"running", d: dur(now() - prevT), log:true}); seen[next] = true; }
227
415
  }
228
- ["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k, mono:false, state:"idle", d:""}); });
416
+ ["Specification","Architecture","Tests","Merge"].forEach(function(k){ if (!seen[k] && r.state === "running") rows.push({name:k, state:"idle", d:""}); });
229
417
  ol.replaceChildren();
230
418
  rows.forEach(function(x){
231
419
  var li = el("li", x.state === "running" ? "running" : x.state === "idle" ? "idle" : "");
@@ -236,14 +424,14 @@ function livePage(projectName) {
236
424
  if (!rows.some(function(x){ return x.log; }) && r.steps.length) { var lr2 = el("li", "logrow"); lr2.appendChild(log(r)); ol.appendChild(lr2); }
237
425
  }
238
426
  function log(r){
239
- var ul = el("ol"); ul.id = "feed";
427
+ var ul = el("ol", "feed"); ul.id = "feed";
240
428
  r.steps.slice(0, 40).slice().reverse().forEach(function(e){
241
429
  var li = el("li"), t = Number(e.ended || e.t) || 0, kind = e.type === "step" ? String(e.kind || "") : String(e.type || "");
242
430
  var what = e.type === "message" ? e.text : e.type === "gate" ? (e.result === "pass" ? "passed " : "failed ") + e.stage
243
431
  : e.type === "merge" ? "merged into " + (e.into || "HEAD") : (e.target || e.tool || "");
244
432
  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);
433
+ li.append(el("span", "ts", stamp(t)), el("span", "k", e.type === "message" ? "agent" : kind),
434
+ el("span", "x" + (e.type === "message" ? " say" : e.type === "gate" ? (e.result === "pass" ? " ok" : " bad") : ""), clip(what, 400)));
247
435
  ul.appendChild(li);
248
436
  });
249
437
  if (!ul.childNodes.length) ul.appendChild(el("li", "", "The agent's steps appear here as it works."));
@@ -251,27 +439,185 @@ function livePage(projectName) {
251
439
  return ul;
252
440
  }
253
441
 
442
+ /** The requirement's details, fetched once per change of its events. */
443
+ function detail(r, then){
444
+ var key = r.req + ":" + r.steps.length + ":" + r.state;
445
+ if (details[r.req] && details[r.req].key === key) return then(details[r.req].d);
446
+ fetch("live/req/" + encodeURIComponent(r.req) + ".json").then(function(x){ return x.json(); })
447
+ .then(function(d){ details[r.req] = {key:key, d:d}; then(d); }).catch(function(){});
448
+ }
449
+ function sect(title){ var d = el("div", "sect"); d.appendChild(el("h3", "", title)); return d; }
450
+ function paneReq(d){
451
+ var p = document.querySelector('[data-pane="req"]'); p.replaceChildren();
452
+ var a = sect("Requirement");
453
+ a.appendChild(el("div", "stmt", d.statement || "No requirement text found in the specification."));
454
+ var m = el("div", "meta"); m.style.marginTop = "8px";
455
+ m.append(el("span", "", "Status: " + d.status), el("span", "", "Module: " + (d.group || "—")));
456
+ if (d.source) { var sp = el("span", "", "Spec: "); sp.appendChild(fileLink(d.source.file, d.source.line)); m.appendChild(sp); }
457
+ a.appendChild(m); p.appendChild(a);
458
+ var b = sect("Scenarios · " + d.scenarios.length);
459
+ d.scenarios.forEach(function(sc){
460
+ var box = el("div", "scn"), h = el("div"); h.append(el("b", "", sc.id), document.createTextNode(" — " + sc.title)); box.appendChild(h);
461
+ var ol = el("ol"); sc.steps.forEach(function(st){ var li = el("li"), sp2 = st.indexOf(" "), kw = sp2 > 0 ? st.slice(0, sp2) : st;
462
+ li.append(el("span", "kw", kw), document.createTextNode(sp2 > 0 ? st.slice(sp2) : "")); ol.appendChild(li); });
463
+ box.appendChild(ol); b.appendChild(box);
464
+ });
465
+ if (!d.scenarios.length) b.appendChild(el("div", "meta", "No scenario."));
466
+ p.appendChild(b);
467
+ if (d.assumptions.length) {
468
+ var c = sect("Assumptions · " + d.assumptions.length), t = el("table"), hr = el("tr");
469
+ ["#", "Assumption", "Status"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
470
+ 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); });
471
+ c.appendChild(t); p.appendChild(c);
472
+ }
473
+ }
474
+ function paneFiles(d){
475
+ var p = document.querySelector('[data-pane="files"]'); p.replaceChildren();
476
+ var a = sect("Tests that name it · " + d.tests.length), ul = el("ul", "list");
477
+ d.tests.forEach(function(f){ var li = el("li"); li.appendChild(fileLink(f)); ul.appendChild(li); });
478
+ if (!d.tests.length) ul.appendChild(el("li", "", "No test names it yet."));
479
+ a.appendChild(ul); p.appendChild(a);
480
+ var b = sect("Feature"), ul2 = el("ul", "list");
481
+ d.features.forEach(function(f){ var li = el("li"); li.appendChild(fileLink(f)); ul2.appendChild(li); });
482
+ if (!d.features.length) ul2.appendChild(el("li", "", "Written when its change is archived."));
483
+ b.appendChild(ul2); p.appendChild(b);
484
+ var c = sect("Files the agent changed (last attempt) · " + d.files.length), ul3 = el("ul", "list");
485
+ 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); });
486
+ if (!d.files.length) ul3.appendChild(el("li", "", "No file changed yet."));
487
+ c.appendChild(ul3); p.appendChild(c);
488
+ }
489
+ function paneAttempts(d){
490
+ var p = document.querySelector('[data-pane="attempts"]'); p.replaceChildren();
491
+ var a = sect("Attempts · " + d.attempts.length + " · branch harness/" + d.id);
492
+ var t = el("table"), hr = el("tr"); ["#", "Result", "Started", "Duration", "Failed at", "Gate"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
493
+ d.attempts.forEach(function(x){
494
+ var tr = el("tr"), res = el("td"), st = el("span", "st");
495
+ st.append(icon(x.result), document.createTextNode(x.result)); res.appendChild(st);
496
+ tr.append(el("td", "mono", String(x.attempt)), res, el("td", "mono", stamp(x.started)), el("td", "", dur(x.duration)),
497
+ el("td", "", x.failedAt || "—"), el("td", "tests", x.gates.map(function(g){ return (g.result === "pass" ? "✓ " : "✗ ") + g.stage; }).join(" · ")));
498
+ t.appendChild(tr);
499
+ });
500
+ if (!d.attempts.length) { var tr0 = el("tr"), td0 = el("td", "", "No attempt recorded."); td0.colSpan = 6; tr0.appendChild(td0); t.appendChild(tr0); }
501
+ a.appendChild(t); p.appendChild(a);
502
+ }
503
+ /** The agent's session for the selected attempt: model, context, tokens, cost and the prompt it was given (REQ-182). */
504
+ function paneAgent(r){
505
+ var p = document.querySelector('[data-pane="agent"]'); p.replaceChildren();
506
+ if (!r.agent && !r.usage && !r.prompt) {
507
+ 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."));
508
+ return;
509
+ }
510
+ var a = sect("Session"), m = el("div", "meta");
511
+ m.append(el("span", "", "Model: " + (r.agent ? r.agent.model || "—" : "—")));
512
+ if (r.agent && r.agent.version) m.appendChild(el("span", "", "Claude Code " + r.agent.version));
513
+ if (r.agent && r.agent.permission) m.appendChild(el("span", "", "Permissions: " + r.agent.permission));
514
+ if (r.agent && r.agent.tools) m.appendChild(el("span", "", r.agent.tools + " tools"));
515
+ if (r.usage && r.usage.turns !== null) m.appendChild(el("span", "", r.usage.turns + " turns"));
516
+ a.appendChild(m); p.appendChild(a);
517
+ if (r.usage) {
518
+ var c = sect("Context window"); c.appendChild(ctxBar(r.usage));
519
+ 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" : "")));
520
+ p.appendChild(c);
521
+ var t = sect("Tokens"), tb = el("table"), hr = el("tr");
522
+ ["Input", "Cache read", "Cache write", "Output", "Cost"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); tb.appendChild(hr);
523
+ var tr = el("tr"), fin = r.usage.input || r.usage.cacheRead || r.usage.cacheWrite;
524
+ [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)); });
525
+ tb.appendChild(tr); t.appendChild(tb);
526
+ if (!fin) t.appendChild(el("div", "meta", "Totals and cost arrive when the session ends."));
527
+ p.appendChild(t);
528
+ }
529
+ if (r.prompt) {
530
+ var q = sect("Prompt"), qm = el("div", "meta");
531
+ 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)"));
532
+ 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";
533
+ qm.appendChild(open1); q.appendChild(qm);
534
+ var dt = el("details", "prompt"), ds = el("summary", "", "Show the prompt"), pre = el("pre");
535
+ dt.append(ds, pre);
536
+ 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."; }); });
537
+ q.appendChild(dt); p.appendChild(q);
538
+ }
539
+ }
540
+ function focus(sel){
541
+ $("fName").textContent = sel ? sel.req + " · " + (sel.title || "") : "—";
542
+ $("fMeta").textContent = sel ? "attempt " + sel.attempt + (sel.group ? " · " + sel.group : "") + " · harness/" + sel.req + (sel.agent ? " · " + shortModel(sel.agent.model) : "") : "";
543
+ Array.prototype.forEach.call(document.querySelectorAll(".tabs button"), function(b){ b.setAttribute("aria-selected", String(b.dataset.tab === tab)); });
544
+ Array.prototype.forEach.call(document.querySelectorAll("[data-pane]"), function(p){ p.hidden = p.dataset.pane !== tab; });
545
+ if (!sel) return steps(null);
546
+ var key = sel.req + ":" + sel.attempt + ":" + sel.steps.length + ":" + sel.state + ":" + tab + ":" + (sel.usage ? sel.usage.context + "/" + sel.usage.output : "");
547
+ if (key === stepsKey) return;
548
+ stepsKey = key;
549
+ if (tab === "steps") steps(sel);
550
+ else if (tab === "agent") paneAgent(sel);
551
+ else detail(sel, function(d){ if (tab === "req") paneReq(d); else if (tab === "files") paneFiles(d); else paneAttempts(d); });
552
+ }
553
+
554
+ /** The traceability matrix, one accordion per module, filtered as the viewer types. */
555
+ function drawMatrix(){
556
+ var q = String($("mFilter").value || "").toLowerCase(), box = $("matrix"); box.replaceChildren();
557
+ 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; });
558
+ 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; });
559
+ var tested = matrix.filter(function(r){ return r.tests.length; }).length;
560
+ $("mSum").textContent = Object.keys(impl).length + " of " + Object.keys(reqs).length + " requirements delivered · " + tested + " of " + matrix.length + " scenario rows have a test";
561
+ var running = {}; (model.requirements || []).forEach(function(r){ if (r.state === "running") running[r.req] = 1; });
562
+ var byGroup = {}, order = [];
563
+ rows.forEach(function(r){ var g = r.group || "spec.md"; if (!byGroup[g]) { byGroup[g] = []; order.push(g); } byGroup[g].push(r); });
564
+ order.forEach(function(g){
565
+ var list = byGroup[g], d = el("details", "box mgroup"), mk = "m:" + g; d.open = mk in open ? open[mk] : true;
566
+ var done = {}; list.forEach(function(r){ if (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") done[r.requirement] = 1; });
567
+ var all = {}; list.forEach(function(r){ all[r.requirement] = 1; });
568
+ 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);
569
+ d.addEventListener("toggle", function(){ open[mk] = d.open; save("sg-live-open", open); });
570
+ var wrap = el("div"); wrap.style.overflowX = "auto";
571
+ var t = el("table"), hr = el("tr"); ["Requirement", "Title", "Scenario", "Feature", "Tests", "Status"].forEach(function(x){ hr.appendChild(el("th", "", x)); }); t.appendChild(hr);
572
+ list.forEach(function(r){
573
+ 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(); });
574
+ reqTd.appendChild(a);
575
+ var feat = el("td", "tests"); if (r.feature) feat.appendChild(fileLink(r.feature)); else feat.textContent = "—";
576
+ 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 = "—";
577
+ var stTd = el("td"), state = running[r.requirement] ? "running" : r.status, sp = el("span", "st " + state.split(" ").join(""));
578
+ sp.append(icon(running[r.requirement] ? "running" : (r.status === "Implemented" || r.status === "Verified" || r.status === "Released") ? "pass" : "idle", 14),
579
+ document.createTextNode(running[r.requirement] ? "in progress" : r.status + (r.source === "change" ? " · in change" : "")));
580
+ stTd.appendChild(sp);
581
+ tr.append(reqTd, el("td", "", r.title), el("td", "mono", r.scenario), feat, tests, stTd); t.appendChild(tr);
582
+ });
583
+ wrap.appendChild(t); d.appendChild(wrap); box.appendChild(d);
584
+ });
585
+ if (!order.length) box.appendChild(el("div", "empty box", matrix.length ? "No row matches the filter." : "Reading the matrix…"));
586
+ }
587
+ function refreshMatrix(){
588
+ if (!matrixDirty) return;
589
+ matrixDirty = false;
590
+ 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; });
591
+ }
592
+ function setView(v){
593
+ view = v; $("vRun").setAttribute("aria-pressed", String(v === "run")); $("vMatrix").setAttribute("aria-pressed", String(v === "matrix"));
594
+ $("runView").hidden = v !== "run"; $("matrixView").hidden = v !== "matrix"; if (v === "matrix") { drawMatrix(); refreshMatrix(); } else { stepsKey = ""; draw(); }
595
+ }
596
+
254
597
  function draw(){
255
598
  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); }
599
+ sidebar(list, sel);
600
+ if (view === "run") { summary(list); modules(list, sel); focus(sel); }
259
601
  }
260
602
  function tick(){
261
603
  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();
604
+ if (view === "run") { summary(list); if (sel && sel.state === "running" && tab === "steps") { stepsKey = ""; focus(sel); } }
265
605
  }
266
- function accept(m){ model = m && m.requirements ? m : {requirements:[]}; draw(); }
606
+ function accept(m){ model = m && m.requirements ? m : {requirements:[]}; if (m && m.projectDir) projectDir = m.projectDir; matrixDirty = true; draw(); refreshMatrix(); }
267
607
 
608
+ Array.prototype.forEach.call(document.querySelectorAll(".tabs button"), function(b){
609
+ b.addEventListener("click", function(){ tab = b.dataset.tab; stepsKey = ""; draw(); });
610
+ });
611
+ $("vRun").addEventListener("click", function(){ setView("run"); });
612
+ $("vMatrix").addEventListener("click", function(){ setView("matrix"); });
613
+ $("mFilter").addEventListener("input", drawMatrix);
268
614
  fetch("live.json").then(function(x){ return x.json(); }).then(accept).catch(function(){});
269
615
  var es = new EventSource("live/stream");
270
616
  es.addEventListener("live", function(e){ try { accept(JSON.parse(e.data)); } catch (err) {} });
271
617
  es.onopen = function(){ $("conn").classList.add("on"); $("connText").textContent = "Live"; };
272
618
  es.onerror = function(){ $("conn").classList.remove("on"); $("connText").textContent = "Reconnecting"; };
273
619
  setInterval(tick, 1000);
274
- var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){ logKey = ""; draw(); }, 120); });
620
+ var t; window.addEventListener("resize", function(){ clearTimeout(t); t = setTimeout(function(){ stepsKey = ""; draw(); }, 120); });
275
621
  })();
276
622
  </script>
277
623
  </body></html>`;