@ifrankwang/openspec-agents 0.119.0 → 0.120.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.
@@ -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/内部使用) |
78
- | `bun run sync` | 同步本地最新开发版本到各 harness 插件缓存 |
91
+ | `bun run build:plugins` | 构建 Claude Code / Codex / ZCode / DeepSeek Harness 插件包(CI/内部使用) |
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 定义
@@ -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
  ```
@@ -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"