@ifrankwang/openspec-agents 0.119.1 → 0.120.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,295 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>编排进度看板 · 5 列看板</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
9
+ :root{
10
+ --bg:#f8fafc;--surface:#fff;--surface-2:#f1f5f9;
11
+ --ink:#0f172a;--muted:#475569;--quiet:#94a3b8;
12
+ --line:#e2e8f0;--line-2:#cbd5e1;
13
+ --blue:#2563eb;--blue-soft:#eff6ff;
14
+ --green:#16a34a;--green-soft:#f0fdf4;
15
+ --amber:#d97706;--amber-soft:#fffbeb;
16
+ --red:#dc2626;--red-soft:#fef2f2;
17
+ --violet:#7c3aed;--violet-soft:#f5f3ff;
18
+ --teal:#0d9488;--teal-soft:#f0fdfa;
19
+ --radius:12px;--radius-sm:8px;--pill:999px;
20
+ --shadow:0 1px 2px rgba(15,23,42,.04),0 6px 16px rgba(15,23,42,.04);
21
+ --sans:'Inter',system-ui,sans-serif;
22
+ --mono:'JetBrains Mono',ui-monospace,monospace;
23
+ }
24
+ *{box-sizing:border-box}html{font-size:15px}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.55;-webkit-font-smoothing:antialiased}
25
+ button{font:inherit;cursor:pointer}button:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
26
+ .shell{max-width:1440px;margin:0 auto;padding:32px 28px 80px}
27
+ /* header */
28
+ .topbar{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;padding-bottom:26px;border-bottom:1px solid var(--line)}
29
+ .brand{display:flex;flex-direction:column;gap:8px}
30
+ .badge{display:inline-flex;align-items:center;gap:8px;font:600 .68rem/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
31
+ .badge::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px var(--green-soft)}
32
+ h1{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-.03em}
33
+ .meta{display:flex;gap:28px;text-align:right;font-size:.8rem;color:var(--muted)}
34
+ .meta span{display:flex;flex-direction:column;gap:5px}.meta strong{color:var(--ink);font-weight:600}
35
+ /* 5 列看板 */
36
+ .board{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;align-items:start;padding-top:24px}
37
+ .col{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden;display:flex;flex-direction:column}
38
+ .col-head{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:14px 16px;border-bottom:1px solid var(--line)}
39
+ .col-head h3{margin:0;font-size:.92rem;font-weight:700}
40
+ .col-count{font:600 .78rem/1 var(--mono);padding:4px 10px;background:var(--surface-2);border-radius:var(--pill);color:var(--muted)}
41
+ .col-body{max-height:calc(100vh - 280px);overflow:auto;padding:12px;display:flex;flex-direction:column;gap:12px}
42
+ .board-empty{grid-column:1/-1;padding:48px 0;text-align:center;color:var(--quiet);font-size:.9rem}
43
+ /* 卡片 */
44
+ .card{position:relative;background:var(--surface);border:1px solid var(--line);border-left-width:3px;border-radius:var(--radius-sm);box-shadow:0 1px 2px rgba(15,23,42,.04);padding:12px 14px;display:flex;flex-direction:column;gap:8px}
45
+ .card.task{border-left-color:var(--blue)}
46
+ .card.issue{border-left-color:var(--violet)}
47
+ .card.suspended{border-style:dashed;opacity:.94}
48
+ .card-head{display:flex;align-items:center;gap:8px}
49
+ .type-tag{flex:0 0 auto;width:22px;height:22px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font:700 .7rem/1 var(--mono);color:#fff}
50
+ .type-tag.task{background:var(--blue)}.type-tag.issue{background:var(--violet)}
51
+ .src-icon{flex:0 0 auto;font-size:.85rem;color:var(--quiet);line-height:1}
52
+ .card-title{margin:0;font-size:.9rem;font-weight:700;line-height:1.4;flex:1}
53
+ .card-desc{font-size:.78rem;color:var(--muted);line-height:1.5}
54
+ .card-desc.collapsed{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
55
+ .desc-toggle{margin-top:-4px;font-size:.72rem;color:var(--blue);background:none;border:none;padding:0;cursor:pointer;text-align:left}
56
+ .card-labels{display:flex;flex-wrap:wrap;gap:6px}
57
+ .chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:var(--pill);font:500 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
58
+ .card-children{display:flex;flex-direction:column;gap:6px;border-top:1px dashed var(--line);padding-top:8px}
59
+ .child-row{display:flex;align-items:center;gap:6px;font-size:.76rem;color:var(--muted)}
60
+ .child-type{flex:0 0 auto;width:16px;height:16px;border-radius:4px;display:inline-flex;align-items:center;justify-content:center;font:700 .58rem/1 var(--mono);color:#fff}
61
+ .child-type.task{background:var(--blue)}.child-type.issue{background:var(--violet)}
62
+ .child-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
63
+ .child-phase{flex:0 0 auto;padding:1px 6px;border-radius:var(--pill);font:600 .62rem/1.3 var(--mono);background:var(--surface-2);color:var(--muted);border:1px solid var(--line)}
64
+ .card-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:8px;margin-top:auto;border-top:1px solid var(--line);padding-top:8px}
65
+ .card-verdicts{display:flex;flex-wrap:wrap;gap:6px}
66
+ .verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
67
+ .verdict.passed{background:var(--green-soft);color:var(--green)}
68
+ .verdict.failed{background:var(--red-soft);color:var(--red)}
69
+ .verdict.pending{background:var(--surface-2);color:var(--quiet)}
70
+ .card-meta{display:flex;align-items:center;gap:6px;flex-shrink:0}
71
+ .step-tag{font:.68rem/1.4 var(--mono);color:var(--quiet)}
72
+ .suspend-tag{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--pill);font:600 .7rem/1.4 var(--mono);background:var(--amber-soft);color:var(--amber);border:1px solid var(--amber);cursor:pointer}
73
+ .suspend-reason{margin-top:4px;font-size:.72rem;color:var(--amber);background:var(--amber-soft);border-radius:var(--radius-sm);padding:6px 8px}
74
+ /* severity 色条 */
75
+ .sev-bar{position:absolute;top:0;left:0;right:0;height:4px;border-radius:var(--radius-sm) var(--radius-sm) 0 0}
76
+ .sev-bar.critical{background:var(--red)}.sev-bar.high{background:var(--amber)}.sev-bar.medium{background:var(--violet)}.sev-bar.low{background:var(--teal)}.sev-bar.info{background:var(--line-2)}
77
+ /* WorkItem 详情弹窗 */
78
+ .modal-open{overflow:hidden}
79
+ .modal-overlay{position:fixed;inset:0;z-index:100;background:rgba(15,23,42,.45);display:flex;align-items:flex-start;justify-content:center;padding:48px 16px;overflow:auto}
80
+ .modal-overlay[hidden]{display:none}
81
+ .modal-dialog{background:var(--surface);border-radius:var(--radius);box-shadow:0 20px 50px rgba(15,23,42,.2);max-width:640px;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column}
82
+ .modal-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--line)}
83
+ .modal-title{flex:1;font-size:.92rem;font-weight:700}
84
+ .modal-close{background:none;border:none;font-size:1.2rem;color:var(--muted);line-height:1;padding:4px 8px;border-radius:6px}
85
+ .modal-close:hover{background:var(--surface-2);color:var(--ink)}
86
+ .modal-back{background:none;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.78rem;color:var(--muted);padding:4px 10px}
87
+ .modal-back:hover{background:var(--surface-2);color:var(--ink)}
88
+ .modal-back[hidden]{display:none}
89
+ .modal-body{padding:18px;overflow:auto;display:flex;flex-direction:column;gap:14px}
90
+ .detail-title{margin:0;font-size:1.1rem;font-weight:700}
91
+ .detail-block{display:flex;flex-direction:column;gap:8px}
92
+ .detail-block h5{margin:0;font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;color:var(--quiet)}
93
+ .meta-row{display:flex;gap:10px;font-size:.8rem;padding:4px 0;border-bottom:1px dashed var(--line)}
94
+ .meta-key{flex:0 0 96px;color:var(--quiet);font:500 .76rem/1.4 var(--mono)}
95
+ .meta-val{flex:1;color:var(--ink);word-break:break-word}
96
+ .detail-desc{margin:0;font-size:.82rem;color:var(--muted);white-space:pre-wrap}
97
+ .detail-verdicts{display:flex;flex-wrap:wrap;gap:6px}
98
+ .detail-verdict{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:var(--pill);font:600 .68rem/1.4 var(--mono);background:var(--surface-2);color:var(--muted)}
99
+ .detail-verdict.passed{background:var(--green-soft);color:var(--green)}
100
+ .detail-verdict.failed{background:var(--red-soft);color:var(--red)}
101
+ .detail-verdict.pending{background:var(--surface-2);color:var(--quiet)}
102
+ .detail-children{display:flex;flex-direction:column;gap:6px}
103
+ .detail-child{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font-size:.8rem;cursor:pointer}
104
+ .detail-child:hover{background:var(--surface-2);border-color:var(--line-2)}
105
+ @media(max-width:1024px){.board{grid-template-columns:repeat(2,1fr)}}
106
+ @media(max-width:640px){.board{grid-template-columns:1fr}.topbar{flex-direction:column;gap:14px}.meta{text-align:left}}
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="shell">
111
+ <header class="topbar">
112
+ <div class="brand">
113
+ <div class="badge" id="connBadge"><span>实时同步</span></div>
114
+ <h1>编排进度看板</h1>
115
+ </div>
116
+ <div class="meta">
117
+ <span>变更编号<strong id="changeId">-</strong></span>
118
+ <span>基准分支<strong id="baseBranch">-</strong></span>
119
+ <span>最后更新<strong id="updatedAt">-</strong></span>
120
+ </div>
121
+ </header>
122
+ <main id="dashboard"></main>
123
+ </div>
124
+ <div class="modal-overlay" id="wiModal" hidden>
125
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-label="工作项详情">
126
+ <header class="modal-head">
127
+ <button class="modal-back" id="wiBack" hidden>← 返回</button>
128
+ <span class="modal-title">工作项详情</span>
129
+ <button class="modal-close" id="wiClose" aria-label="关闭">×</button>
130
+ </header>
131
+ <div class="modal-body" id="wiBody"></div>
132
+ </div>
133
+ </div>
134
+ <script>
135
+ const COLUMNS=[['todo','待办'],['in_progress','处理中'],['review','审核中'],['done','完成'],['cancelled','已取消']];
136
+ const SOURCE_ICONS={openspec:'◈',ado:'◆',custom:'✚'};
137
+ const VERDICT_ICONS={passed:'✓',failed:'✕',pending:'○'};
138
+ const AGENT_SHORT={'openspec-architect':'architect','openspec-developer':'dev','openspec-reviewer-tool':'tool','openspec-reviewer-task':'task','openspec-reviewer-style':'style','openspec-reviewer-architecture':'arch','openspec-reviewer-performance':'perf','openspec-reviewer-security':'sec','openspec-reviewer-maintainability':'maint'};
139
+ let dashboardState={};
140
+ function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\'':'&#39;','"':'&quot;'}[c]))}
141
+ function cls(v){return String(v??'').replace(/[^a-zA-Z0-9_-]/g,'_')}
142
+ function shortAgent(key){if(AGENT_SHORT[key])return AGENT_SHORT[key];const m=/^openspec-reviewer-(.+)$/.exec(key)||/^openspec-(.+)$/.exec(key);return m?m[1]:(key||'')}
143
+ function descBlock(text){if(!text)return '';if(text.length<90)return `<div class="card-desc">${esc(text)}</div>`;return `<div class="card-desc collapsed">${esc(text)}</div><button class="desc-toggle">展开</button>`}
144
+ function verdictDot(v){return `<span class="verdict ${cls(v.verdict)}" title="${esc(v.stepId)}">${esc(shortAgent(v.agentKey))}:${VERDICT_ICONS[v.verdict]||'○'}</span>`}
145
+ function renderCard(c){
146
+ const isIssue=c.type==='issue';
147
+ const typeTag=`<span class="type-tag ${isIssue?'issue':'task'}">${isIssue?'I':'T'}</span>`;
148
+ const srcIcon=`<span class="src-icon" title="${esc(c.source||'')}">${SOURCE_ICONS[c.source]||'●'}</span>`;
149
+ const sevBar=isIssue&&c.severity?`<span class="sev-bar ${cls(String(c.severity).toLowerCase())}" title="${esc(c.severity)}"></span>`:'';
150
+ const labels=(c.labels||[]).map(l=>`<span class="chip">${esc(l)}</span>`).join('');
151
+ const step=c.currentStep?`<span class="step-tag">${esc(c.currentStep)}</span>`:'';
152
+ const suspTag=c.suspended?'<span class="suspend-tag" role="button" tabindex="0">⏸ 暂停</span>':'';
153
+ const suspReason=c.suspended?`<div class="suspend-reason" hidden>${esc(c.suspendReason||'未提供暂停原因')}</div>`:'';
154
+ const verdicts=(c.agentVerdicts||[]).map(verdictDot).join('');
155
+ const children=(c.children||[]).map(ch=>`<div class="child-row"><span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span><span class="child-title">${esc(ch.title)}</span>${ch.type==='task'?`<span class="child-phase">${esc(ch.phase)}</span>`:''}</div>`).join('');
156
+ return `<article class="card ${isIssue?'issue':'task'}${c.suspended?' suspended':''}" data-id="${esc(c.id)}">${sevBar}
157
+ <div class="card-head">${typeTag}${srcIcon}<h4 class="card-title">${esc(c.title)}</h4></div>
158
+ ${descBlock(c.description)}
159
+ ${labels?`<div class="card-labels">${labels}</div>`:''}
160
+ ${children?`<div class="card-children">${children}</div>`:''}
161
+ <div class="card-foot">${verdicts?`<div class="card-verdicts">${verdicts}</div>`:'<span></span>'}<div class="card-meta">${step}${suspTag}</div></div>
162
+ ${suspReason}
163
+ </article>`;
164
+ }
165
+ function renderBoard(){
166
+ const cards=dashboardState.workItemCards||[];
167
+ if(!cards.length)return `<section class="board"><div class="board-empty">暂无工作项数据(旧格式状态或未初始化)</div></section>`;
168
+ return `<section class="board">${COLUMNS.map(([key,label])=>{
169
+ const items=cards.filter(c=>c.phase===key);
170
+ return `<div class="col" data-col="${key}"><header class="col-head"><h3>${label}</h3><span class="col-count">${items.length}</span></header><div class="col-body">${items.map(renderCard).join('')}</div></div>`;
171
+ }).join('')}</section>`;
172
+ }
173
+ function render(){
174
+ document.getElementById('changeId').textContent=dashboardState.changeId||'-';
175
+ document.getElementById('baseBranch').textContent=dashboardState.baseBranch||'-';
176
+ document.getElementById('updatedAt').textContent=dashboardState.updatedAt?new Date(dashboardState.updatedAt).toLocaleString():'-';
177
+ document.getElementById('dashboard').innerHTML=renderBoard();
178
+ document.querySelectorAll('.desc-toggle').forEach(btn=>btn.addEventListener('click',e=>{
179
+ const d=e.target.previousElementSibling;
180
+ if(d&&d.classList.contains('card-desc')){d.classList.toggle('collapsed');e.target.textContent=d.classList.contains('collapsed')?'展开':'收起'}
181
+ }));
182
+ document.querySelectorAll('.suspend-tag').forEach(tag=>tag.addEventListener('click',e=>{
183
+ const card=e.target.closest('.card');
184
+ const reason=card&&card.querySelector('.suspend-reason');
185
+ if(reason)reason.hidden=!reason.hidden;
186
+ }));
187
+ if(modalState.id)refreshModal();
188
+ }
189
+ /* WorkItem 详情弹窗 */
190
+ let modalState={id:null,back:[]};
191
+ function findWorkItem(items,id){
192
+ for(const it of items||[]){
193
+ if(it.id===id)return it;
194
+ if(it.children&&it.children.length){
195
+ const found=findWorkItem(it.children,id);
196
+ if(found)return found;
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ function renderTags(tags){
202
+ const verdicts=Object.entries(tags||{}).map(([key,verdict])=>{
203
+ const sep=key.indexOf(':');
204
+ const stepId=sep===-1?key:key.slice(0,sep);
205
+ const agentKey=sep===-1?key:key.slice(sep+1);
206
+ return `<span class="detail-verdict ${cls(verdict)}" title="${esc(key)}">${esc(shortAgent(agentKey))}:${VERDICT_ICONS[verdict]||'○'}</span>`;
207
+ }).join('');
208
+ return verdicts?`<div class="detail-block"><h5>Agent 裁决</h5><div class="detail-verdicts">${verdicts}</div></div>`:'';
209
+ }
210
+ function renderWriteback(wb){
211
+ if(!wb)return '';
212
+ const rows=Object.entries(wb).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
213
+ const val=typeof v==='object'?JSON.stringify(v):String(v);
214
+ return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
215
+ }).join('');
216
+ return rows?`<div class="detail-block"><h5>写回</h5>${rows}</div>`:'';
217
+ }
218
+ function renderMeta(metadata){
219
+ const rows=Object.entries(metadata||{}).filter(([k])=>!k.startsWith('_')).map(([k,v])=>{
220
+ const val=typeof v==='object'?JSON.stringify(v):String(v);
221
+ return `<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(val)}</span></div>`;
222
+ }).join('');
223
+ return rows?`<div class="detail-block"><h5>元数据</h5>${rows}</div>`:'';
224
+ }
225
+ function renderChildren(children){
226
+ if(!children||!children.length)return '';
227
+ const rows=(children||[]).map(ch=>`<div class="detail-child" data-id="${esc(ch.id)}" role="button" tabindex="0">
228
+ <span class="child-type ${cls(ch.type)}">${ch.type==='issue'?'I':'T'}</span>
229
+ <span class="child-title">${esc(ch.title)}</span>
230
+ <span class="child-phase">${esc(ch.phase)}</span>
231
+ </div>`).join('');
232
+ return `<div class="detail-block"><h5>Children</h5><div class="detail-children">${rows}</div></div>`;
233
+ }
234
+ function renderDetail(item){
235
+ const rows=[['类型',item.type],['来源',item.source],['ID',item.id]];
236
+ if(item.externalId)rows.push(['外部ID',item.externalId]);
237
+ rows.push(['阶段',item.phase]);
238
+ if(item.severity)rows.push(['严重级别',item.severity]);
239
+ rows.push(['当前步骤',item.currentStep||'-']);
240
+ const kv=rows.map(([k,v])=>`<div class="meta-row"><span class="meta-key">${esc(k)}</span><span class="meta-val">${esc(v)}</span></div>`).join('');
241
+ return `<h3 class="detail-title">${esc(item.title)}</h3>
242
+ ${item.suspended?`<div class="suspend-reason">⏸ 暂停:${esc(typeof (item.metadata||{})['suspend_reason']==='string'?(item.metadata||{})['suspend_reason']:'未提供暂停原因')}</div>`:''}
243
+ ${item.description?`<div class="detail-block"><h5>描述</h5><p class="detail-desc">${esc(item.description)}</p></div>`:''}
244
+ <div class="detail-block"><h5>字段</h5>${kv}</div>
245
+ ${item.labels&&item.labels.length?`<div class="detail-block"><h5>标签</h5><div class="card-labels">${item.labels.map(l=>`<span class="chip">${esc(l)}</span>`).join('')}</div></div>`:''}
246
+ ${renderTags(item.tags)}
247
+ ${renderWriteback(item.writeback)}
248
+ ${renderMeta(item.metadata)}
249
+ ${renderChildren(item.children)}`;
250
+ }
251
+ function refreshModal(){
252
+ const modal=document.getElementById('wiModal');
253
+ const item=findWorkItem(dashboardState.workItems||[],modalState.id);
254
+ if(!item){closeDetail();return;}
255
+ document.getElementById('wiBody').innerHTML=renderDetail(item);
256
+ document.getElementById('wiBack').hidden=!modalState.back.length;
257
+ modal.hidden=false;
258
+ document.body.classList.add('modal-open');
259
+ }
260
+ function openDetail(id,back){
261
+ modalState={id,back:back||[]};
262
+ refreshModal();
263
+ }
264
+ function closeDetail(){
265
+ modalState={id:null,back:[]};
266
+ const modal=document.getElementById('wiModal');
267
+ if(modal)modal.hidden=true;
268
+ document.body.classList.remove('modal-open');
269
+ }
270
+ document.addEventListener('click',e=>{
271
+ if(e.target.closest('.desc-toggle,.suspend-tag'))return;
272
+ const card=e.target.closest('.card');
273
+ if(card&&card.dataset.id)openDetail(card.dataset.id,[]);
274
+ });
275
+ document.getElementById('wiModal').addEventListener('click',e=>{
276
+ if(e.target.closest('#wiBack')){
277
+ const prev=modalState.back[modalState.back.length-1];
278
+ openDetail(prev,modalState.back.slice(0,-1));
279
+ return;
280
+ }
281
+ if(e.target.classList.contains('modal-overlay')){closeDetail();return;}
282
+ if(e.target.closest('.modal-close')){closeDetail();return;}
283
+ const child=e.target.closest('.detail-child');
284
+ if(child&&child.dataset.id)openDetail(child.dataset.id,[...modalState.back,modalState.id]);
285
+ });
286
+ document.addEventListener('keydown',e=>{
287
+ if(e.key==='Escape'){const modal=document.getElementById('wiModal');if(modal&&!modal.hidden)closeDetail();}
288
+ });
289
+ async function loadState(){try{const res=await fetch('/api/state');if(!res.ok)throw new Error('fetch failed');const data=await res.json();if(data&&data.active===true){dashboardState=data;render()}}catch(e){console.error('[dashboard] load state failed',e)}}
290
+ render();
291
+ loadState();
292
+ setInterval(loadState,2000);
293
+ </script>
294
+ </body>
295
+ </html>
package/README.md CHANGED
@@ -15,7 +15,7 @@ OpenSpec 把需求、设计、任务拆解成规范文档后,实施阶段仍
15
15
 
16
16
  ## 特色
17
17
 
18
- - **Agent 无关**:内核不绑定具体 Agent,opencode / claude code / codex / zcode 通过各自原生插件/适配器接入同一套状态机。
18
+ - **Agent 无关**:内核不绑定具体 Agent,opencode / claude code / codex / zcode / deepseek harness 通过各自原生插件/适配器接入同一套状态机。
19
19
  - **Workflow 驱动**:步骤、角色、门禁在 workflow 中声明,`opx_status` 是下一步调度的唯一事实源。
20
20
  - **多 Agent 团队**:主代理编排,子代理按角色分工,避免单一大模型从头写到尾失控。
21
21
  - **强 Review 门禁**:tool / task / quality 三层审查 + 收尾验证,问题可回退、可豁免、可检查点决策。
@@ -68,20 +68,34 @@ bun run typecheck
68
68
 
69
69
  - **ZCode**:在 ZCode 插件页添加市场 `ifrankwang/zcode-plugins`,然后安装 `openspec-agents`。
70
70
 
71
+ - **DeepSeek Harness(DSH)**:通过 DSH 的 bundle 插件机制接入。发布到 npm 后可直接安装:
72
+ ```bash
73
+ dsh plugin --profile web add @ifrankwang/openspec-agents
74
+ ```
75
+ 本地开发时也可使用构建产物:
76
+ ```bash
77
+ bun run build:plugins
78
+ dsh plugin --profile web add ./dist/deepseek-harness-plugin
79
+ ```
80
+ 安装后重启 `dsh web`:
81
+ - `opx_*` 会以 `mcp__opx__*`(如 `mcp__opx__status`、`mcp__opx__agent_submit`) 原生工具出现;
82
+ - `assets/skills` 会作为额外 skill 根被扫描;
83
+ - 每个子代理会注册为 DSH 原生 subagent 工具,例如 `openspec_architect`、`openspec_developer`、`openspec_reviewer_tool`、`openspec_reviewer_task`、`openspec_reviewer_architecture` 等,编排主代理可直接分派。
84
+
71
85
  ## 常用命令
72
86
 
73
87
  | 命令 | 说明 |
74
88
  |------|------|
75
89
  | `bun test` | 运行所有测试 |
76
90
  | `bun run typecheck` | TypeScript 类型检查 |
77
- | `bun run build:plugins` | 构建 Claude Code / Codex / ZCode 插件包(CI/内部使用) |
91
+ | `bun run build:plugins` | 构建 Claude Code / Codex / ZCode / DeepSeek Harness 插件包(CI/内部使用) |
78
92
  | `bun run sync` | 同步本地最新开发版本到各 harness 插件缓存,并安装/刷新依赖 |
79
93
 
80
94
  ## 项目结构
81
95
 
82
96
  ```
83
97
  src/core/ — Agent 无关内核(状态机、工具执行器、状态持久化)
84
- src/adapters/ — opencode / claude-code / codex / zcode / mcp 适配层
98
+ src/adapters/ — opencode / claude-code / codex / zcode / deepseek-harness / mcp 适配层
85
99
  assets/agents/ — Agent 定义
86
100
  assets/skills/ — 内置 skill(含 orchestrator)
87
101
  assets/workflows/ — workflow 定义
@@ -125,6 +125,10 @@ PMD 违规输出格式:
125
125
 
126
126
  `target/pmd.xml` 被 `<suppressedviolation>` 污染(全量规则集 + 抑制配置时)时,改用自定义 ruleset 生成的干净报告,或读取 `mvn pmd:check` 非 -q 形态 console 输出中的 `[WARNING] PMD Failure` 行。
127
127
 
128
+ ### 覆盖范围
129
+
130
+ PMD 全量报告中的每条违规均须纳入问题清单,不因文件不在本次修改范围或经基线对比确认为存量而省略。
131
+
128
132
  ### 测量与复现
129
133
 
130
134
  pom 中 maven-pmd-plugin 显式配置的 `<rulesets>` 会覆盖命令行 `-Dpmd.rulesets`,修改生效规则集须改 pom 配置,勿用命令行参数覆盖。
@@ -10,6 +10,29 @@ capabilities: ["orchestrator"]
10
10
 
11
11
  你不亲自修改代码、不亲自审查、不亲自运行测试,也不向子代理转述动态上下文——所有子代理通过状态查询工具按角色路由自行获取所需上下文。
12
12
 
13
+ ## 工具接入形态(MCP 通用)
14
+
15
+ - 在 DeepSeek Harness(DSH)、Claude Code、Codex、ZCode 等经 MCP 接入的形态下,本流程的 `opx_*` 工具统一以 `mcp__opx__*` 形式出现(serverName=opx,且已去掉 `opx_` 前缀),例如:
16
+ - `mcp__opx__status`
17
+ - `mcp__opx__agent_submit`
18
+ - `mcp__opx__orch_init`
19
+ - 实际前缀以当前工具列表为准
20
+ - 若当前工具列表里没有这些 MCP 工具,只有 `dev_tool_search`(DSH 受限工具目录常见),必须先调用 `dev_tool_search` 搜索并解锁:
21
+ 1. 搜索关键词直接用 `opx`(不要用 `orchestrator`、`workflow` 等模糊词);
22
+ 2. 在同一轮 `dev_tool_search` 中一次性解锁全部需要的工具,包括全部 `mcp__opx__*`、全部 `openspec_*` 子代理工具,以及计划使用的 `todo_write` 等常用工具;不要分多次解锁,避免反复失效 KV Cache;
23
+ 3. 解锁后这些工具会从下一轮请求开始出现在工具列表。
24
+ - 子代理工具也需一并解锁:搜索 `agent` 获取全部 `openspec_*` 后,与 `mcp__opx__*` 一起一次性 `dev_tool_search({"toolNames": [...]})` 解锁。
25
+ - 解锁完成前禁止用 `find`/`cat`/`grep` 探查仓库或 MCP 配置;先按工具链调用 status/init。
26
+ - 调用时直接使用工具列表中的完整名称,不要因为带 `mcp__...__` 前缀而误认为不是本流程工具。
27
+ - 启动编排的常规入口(以工具实际可用为准):
28
+ 1. 先调用状态查询工具(如 `mcp__opx__status` 或列表中对应的 status 工具)查看当前是否已初始化 / worktree 是否就绪;
29
+ 2. 若未初始化,调用初始化工具(如 `mcp__opx__orch_init`)传入 `change_id` / `task_group_id`;
30
+ 3. 若 worktree 未就绪,调用 `mcp__opx__orch_set_worktree`(或列表中对应工具);
31
+ 4. 再次调用状态查询工具获取权威「下一步」并严格按返回执行。
32
+ - 子代理工具命名随 harness 不同:
33
+ - DSH 形态:`openspec_<role>`(如 `openspec_architect`、`openspec_developer`、`openspec_reviewer_tool`),分派时直接调用这些专用工具,不要使用通用 `subagent` 代替;
34
+ - 其他 harness 按各自原生子代理机制分派(Claude Code / Codex / ZCode 插件已注入对应子代理)。
35
+
13
36
  ## 行为准则
14
37
 
15
38
  - 每次子代理返回后,调用状态查询工具获取权威「下一步」指令并严格遵循;推进被拦时按工具给出的阻塞原因与修复建议处理,不自行推断其他动作
@@ -19,6 +42,7 @@ capabilities: ["orchestrator"]
19
42
  - 断点续传:子代理因步骤限制中断后重新分派即可继续,无需保存已完成子任务列表
20
43
  - 工具列出多个子代理时并排分派(单条消息中同时发送),不串行等待
21
44
  - 分派前校验分派 prompt:按「禁止事项」中禁止转述的动态内容清单逐项检查,校验通过后再分派
45
+ - 先解锁、后 status、再探索;禁止在首次 status 前手动探查项目文件/状态文件
22
46
 
23
47
  ## 禁止事项
24
48
 
@@ -34,7 +58,7 @@ capabilities: ["orchestrator"]
34
58
 
35
59
  ## 分派范式
36
60
 
37
- 分派子代理时,prompt 仅限以下模板,除 `_agent` 身份指令外严禁增减内容:
61
+ 分派子代理时,prompt 仅限以下模板,除 `_agent` 身份指令和 MCP 工具接入提示外严禁增减内容:
38
62
 
39
63
  ```
40
64
  ## 任务:<动词> — 任务组 <id> <轮次> — Change: <changeId>
@@ -45,6 +69,14 @@ capabilities: ["orchestrator"]
45
69
  按本 agent md 中定义的规范执行,完成后调用 `opx_agent_submit` 提交。
46
70
  ```
47
71
 
72
+ MCP 接入形态下,将模板中的 `opx_status` / `opx_agent_submit` 替换为实际工具列表中的完整名称(如 `mcp__opx__status` / `mcp__opx__agent_submit`,具体以当前工具列表为准)。
73
+
74
+ 当子代理也经 MCP 接入且工具列表中可能没有 `mcp__opx__*` 时,应在分派 prompt 末尾追加一行静态提示:
75
+
76
+ ```
77
+ 若工具列表中暂无 mcp__opx__*,先 dev_tool_search 一次性解锁 ["mcp__opx__status", "mcp__opx__agent_submit"],不要用 shell 搜索。
78
+ ```
79
+
48
80
  子代理经 MCP 接入(非 OpenCode 直载形态)时,模板末尾追加一行 `_agent` 身份指令——角色名以分派指令中的 agent 名为准;不追加时子代理首次调用 `opx_status` 会按编排主代理视角返回,永远拿不到自身角色的执行视图:
49
81
 
50
82
  ```
@@ -42,6 +42,10 @@ must_do: ["env", "compile", "format", "architecture", "static_analysis", "unit_t
42
42
  - **通过**:正常记录结果
43
43
  - **不通过**:生成统一结构 issue,阻塞 task 完成
44
44
 
45
+ ## 全量检查覆盖范围
46
+
47
+ 执行全量确定性检查时,所有工具输出均须纳入检查结果与问题清单,不因文件不在本次变更范围或问题为存量而省略;存量问题按原严重级别处理。
48
+
45
49
  ## 失败定位
46
50
 
47
51
  构建/检查失败时,失败的完整输出文本是定位全部失败项的唯一可靠依据;仅依据报错尾部数行做出修复决策会导致全量问题被遗漏并触发反复重试,视为不合规的故障处理方式。
@@ -11,6 +11,8 @@ max_retries: 10
11
11
  common:
12
12
  instructions:
13
13
  - "非 OpenCode 直载形态(经 MCP 接入的 agent 客户端)调用 opx_* 工具时,须携带 `_agent` 参数并传自身角色名(如 openspec-developer);缺省会被视为编排主代理视角,将拿不到自身角色的执行视图与操作指引"
14
+ - "若当前工具列表中的编排工具带有 `mcp__<server>__` 前缀(MCP 接入形态,如 DeepSeek Harness / Claude Code / Codex / ZCode),调用时使用工具列表中的完整名称,例如 `mcp__opx__status`、`mcp__opx__agent_submit`;若工具列表中没有这些工具且存在 `dev_tool_search`,先用 `dev_tool_search` 搜索 `opx` 并一次性解锁全部需要的工具(含 `todo_write`);禁止用 shell 查找 opx/openspec/MCP 配置"
15
+ - "首次调用 opx_status 前不要探查仓库结构或状态文件;工具未解锁时只用 dev_tool_search 解锁,解锁后立即调用 opx_status"
14
16
  - "调用 opx_status 获取动态上下文({{worktree_path}}、变更范围、上轮会话摘要)"
15
17
  - "按 Skill 加载清单加载匹配的 skill 并遵循其全部规范"
16
18
  - "读取 `{{worktree_path}}/openspec/changes/{{change_id}}/` 下的相关文档:tasks.md(任务清单,按行内 `[spec:xxx]` 溯源定位 specs/ 下相关 spec.md)、design.md、proposal.md"
@@ -22,7 +24,7 @@ common:
22
24
  - "禁止为获取外部框架/工具的规则语义、API 用法等知识而解析其源码、jar 包或执行 AST dump;此类知识一律通过文档检索渠道获取;声明式规则定义来源(规则定义 XML 直拉、工具官方 Web API 返回、官方文档页)属于文档检索渠道范畴"
23
25
  - "不跳步骤、不自行推断阶段流转,next-step 以 opx_status 为准"
24
26
  - "禁止调用任何 opx_orch_* 工具(编排者专属)"
25
- - "以视图「变更范围」命令产出的文件清单作为本次工作范围"
27
+ - "以视图「变更范围」命令产出的文件清单作为本次实施/修改范围;审查/报告范围按各 step 指令执行"
26
28
 
27
29
  phases:
28
30
  - name: todo
@@ -94,7 +96,6 @@ phases:
94
96
  - "依据 opx_status 提供的变更检测结果分流:无代码/配置变更且本层无待复核/待裁定项 → 直接提交 passed,不运行全量工具检查,并以一条 step 名首段为 no_change 的 validation_steps 声明本轮整体豁免必做清单(completed=false + 结构化 skip_reason 注明判定依据);无变更但有本层待复核/待裁定项 → 仅复核/裁定待处理项后提交,不跑全量工具扫描;有变更 → 加载质量门类 skill 获取工具清单与映射规则,按已加载质量门 skill 必做清单逐项执行全部确定性工具检查(代码格式/架构约束/静态分析/单元测试编译/深度扫描——按必做清单中的对应项逐项执行并逐项申报),将工具输出映射为统一 issue 结构后提交"
95
97
  - "「有变更」分支先审查上方「本次变更证据」区块的变更文件清单与区间 diff 内容后自行判断:若确认本次变更仅为注释/文档性且无逻辑影响(非注释掉代码、非结构性/格式化变更、非文件重命名),可自行决定跳过全量工具检查直接提交 passed(validation_steps 以 step=no_change 申报整体豁免必做清单);无法确认或含逻辑/结构变更,必须顺序运行全部确定性工具检查;裁量仅免除全量工具扫描,不免除复核/裁定——本层 blocking issue 仍须处理至终态后提交"
96
98
  - "报 issue 时必须显式声明归因维度 dimension(style/architecture/performance/security/maintainability):本 reviewer 不归属任一质量维度,dimension 即该 issue 归属的目标质量维度"
97
- - "无变更直提分支不执行全量工具扫描,该分支下存量工具违规不重复扫描;实质代码/配置变更必须按全量覆盖:工具检查覆盖全量代码,非本轮变更文件的工具违规同样映射为 issue 提交,禁止因非本轮引入静默丢弃;仅注释/文档性变更(经审查确认)可免全量工具检查;命中项目级豁免清单(视图标注)的存量问题按 Info 提报,不重新豁免,其余非本轮引入问题仍按原严重级别上报"
98
99
  - "当发现修复需要调整 openspec/changes/ 下文档时,所报 issue 的 file 字段必须指向目标文档"
99
100
  - "复核本层已修复待复核(review 态)issue:通过置 done,不通过驳回(附驳回原因,issue 回待修复并累计修复未过次数)"
100
101
  - "对视图「待裁定是否可豁免」区块中的豁免申请经 exempt_adjudications 裁定(dismissed/rejected)"
@@ -107,6 +108,7 @@ phases:
107
108
  - "即使无 issue 也必须提交 verdict=passed(不通过必须有至少一个 Low+ issue 作为理由)"
108
109
  - "Info 级 issue 的 description/suggestion 禁止阶段/时机表述(如'可后续处理')"
109
110
  - "禁止以 SonarQube 平台状态修改(SAFE/WONTFIX/False Positive/Confirmed 等)作为豁免手段:平台状态仅作用于当前 SonarQube 项目实例,每次 change 使用独立项目 key,不跨 change 生效;存量问题跨 change 豁免必须经 exempt_issue_ids 申请 → 报源 reviewer 裁定 dismissed 的流程"
111
+ - "有实质变更时,工具检查及 issue 报告覆盖全量代码;非本轮变更文件的工具违规同样必须映射为 issue(命中项目级豁免清单的按 Info,不重新豁免;其余非本轮引入问题按原严重级别上报),不得以不在变更清单或未越界修改为由丢弃"
110
112
  - id: verify_task
111
113
  agents:
112
114
  - id: openspec-reviewer-task