@minonl/dia 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cli.js +1966 -270
- package/docs/builtin-kit-design.md +65 -0
- package/docs/cli.md +3 -2
- package/docs/design/graph-prio-dag.md +85 -0
- package/docs/design/graph-ui-m1.md +106 -0
- package/docs/design/graph-web-m1.md +113 -0
- package/docs/design/graph-web-m2.md +92 -0
- package/docs/design/graph-web-mockup.html +49 -25
- package/docs/design/graph-web.md +9 -4
- package/docs/design/seat-scheduling-m2.md +138 -0
- package/docs/index.md +9 -1
- package/docs/install-guard-design.md +148 -0
- package/docs/kit-install.md +7 -0
- package/docs/kit-layer.md +78 -0
- package/docs/migrations/data-root-2026-11.md +3 -2
- package/docs/status.md +3 -0
- package/kits/git/dia.json +10 -0
- package/kits/git/knowledge/hooks.md +30 -0
- package/kits/git/knowledge/worktree.md +34 -0
- package/kits/node/dia.json +10 -0
- package/kits/node/knowledge/hook-managers.md +17 -0
- package/kits/node/knowledge/pnpm.md +187 -0
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +15 -0
- package/packages/wt/tests/adopt.test.sh +8 -0
|
@@ -32,14 +32,18 @@
|
|
|
32
32
|
.seg button.on{background:var(--blue);color:#fff}
|
|
33
33
|
.seg button:disabled{opacity:.35;cursor:not-allowed}
|
|
34
34
|
.win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
|
|
35
|
-
.search{position:relative}
|
|
36
|
-
.search input{border:
|
|
37
|
-
.
|
|
35
|
+
.search{position:relative;display:flex;align-items:center;gap:4px;flex-wrap:wrap;border:1px solid var(--line);background:var(--bg);border-radius:8px;padding:4px 8px;min-width:240px;max-width:420px;cursor:text}
|
|
36
|
+
.search input{border:0;background:transparent;color:var(--ink);padding:2px 0;font:inherit;font-size:12px;flex:1;min-width:80px;outline:none}
|
|
37
|
+
.token{display:inline-flex;align-items:center;gap:4px;background:color-mix(in srgb,var(--blue) 22%,transparent);color:var(--blue);border-radius:999px;padding:2px 4px 2px 8px;font-size:11px;white-space:nowrap}
|
|
38
|
+
.token button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:0 2px}
|
|
39
|
+
.search .q-list{position:absolute;left:0;top:calc(100% + 4px);z-index:20;width:300px;max-height:250px;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);display:none}
|
|
38
40
|
.search .q-list.open{display:block}
|
|
39
41
|
.q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
40
42
|
.q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
|
|
41
43
|
.q-item .k{color:var(--muted)}
|
|
42
44
|
.q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
|
|
45
|
+
.q-item.sel{color:var(--blue)}
|
|
46
|
+
.q-item.sel .k{color:var(--blue)}
|
|
43
47
|
.q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
|
|
44
48
|
.q-empty{padding:10px;font-size:12px;color:var(--muted)}
|
|
45
49
|
.node.hit{outline:2px solid var(--blue);outline-offset:1px}
|
|
@@ -181,11 +185,12 @@
|
|
|
181
185
|
<button class="on" data-m="simple">简约</button>
|
|
182
186
|
<button data-m="detail">详细</button>
|
|
183
187
|
</div>
|
|
184
|
-
<div class="search">
|
|
185
|
-
<
|
|
188
|
+
<div class="search" id="search-box">
|
|
189
|
+
<span id="tokens" style="display:contents"></span>
|
|
190
|
+
<input id="q" type="search" placeholder="搜索任务,选中后变成 tag" autocomplete="off">
|
|
186
191
|
<div class="q-list" id="q-list"></div>
|
|
187
192
|
</div>
|
|
188
|
-
<span class="focus-chip" id="focus-chip"><
|
|
193
|
+
<span class="focus-chip" id="focus-chip"><select id="focus-fd" class="fd"><option value="1">1 层</option><option value="2" selected>2 层</option><option value="all">全链路</option></select><button id="focus-exit">清空</button></span>
|
|
189
194
|
</div>
|
|
190
195
|
<div id="time-tools" style="display:none;gap:8px;align-items:center">
|
|
191
196
|
<div class="seg" id="zoom">
|
|
@@ -281,7 +286,7 @@ for (let i=0;i<100;i++){
|
|
|
281
286
|
const startD = -15 + ((i*7)%24);
|
|
282
287
|
const endD = Math.min(15, startD + 1 + ((i*3)%8));
|
|
283
288
|
const id = 't-'+String(i+1).padStart(3,'0');
|
|
284
|
-
const deps = i
|
|
289
|
+
const back=(i%5)+1, deps = i-back>=0 ? ['t-'+String(i-back).padStart(3,'0')] : [];
|
|
285
290
|
tasks.push({id, label:modules[i%modules.length]+' '+String(i+1).padStart(3,'0'), status, prio, progress, start:startD, end:endD, owner:i%3===0?null:(i%2?'ses_a':'ses_b'), deps});
|
|
286
291
|
}
|
|
287
292
|
const pad=n=>'t-'+String(n+1).padStart(3,'0');
|
|
@@ -292,7 +297,7 @@ for(let i=0;i<20;i++) taskEdges.push({from:tasks[i*5].id,to:paths[i%paths.length
|
|
|
292
297
|
for(let i=0;i<12;i++) taskEdges.push({from:i%2?'ses_a':'ses_b',to:paths[i%paths.length],kind:'claims'});
|
|
293
298
|
taskEdges.push({from:'t-001',to:'t-002',kind:'collaborates_with'});
|
|
294
299
|
|
|
295
|
-
const state={view:'graph', layout:'priority', mode:'simple', range:'30d', zoom:28, window:7, q:'',
|
|
300
|
+
const state={view:'graph', layout:'priority', mode:'simple', range:'30d', zoom:28, window:7, q:'', sel:[], fd:2, from:null, to:null};
|
|
296
301
|
const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
|
|
297
302
|
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
298
303
|
const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○'};
|
|
@@ -313,7 +318,11 @@ function focusSet(id,depth){
|
|
|
313
318
|
return out;
|
|
314
319
|
}
|
|
315
320
|
function visibleTasks(){
|
|
316
|
-
if(state.
|
|
321
|
+
if(state.sel.length){
|
|
322
|
+
const keep=new Set();
|
|
323
|
+
state.sel.forEach(id=>focusSet(id,state.fd).forEach(x=>keep.add(x)));
|
|
324
|
+
return tasks.filter(t=>keep.has(t.id));
|
|
325
|
+
}
|
|
317
326
|
return tasks.filter(t=>state.window<=0 || t.start>=-state.window);
|
|
318
327
|
}
|
|
319
328
|
function searchMatches(){
|
|
@@ -332,7 +341,7 @@ function syncStats(){
|
|
|
332
341
|
function updateHint(){
|
|
333
342
|
const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
|
|
334
343
|
const n=visibleTasks().length;
|
|
335
|
-
if(state.
|
|
344
|
+
if(state.sel.length){
|
|
336
345
|
h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
|
|
337
346
|
return;
|
|
338
347
|
}
|
|
@@ -403,7 +412,7 @@ function card(t,x,y,showPrio=false){
|
|
|
403
412
|
(state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
|
|
404
413
|
el.dataset.id=t.id;
|
|
405
414
|
el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
|
|
406
|
-
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer);
|
|
415
|
+
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); toggleSel(t.id); };
|
|
407
416
|
return el;
|
|
408
417
|
}
|
|
409
418
|
function edgePath(a,b){
|
|
@@ -609,16 +618,19 @@ function renderSearchList(ms){
|
|
|
609
618
|
const box=document.getElementById('q-list'); if(!box) return;
|
|
610
619
|
const q=(state.q||'').trim();
|
|
611
620
|
const browse=!q;
|
|
612
|
-
const pool=browse?
|
|
621
|
+
const pool=browse?tasks.filter(t=>state.window<=0||t.start>=-state.window).slice().sort((a,b)=>a.prio.localeCompare(b.prio)||a.start-b.start):ms;
|
|
613
622
|
if(!pool||!pool.length){
|
|
614
623
|
box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
|
|
615
624
|
box.classList.add('open'); return;
|
|
616
625
|
}
|
|
617
|
-
const head=browse?('<div class="q-head"
|
|
618
|
-
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter
|
|
619
|
-
box.innerHTML=head+pool.slice(0,20).map(t=>
|
|
626
|
+
const head=browse?('<div class="q-head">'+pool.length+' 个任务 · ↑↓ 选择,Enter 加入</div>')
|
|
627
|
+
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 加入</div>');
|
|
628
|
+
box.innerHTML=head+pool.slice(0,20).map(t=>{
|
|
629
|
+
const on=state.sel.includes(t.id);
|
|
630
|
+
return '<div class="q-item'+(on?' sel':'')+'" data-id="'+esc(t.id)+'"><span>'+esc(t.label)+'<i class="q-prio">'+esc(t.prio)+'</i></span><span class="k">'+(on?'✓ ':'')+esc(t.id)+'</span></div>';
|
|
631
|
+
}).join('');
|
|
620
632
|
box.classList.add('open');
|
|
621
|
-
box.querySelectorAll('.q-item').forEach(el=>el.onclick=()
|
|
633
|
+
box.querySelectorAll('.q-item').forEach(el=>el.onclick=e=>{ e.stopPropagation(); toggleSel(el.dataset.id); });
|
|
622
634
|
}
|
|
623
635
|
function searchListItems(){
|
|
624
636
|
const box=document.getElementById('q-list');
|
|
@@ -632,11 +644,18 @@ function moveSearchCursor(delta){
|
|
|
632
644
|
items.forEach((el,i)=>el.classList.toggle('active',i===idx));
|
|
633
645
|
items[idx].scrollIntoView({block:'nearest'});
|
|
634
646
|
}
|
|
635
|
-
function
|
|
636
|
-
|
|
647
|
+
function toggleSel(id){
|
|
648
|
+
const i=state.sel.indexOf(id);
|
|
649
|
+
if(i>=0) state.sel.splice(i,1); else state.sel.push(id);
|
|
650
|
+
state.fd=state.fd||2; state.q='';
|
|
637
651
|
const qi=document.getElementById('q'); if(qi) qi.value='';
|
|
638
652
|
go(state.view,state.mode,state.range,state.zoom);
|
|
639
653
|
}
|
|
654
|
+
function renderTokens(){
|
|
655
|
+
const box=document.getElementById('tokens'); if(!box) return;
|
|
656
|
+
box.innerHTML=state.sel.map(id=>'<span class="token">'+esc(id)+'<button data-id="'+esc(id)+'" title="移除">×</button></span>').join('');
|
|
657
|
+
box.querySelectorAll('button').forEach(b=>b.onclick=e=>{ e.stopPropagation(); toggleSel(b.dataset.id); });
|
|
658
|
+
}
|
|
640
659
|
function readRoute(){
|
|
641
660
|
const raw=location.hash.replace(/^#\/?/,''); const [path,qs]=raw.split('?');
|
|
642
661
|
const params=new URLSearchParams(qs||location.search);
|
|
@@ -650,7 +669,7 @@ function readRoute(){
|
|
|
650
669
|
const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
|
|
651
670
|
const fdRaw=params.get('fd');
|
|
652
671
|
const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
|
|
653
|
-
return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'',
|
|
672
|
+
return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'', sel:(params.get('sel')||params.get('focus')||'').split(',').map(x=>x.trim()).filter(x=>x&&tasks.some(t=>t.id===x)), fd, from:toOff(fromRaw), to:toOff(toRaw), task:params.get('task'), tab:params.get('tab')==='intervene'?'intervene':'task'};
|
|
654
673
|
}
|
|
655
674
|
function routeString(view,mode,range,zoom,from,to){
|
|
656
675
|
const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
|
|
@@ -659,7 +678,7 @@ function routeString(view,mode,range,zoom,from,to){
|
|
|
659
678
|
if(view==='graph'){
|
|
660
679
|
if(state.window!==7) qs.set('w',String(state.window));
|
|
661
680
|
if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
|
|
662
|
-
if(state.
|
|
681
|
+
if(state.sel.length){ qs.set('sel',state.sel.join(',')); if(state.fd!==2) qs.set('fd',String(state.fd)); }
|
|
663
682
|
}
|
|
664
683
|
if(view==='time'){
|
|
665
684
|
if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
|
|
@@ -669,7 +688,7 @@ function routeString(view,mode,range,zoom,from,to){
|
|
|
669
688
|
return '#/'+p+(qs.toString()?'?'+qs.toString():'');
|
|
670
689
|
}
|
|
671
690
|
function applyRoute(){
|
|
672
|
-
const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.
|
|
691
|
+
const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.sel=r.sel; state.fd=r.fd; state.from=r.from; state.to=r.to;
|
|
673
692
|
document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
|
|
674
693
|
document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
|
|
675
694
|
document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
|
|
@@ -683,8 +702,10 @@ function applyRoute(){
|
|
|
683
702
|
const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
|
|
684
703
|
const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
|
|
685
704
|
const chip=document.getElementById('focus-chip');
|
|
686
|
-
if(chip)
|
|
705
|
+
if(chip) chip.classList.toggle('on',r.sel.length>0);
|
|
706
|
+
if(chip) chip.title='按依赖邻域聚焦;清空即回全量';
|
|
687
707
|
const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
|
|
708
|
+
renderTokens();
|
|
688
709
|
const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
|
|
689
710
|
document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
|
|
690
711
|
if(r.view==='time'){ drawTimeline(); const sc=document.getElementById('tl-scroll'), lb=document.getElementById('tl-labels-body'); sc.scrollTop=0; lb.style.transform='translateY(0px)'; if(state.sx) sc.scrollLeft=state.sx; } else drawGraph();
|
|
@@ -705,17 +726,20 @@ if(qInput){
|
|
|
705
726
|
else if(e.key==='Enter'){
|
|
706
727
|
const items=searchListItems();
|
|
707
728
|
const active=items.find(el=>el.classList.contains('active'))||items[0];
|
|
708
|
-
if(active)
|
|
729
|
+
if(active) toggleSel(active.dataset.id);
|
|
709
730
|
} else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
|
|
731
|
+
else if(e.key==='Backspace' && !qInput.value && state.sel.length){ e.preventDefault(); toggleSel(state.sel[state.sel.length-1]); }
|
|
710
732
|
};
|
|
711
733
|
qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
|
|
712
734
|
qInput.onclick=()=>{ renderSearchList(searchMatches()); };
|
|
713
|
-
|
|
735
|
+
const sbox=document.getElementById('search-box');
|
|
736
|
+
if(sbox) sbox.onclick=e=>{ if(e.target.closest('.q-item')||e.target.closest('.token')) return; qInput.focus(); };
|
|
737
|
+
qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box && !document.getElementById('search-box').contains(document.activeElement)) box.classList.remove('open'); },160); };
|
|
714
738
|
}
|
|
715
739
|
const fdSel=document.getElementById('focus-fd');
|
|
716
740
|
if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
717
741
|
const fxBtn=document.getElementById('focus-exit');
|
|
718
|
-
if(fxBtn) fxBtn.onclick=()=>{ state.
|
|
742
|
+
if(fxBtn) fxBtn.onclick=()=>{ state.sel=[]; go(state.view,state.mode,state.range,state.zoom); };
|
|
719
743
|
document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
|
|
720
744
|
document.querySelectorAll('#zoom button').forEach(b=>b.onclick=()=>{ const z=b.dataset.z==='+'?Math.min(84,state.zoom+8):b.dataset.z==='-'?Math.max(14,state.zoom-8):28; go('time',state.mode,state.range,z); });
|
|
721
745
|
layer.addEventListener('scroll',syncLaneLabels,{passive:true});
|
package/docs/design/graph-web.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# dia graph Web 视觉设计稿(v0.3)
|
|
2
2
|
|
|
3
|
+
> **落地状态(2026-11-06)**:M1 已实现 `dia graph --export-html`(真实账本 → 单文件静态 HTML,无网络);
|
|
4
|
+
> 数据模型/契约/安全边界见 [graph-web-m1.md](graph-web-m1.md)。live 服务(M2)未做。
|
|
5
|
+
|
|
3
6
|
> 静态 mockup:[`graph-web-mockup.html`](graph-web-mockup.html)(浏览器直接打开,无网络/无依赖)
|
|
4
7
|
> 数据契约:`dia graph --json`(schema 1);时间线所需 start/end 为后续扩展字段。
|
|
5
8
|
|
|
@@ -144,10 +147,12 @@ mockup 数据改为 **100 个任务、时间跨度 30 天**,用来验证真实
|
|
|
144
147
|
|
|
145
148
|
- **时间窗(默认近 7 天,按开始时间,结束时间不限)**:只显示 `start >= 今天 - N` 的任务,
|
|
146
149
|
长任务即使结束在很久以后也照常显示;时间线轴自动从 `-N` 延伸到最晚的 `end`;
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
- **搜索即选择(token 输入框)**:聚焦就出下拉(列出当前时间窗全部任务,按优先级/开始时间排序),
|
|
151
|
+
输入 ID / 名称 / owner 实时过滤;选中项**回显成 tag**(`t-003 ×`)留在输入框里,可多选;
|
|
152
|
+
`↑↓` 选择、`Enter` 加入、`Esc` 收起、`Backspace` 删除最后一个 tag、点 `×` 删除单个;
|
|
153
|
+
已选 tag 用蓝底高亮,下拉里带 `✓`;选择状态写进 URL(`sel=t-003,t-006`,向后兼容旧的 `focus=`);
|
|
154
|
+
- **邻域聚焦(类 nx graph focus)**:选中任务后只保留这些任务的依赖邻域(多个取并集),
|
|
155
|
+
深度可切 `1 层 / 2 层 / 全链路`,`清空` 回全量;双击卡片等同于点选该任务;
|
|
151
156
|
- 被过滤掉的任务不计入概览统计,标题显示 `显示 N / 总数`。
|
|
152
157
|
|
|
153
158
|
## 11. 时间线滚动与缩放
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# 固定席位 + 上下文亲和(M2 设计)
|
|
2
|
+
|
|
3
|
+
> 状态:设计定稿(2026-11-06,未实现)|依赖:`graph-prio-dag.md`(eff/rank)、`graph-ui-m1.md`(面板)
|
|
4
|
+
> 红线:**只读建议;不写 tasks 账本、不常驻守护、不抢占、不自动 force 接管**。
|
|
5
|
+
|
|
6
|
+
## 1. 目标 / 非目标
|
|
7
|
+
|
|
8
|
+
**目标**:席位(并发会话)数量固定时,回答「每个席位现在接哪件」——同时考虑价值(rank)、约束(依赖/claim)与**断链成本**(上下文)。
|
|
9
|
+
|
|
10
|
+
**非目标**:自动领取/自动开工;跨机席位发现;任务级分布式锁;把 rank 变成门禁。
|
|
11
|
+
|
|
12
|
+
## 2. 名词与事实源
|
|
13
|
+
|
|
14
|
+
| 名词 | 定义 | 事实源 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| seat | 一个在线会话(opencode/dsh 进程) | `~/.dia/collab/sessions/*.json` 的 `last_seen` |
|
|
17
|
+
| presence | `now - last_seen <= TTL`(默认 30m,`--presence-ttl`) | 同上 |
|
|
18
|
+
| claim | **路径**租约(谁在改哪个文件) | `~/.dia/collab/claims.jsonl`(TTL 默认 4h) |
|
|
19
|
+
| owner | 任务声明的负责人(不发租约) | `tasks/ledger.jsonl` 的 add/plan |
|
|
20
|
+
| busy | 席位有 `doing` 任务 | 任务 status |
|
|
21
|
+
| 亲和 | 任务与该席位近期工作面的重叠度 | 任务 touches + 席位最近任务 |
|
|
22
|
+
|
|
23
|
+
**分工写死**:`rank` 只回答「先做什么」;`claim` 只回答「谁能改哪条路径」;`owner` 只回答「谁负责」,三者不得互相替代。
|
|
24
|
+
|
|
25
|
+
## 3. 席位状态机(全派生,零写入)
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
online & 无 doing online & doing(todo/doing)
|
|
29
|
+
┌──────── idle ◄────────┐ ┌──────── busy
|
|
30
|
+
│ │ │
|
|
31
|
+
│ (接新任务即 busy) └────────┤
|
|
32
|
+
└──────── holding ◄──────┘ doing 但 status∈{review,blocked}(释放执行位,保留上下文)
|
|
33
|
+
│
|
|
34
|
+
last_seen 超 TTL
|
|
35
|
+
▼
|
|
36
|
+
offline(不再分配;其 doing 任务标 stale,需人确认)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `holding` 仍可接新任务(执行位空闲),但提示「上下文占用 N 个」;N>3 建议先 handoff;
|
|
40
|
+
- `offline` 的 `doing` 任务**回候选集**但标 `reason:'seat_offline'`,不自动改 status/owner;
|
|
41
|
+
- 无自动回收路径:busy→idle 只在任务状态变化时派生,进程退出即 offline(靠 TTL)。
|
|
42
|
+
|
|
43
|
+
## 4. 亲和度(affinity)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
aff(t, seat) = 3 * ownerMatch # seat.session == t.owner
|
|
47
|
+
+ 2 * splitGroup # 与席位最近任务共享 split_from 或同一父
|
|
48
|
+
+ 2 * jaccard # touches 的 Jaccard,[0,1],0/0 = 0
|
|
49
|
+
+ 1 * moduleOverlap # 路径前 2 段前缀相同
|
|
50
|
+
cost(t, seat) = 1 - aff/8 # ∈ [0,1],越小越省
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `seatTouches` = 该席位最近 H=5 个任务(doing/review/done)的 touches 并集;
|
|
54
|
+
- 分量量纲固定:owner/split/module 为 0/1,jaccard 归一化到 [0,1];`aff` 最大 8;
|
|
55
|
+
- **算例**(必须进单测):
|
|
56
|
+
|
|
57
|
+
| 场景 | aff | cost | 结论 |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| 同 owner(3) | 3 | 0.625 | 可接,但不算强亲和 |
|
|
60
|
+
| 同 split 组(2)+ jaccard 0.5(1) | 3 | 0.625 | 同上 |
|
|
61
|
+
| 同 owner + 同模块(4) | 4 | 0.5 | 达到「续做」阈值 |
|
|
62
|
+
| 完全同 touches(jaccard=1,2) | 2 | 0.75 | 单靠路径重叠不续做 |
|
|
63
|
+
| owner+split+jaccard1+module(8) | 8 | 0 | 强续做 |
|
|
64
|
+
|
|
65
|
+
> 阈值:**续做** `cost ≤ 0.5`;**换人** 当 `cost > 0.5` 且存在另一席位 `cost 低 ≥ 0.25`。单靠「路径重叠」不足以免换人——避免把不相关任务硬塞给同一席位。
|
|
66
|
+
|
|
67
|
+
## 5. 分配算法(rank 优先,亲和定席位)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
候选 = startable ∧ rank≠null ∧ 无「路径 claim 冲突」
|
|
71
|
+
对候选按 rank 升序:
|
|
72
|
+
在空闲/持有席位中,选 (cost, seat.id) 最小者
|
|
73
|
+
若该席位 busy → 跳过(不抢占)
|
|
74
|
+
输出 assignments;未被分配的进入 unassigned,带原因
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- 复杂度 `O(N·S)`(N 候选、S 席位),500×3 < 1ms;
|
|
78
|
+
- 确定性 tiebreak:`(cost, declared prio index, task.id, seat.id)`;
|
|
79
|
+
- **不做**「全局最优分配」:席位小、账本变动频繁,贪心 + 可解释优先;
|
|
80
|
+
- 显式反例(写入文档,防止误读):rank 1 但亲和极差的任务仍会被先分配——这是「价值优先」的代价;若需要「上下文优先」模式,M3 再加 `--by=affinity`。
|
|
81
|
+
|
|
82
|
+
## 6. CLI 契约
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
dia graph --next [n] --seats 2 --presence-ttl 30m --now 2026-11-06T10:00:00 --json
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"schema": 1,
|
|
91
|
+
"policy": "seats-v1",
|
|
92
|
+
"seats": [{"seat":"ses_a","state":"idle","presence_at":"2026-11-06T09:58:12Z","focus":null}],
|
|
93
|
+
"assignments": [{"seat":"ses_a","task":"t-3c1f","rank":1,"eff":"P0","cost":0.25,"affinity":{"owner":1,"split":0,"jaccard":1,"module":0},"why":"同 owner + 路径重叠 1.0"}],
|
|
94
|
+
"unassigned": [{"task":"t-9","rank":4,"reason":"no_seat|seat_offline|claim_conflict"}],
|
|
95
|
+
"conflicts": [{"task":"t-9","kind":"claim_overlap","detail":"src/x.ts 被 ses_b claim"}],
|
|
96
|
+
"metrics": {"switches": 1, "handoff_rate": 0.0, "idle_slots": 0}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- **不含墙钟**:只出 `presence_at`(ISO);`--now` 仅用于计算 state/交接判断,不落输出;
|
|
101
|
+
- 同一 `--now` + 同一账本 → 字节相等;打乱任务顺序结果不变;
|
|
102
|
+
- 退出码:0 正常(含无任务)、2 参数错、3 依赖环(排序不可用)。
|
|
103
|
+
|
|
104
|
+
## 7. 链式续做与 handoff
|
|
105
|
+
|
|
106
|
+
默认**续做**(cost ≤ 0.5 且席位非 busy)。**必须打断**的四条:
|
|
107
|
+
1. 出现更高 `rank` 且其它席位都接不住的任务(价值让位);
|
|
108
|
+
2. 下一环 touches 与席位工作面**无重叠**(模块切换,cost > 0.5);
|
|
109
|
+
3. 当前任务进入 review/blocked(执行位空出,可换手);
|
|
110
|
+
4. 上下文将满(启发式:本会话完成任务数 ≥K 或已有 1 次 handoff 建议)。
|
|
111
|
+
|
|
112
|
+
handoff 内容固定五段:目标状态 / 已完成 / 未完成 / 触碰文件 / 下一步命令;通道用 `dia msg send --kind handoff`,交接后**人工重新 claim**,不自动转移。
|
|
113
|
+
|
|
114
|
+
## 8. 指标(只读派生)
|
|
115
|
+
|
|
116
|
+
- `switches`:同一任务换席位的次数;
|
|
117
|
+
- `handoff_rate`:有 handoff 消息的任务 / 被接手任务;
|
|
118
|
+
- `idle_slots`:在线但无任务的席位数;
|
|
119
|
+
- 采样窗口由调用方传 `--since`,不落账本;用于评估「固定席位是否够用」。
|
|
120
|
+
|
|
121
|
+
## 9. 边界
|
|
122
|
+
|
|
123
|
+
- **claim 与 presence 冲突**:claim 4h 有效但会话 30m 离线 → 标 `conflicts: claim_held_offline`,任务进 unassigned 且**不自动 force**(人工 `dia claim --force` 或等 TTL);
|
|
124
|
+
- 席位掉线:`offline`,其任务回候选并标 stale;
|
|
125
|
+
- S > 任务数:多余席位 idle,不为填满降低门槛;
|
|
126
|
+
- S=0:纯建议模式,全量 unassigned,exit 0;
|
|
127
|
+
- 跨机:只读本机 `~/.dia`,其它机器会话视为离线;
|
|
128
|
+
- 空 touches:jaccard=0,不除零;
|
|
129
|
+
- 环内/终态任务不进候选。
|
|
130
|
+
|
|
131
|
+
## 10. 验收
|
|
132
|
+
|
|
133
|
+
- 打乱任务顺序、固定 `--now` 两次输出字节一致;
|
|
134
|
+
- busy 席位不被二次分配;holding 可接新任务;
|
|
135
|
+
- affinity 五个算例硬断言(含 0/0);
|
|
136
|
+
- claim 冲突进 conflicts 且不抢占;
|
|
137
|
+
- `--seats 1` 与 `graph --next` 单项一致(同一排序源);
|
|
138
|
+
- e2e:两会话(`DIA_SESSION_ID`)跑 `whoami` + doing → 各得一任务;杀一个会话超 TTL 后任务回落。
|
package/docs/index.md
CHANGED
|
@@ -10,6 +10,7 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
|
|
|
10
10
|
|---|---|
|
|
11
11
|
| [ACP 集成与降级设计(2026-10)](acp-integration.md) | 问题:通过 ACP 是否更好?fallback 怎么做?配置失败如何降级、体验如何? |
|
|
12
12
|
| [架构:框架 / 场景入口 / kit](architecture.md) | 本文回答一个问题:什么该留在 dabo-agent,什么该抽到独立框架。 |
|
|
13
|
+
| [内置能力的分层:dia-msg 不该让用户「装」](builtin-kit-design.md) | 日期:2026-11-09 | 状态:实现中(P0 已合,P1 本文档) | 触发:用户质疑「dia 是命令入口,为什么内置的还要单独装?dia-msg 都算内置」 |
|
|
13
14
|
| [dia CLI 词表(动作规范)](cli.md) | 原则:动作成对、名词分层、不混轴。 |
|
|
14
15
|
| [外部依赖索引与原始记录维护(2026-10)](dependency-registry.md) | 回答两件事:依赖怎么登记/验证/淘汰,原始记录怎么保留/重建/删除。 |
|
|
15
16
|
| [生态与组合:可复用性调研(2026-10)](ecosystem-integration.md) | 问题:dia 的包和集成面应该长什么样,才能同时服务 opencode / dsh(未来 OpenClaw/Hermes/Pi)? |
|
|
@@ -17,7 +18,9 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
|
|
|
17
18
|
| [用户入口:人在哪进,agent 叫什么](entrypoints.md) | 一句话:入口仍是 opencode/dsh(和 yan 一样),但 agent 名是 dabo,不是 dia;dia 是控制面命令,不是人格。 |
|
|
18
19
|
| [进化:dia 的第一性定位](evolution-process.md) | 本文不是功能清单,是过程定义。 |
|
|
19
20
|
| [宿主控制面:黑盒边界与内部 seam(2026-10)](host-control-surface.md) | 问题:把 opencode(含 v2)与 dsh 当"黑盒宿主"时,我们能在哪些边界上控制它们? |
|
|
21
|
+
| [安装防呆:脚本安装不许抢走 npm 全局安装](install-guard-design.md) | 日期:2026-11-06 | 状态:已实现(D0.65) | 触发:同一台 Linux 机器 3 次出现 ~/.local/bin/dia 被指回脚本安装 |
|
|
20
22
|
| [Kit:内容包安装(persona 的第二个半边)](kit-install.md) | 前置:docs/persona-install.md(persona = 人格,kit = 内容)。 |
|
|
23
|
+
| [Kit 层:把「经验」从 persona 里抽出来(设计 v0.1)](kit-layer.md) | 2026-11-06 用户定调:dws-hub 作为 channel 层重构,同时把 git / node 这类通用工程经验抽成 kit 层, |
|
|
21
24
|
| [模型基准与 Sidecar 监控(A+B 一起)](model-baseline-sidecar.md) | A:首次使用先跑基准,用数据决定配置。 |
|
|
22
25
|
| [模型行为调控(客户端侧)](model-behavior.md) | 边界(用户定):只使用各平台提供的通用模型;不做本地构建/微调;不依赖多数模型不支持的推理与输入输出控制参数; |
|
|
23
26
|
| [dia monorepo:框架模块的归属](monorepo.md) | 结论:dia-job 回 dia monorepo,作为 packages/job;kb 作为 packages/kb。 |
|
|
@@ -92,5 +95,10 @@ Agent 入口:[`llms.txt`](https://github.com/minonl/dia/blob/main/llms.txt)(
|
|
|
92
95
|
|
|
93
96
|
| 文档 | 一句话 |
|
|
94
97
|
|---|---|
|
|
95
|
-
| [
|
|
98
|
+
| [优先级 × 依赖:任务编排语义(M1)](design/graph-prio-dag.md) | 状态:已实现(2026-11-06)|代码:src/plan.ts(派生纯函数)、src/task.ts(split)、src/graph.ts(契约) |
|
|
99
|
+
| [关系图界面:派生量上屏(M1 UI)](design/graph-ui-m1.md) | 状态:设计定稿(2026-11-06)|实现:src/graph-html/(待接)|上游:graph-prio-dag.md |
|
|
100
|
+
| [dia graph Web(M1)设计:真实数据 → 单文件静态导出](design/graph-web-m1.md) | 状态:M1 设计(2026-11-06)|前置:graph-web.md(视觉稿 v0.3)、graph-web-mockup.html(交互原型) |
|
|
101
|
+
| [graph live 服务(M2 设计,可选包)](design/graph-web-m2.md) | 状态:设计定稿(2026-11-06,未实现)|前置:graph-web-m1.md(静态导出)、graph-ui-m1.md、seat-scheduling-m2.md |
|
|
102
|
+
| [dia graph Web 视觉设计稿(v0.3)](design/graph-web.md) | 落地状态(2026-11-06):M1 已实现 dia graph --export-html(真实账本 → 单文件静态 HTML,无网络); |
|
|
103
|
+
| [固定席位 + 上下文亲和(M2 设计)](design/seat-scheduling-m2.md) | 状态:设计定稿(2026-11-06,未实现)|依赖:graph-prio-dag.md(eff/rank)、graph-ui-m1.md(面板) |
|
|
96
104
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# 安装防呆:脚本安装不许抢走 npm 全局安装
|
|
2
|
+
|
|
3
|
+
> 日期:2026-11-06 | 状态:**已实现(D0.65)** | 触发:同一台 Linux 机器 3 次出现 `~/.local/bin/dia` 被指回脚本安装
|
|
4
|
+
> 上游:三名独立子会话的 grill(架构/契约、红队、兼容迁移),结论一致 | 判据:`bash tests/install-guard.test.sh`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 现象与根因
|
|
9
|
+
|
|
10
|
+
**现象**:`~/.local/bin/dia` 从 npm 包 bin 被改成 `~/.local/share/dia/bin/dia.js`,随后:
|
|
11
|
+
|
|
12
|
+
- `paths.installDir()` 按 `import.meta.url` 反推安装根 → 落在脚本安装目录
|
|
13
|
+
- 内置能力包 `kits/dia-msg` 去旧目录找 → `missing-kit` / `E_KIT_EMPTY`
|
|
14
|
+
- `dia doctor` 红、`dia update` 走脚本模式(自己 `git pull && bun run build && bash install.sh`,再抢一次)
|
|
15
|
+
|
|
16
|
+
**根因**:`install.sh` 第 58 行 **无条件** `ln -sfn "$DIA_INSTALL_DIR/bin/dia.js" "$DIA_BIN_DIR/dia"`。
|
|
17
|
+
两种安装共存本身不是错误(npm 可以装在别的 prefix),错误是**后来者覆盖了正在生效的那条链接**。
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
graph LR
|
|
21
|
+
A[install.sh 运行] --> B{目标 bin 现在指向谁}
|
|
22
|
+
B -->|npm 包 bin| C[拒绝 exit 3<br/>不动任何文件]
|
|
23
|
+
B -->|脚本安装 / 不存在| D[正常安装]
|
|
24
|
+
D --> E[打印实际生效的 dia + version]
|
|
25
|
+
E --> F{生效路径 == 目标?}
|
|
26
|
+
F -->|否| G[warn:不在 PATH 或 PATH 顺序靠后]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 2. 被否掉的初版方案
|
|
30
|
+
|
|
31
|
+
初版设想是「检测到 npm 全局包存在就拒绝」。grill 三方一致否掉,理由:
|
|
32
|
+
|
|
33
|
+
| 问题 | 证据 |
|
|
34
|
+
|---|---|
|
|
35
|
+
| 两种安装可以合法共存 | 本机 `npm prefix -g` 有 `@minonl/dia`,同时 `~/.local/bin/dia` 是脚本安装,两者并存且脚本版更新 |
|
|
36
|
+
| npm 包可能是**旧残留** | npm 0.0.13 + 脚本 0.0.15 时,拒绝安装等于**永久锁死升级** |
|
|
37
|
+
| 多 Node 前缀 | fnm/nvm 切版本后 `npm ls -g` 结果就变,检测不稳定 |
|
|
38
|
+
| 依赖 npm 命令 | 无 npm 的容器/离线场景会误伤;且 `npm ls` 有 0.5s 启动成本 |
|
|
39
|
+
|
|
40
|
+
## 3. 决定(三条)
|
|
41
|
+
|
|
42
|
+
**D1 判据 = 抢链接,不是「包存在」**
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pkgOwned(p):三种形态任一命中即算包管理器全局安装
|
|
46
|
+
A 软链/拷贝:逐级解析符号链接后,真实路径匹配 (/|\\)node_modules(/|\\)@minonl(/|\\)dia
|
|
47
|
+
B shim 脚本:目标文件不是软链,内容含 @minonl/dia 或 node_modules/(@minonl/)?dia/
|
|
48
|
+
C 二进制 shim:所在目录是已知包管理器全局 bin(~/.volta/bin、PNPM_HOME 等)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
只在这些情况拒绝(且该路径正是 `command -v dia` 会命中的那条):
|
|
52
|
+
|
|
53
|
+
1. `$DIA_BIN_DIR/dia` 的 realpath 落在 `node_modules/@minonl/dia`(npm / bun 全局安装);
|
|
54
|
+
2. bin 是 **shim 脚本/拷贝**且内容带包路径(pnpm / yarn-berry);
|
|
55
|
+
3. bin 所在目录属于已知包管理器全局 bin(volta 这类不含包路径的同名 shim)。
|
|
56
|
+
|
|
57
|
+
包存在但 bin 未被占 → **只 warn 一行,继续安装**。
|
|
58
|
+
|
|
59
|
+
> 红队回灌(2026-11-06,实现后):`--home` 独占**不算**沙箱(链接仍写默认 bin,守卫照旧生效);
|
|
60
|
+
> 链接落位失败必须退 1(不再谎报装好);`--migrate` 改为**只搬数据**(不装、不改 bin)。
|
|
61
|
+
|
|
62
|
+
> 为什么要有 B/C:pnpm / yarn-berry / volta 的全局 bin 是 **shim 脚本或同名二进制**,不是指向
|
|
63
|
+
> `node_modules/@minonl/dia` 的软链。只认软链时守卫会把这些安装判成「脚本安装」并静默覆盖,
|
|
64
|
+
> 事故形态与 npm 完全一致(本仓库 packageManager 就是 pnpm)。
|
|
65
|
+
|
|
66
|
+
**D2 退出码 3 + 显式逃生舱**
|
|
67
|
+
|
|
68
|
+
| 码 | 含义 |
|
|
69
|
+
|---|---|
|
|
70
|
+
| 0 | 装好(含「已警告但仍安装」) |
|
|
71
|
+
| 1 | 前置缺失(缺 `dist/cli.js` / `bin/dia.js`) |
|
|
72
|
+
| 2 | usage |
|
|
73
|
+
| **3** | **守卫拒绝:未改动任何文件**(可安全重试,重试必然同样结果) |
|
|
74
|
+
|
|
75
|
+
放行条件(任一命中即跳过守卫):
|
|
76
|
+
|
|
77
|
+
- `DIA_ALLOW_SCRIPT_INSTALL=1`(人类显式意图)
|
|
78
|
+
- `--dry-run`(只演练)
|
|
79
|
+
- **生效 bin 目录被重定向**:`--bin DIR` / `DIA_BIN_DIR=DIR`,且不等于默认 `~/.local/bin`
|
|
80
|
+
|
|
81
|
+
> **只改安装目录不算沙箱**(`--home` / `--install-dir` / `DIA_INSTALL_DIR` / `DIA_HOME_INSTALL`):
|
|
82
|
+
> 链接仍写在默认 `~/.local/bin/dia`,照样会抢走生效安装,守卫必须照旧生效(exit 3)。
|
|
83
|
+
> 想要「不碰生效路径」的沙箱,必须同时指定 `--bin`。
|
|
84
|
+
|
|
85
|
+
**D3 `dia update` 不硬失败**
|
|
86
|
+
|
|
87
|
+
脚本模式下**保持现状**(打印升级指引、exit 0),不升级为错误:否则「脚本安装的机器」会彻底无法升级,且 CI 里 `update` 测试会红。只加一条:若检测到 npm 包与当前安装不一致,多打印一行 `dia doctor` 提示。
|
|
88
|
+
|
|
89
|
+
## 4. 配套
|
|
90
|
+
|
|
91
|
+
**D4 安装后自检**:`install.sh` 结束时打印
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
dia installed: <install dir> → <bin dir>/dia
|
|
95
|
+
effective: <realpath of $(command -v dia)> (<version>)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
生效路径 != 目标时 warn(这正是「npm 装到不在 PATH 的 prefix」的可见化)。
|
|
99
|
+
|
|
100
|
+
**D5 `dia doctor` 新增 `install_shadow`(fail)**
|
|
101
|
+
|
|
102
|
+
判定:`installDir()` 不是包管理器安装 + `command -v dia` 解析到包管理器安装(或反过来)→ 报不一致,附修复命令。
|
|
103
|
+
包管理器安装的 bin 是 shim(pnpm/yarn)时按脚本内容识别,不只看 realpath:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
ln -sfn "$(npm prefix -g)/bin/dia" ~/.local/bin/dia
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**D6 原子写**(低成本顺带):`cp` 落 `<file>.tmp` 再 `mv -f`;`ln -sfn` 改「先建临时链接再 rename」,避免并发会话看到半截文件/短暂无命令。
|
|
110
|
+
|
|
111
|
+
## 5. 失败模式(红队保留项)
|
|
112
|
+
|
|
113
|
+
| 场景 | 处理 |
|
|
114
|
+
|---|---|
|
|
115
|
+
| npm 包在别的 Node 前缀 | 不检测 npm 列表,只判真实路径 → 天然不受影响 |
|
|
116
|
+
| pnpm/yarn/bun/volta 全局装 | 软链看 realpath(`node_modules/@minonl/dia`);shim 看内容(`@minonl/dia`);volta 二进制 shim 看全局 bin 目录归属 |
|
|
117
|
+
| 只改安装目录当沙箱 | 不算沙箱:链接仍写默认 bin,守卫照旧 exit 3;沙箱必须重定向 `--bin`/`DIA_BIN_DIR` |
|
|
118
|
+
| `readlink -f` 在 macOS 不存在 | 用 `cd "$(dirname x)" && pwd -P` 逐级解析,两边同构 |
|
|
119
|
+
| 本机测试被守卫误拦 | 测试全部用临时 `DIA_INSTALL_DIR` + 临时 `DIA_BIN_DIR`(= 沙箱放行),不探测真机环境;只给安装目录的用例应得 rc=3(`tests/install-guard-strict.test.sh` ①) |
|
|
120
|
+
| 上层脚本把 exit 3 当致命 | 文案给出三条出路(显式放行 / 回 npm / 换 `--bin` 沙箱);`docs/` 里所有 `bash install.sh` 用法补注「默认路径已被包管理器占用时会拒绝」 |
|
|
121
|
+
| 老版本 dia 没有防呆 | `doctor install_shadow` 负责发现,修复命令一条 |
|
|
122
|
+
|
|
123
|
+
## 6. 判据(可执行)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# 1) 目标 bin 被包管理器占用(**默认路径**)→ 拒绝,且链接字节不变
|
|
127
|
+
# 自动化等价物:tests/install-guard.test.sh ①(临时 HOME + 临时 PATH + 伪 npm 包)
|
|
128
|
+
# 1b) shim 形态(pnpm/yarn/volta)→ 同样拒绝;只改安装目录(--home 等)→ 同样拒绝
|
|
129
|
+
# 自动化等价物:tests/install-guard-strict.test.sh ①②
|
|
130
|
+
# 2) 真沙箱(**bin 目录**被重定向)→ 放行
|
|
131
|
+
DIA_INSTALL_DIR=/tmp/x DIA_BIN_DIR=/tmp/xbin bash install.sh; test $? -eq 0
|
|
132
|
+
# 3) npm 包存在但 bin 未被占 → warn 但安装(install-guard.test.sh ③)
|
|
133
|
+
# 4) doctor:真实路径与 installDir 不一致 → install_shadow fail
|
|
134
|
+
dia doctor | grep -q install_shadow
|
|
135
|
+
# 5) npm 安装的机器上:installDir 含 node_modules/@minonl,且 kits/dia-msg 可解析
|
|
136
|
+
dia doctor --paths | grep -q 'node_modules/@minonl'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 7. 不做的
|
|
140
|
+
|
|
141
|
+
- **不做 `dia install repair`**:新增命令面大于收益;doctor 给一行可复制命令即可。
|
|
142
|
+
- **不改 `dia update` 退出码语义**:`--check` 的 0/1 语义保持不变。
|
|
143
|
+
- **不引入 npm 依赖检测**(`npm ls -g` / `npm prefix -g`):确定性优先。
|
|
144
|
+
|
|
145
|
+
## 8. 文档同步
|
|
146
|
+
|
|
147
|
+
`README.md`(安装章节)、`docs/cli.md`(`doctor` 检查项)、`docs/kit-install.md`(install.sh 分发说明)、
|
|
148
|
+
`docs/status.md`(新增条目)、`docs/index.md` + `llms.txt`(新文档索引,改完跑 `npm run docs:index`)。
|
package/docs/kit-install.md
CHANGED
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
|
|
31
31
|
结论:**persona 装完是"哑"的**;kit 就是把这些补上,且要能 `verify` / `remove`。
|
|
32
32
|
|
|
33
|
+
> **install.sh 分发守卫(2026-11-06)**:`~/.local/bin/dia` 当前属于包管理器全局安装
|
|
34
|
+
> (npm/bun 的 `node_modules/@minonl/dia` 软链、pnpm/yarn 的 shim 脚本、volta 的 bin)且正是 `command -v dia`
|
|
35
|
+
> 命中的那条时,`install.sh` **exit 3 且不改动任何文件**(避免脚本安装抢链接后 kit 去旧目录找、
|
|
36
|
+
> `missing-kit` / `E_KIT_EMPTY`)。逃生舱:`DIA_ALLOW_SCRIPT_INSTALL=1`、`--dry-run`,
|
|
37
|
+
> 或**把生效 bin 目录重定向**(`--bin DIR` / `DIA_BIN_DIR=DIR`)。注意:`--home` / `DIA_INSTALL_DIR`
|
|
38
|
+
> 只改安装目录,链接仍写默认 `~/.local/bin`,**不算沙箱**(守卫照旧生效)。包存在但 bin 未被占 → 只 warn 一行。
|
|
39
|
+
|
|
33
40
|
---
|
|
34
41
|
|
|
35
42
|
## 2. 清单:`dia.json`(kind=kit)
|