weir-harness 1.1.4 → 1.1.6

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/cordis.patch.yml +48 -87
  2. package/icon.svg +1 -0
  3. package/lib/client.capability-badge.js +16 -16
  4. package/lib/client.capability-manager.js +87 -36
  5. package/lib/client.capability-model.js +151 -31
  6. package/lib/client.capability-presets.js +187 -39
  7. package/lib/client.edit-lock-maintenance.js +27 -12
  8. package/lib/client.edit-lock-panel.js +52 -19
  9. package/lib/client.js +270 -12
  10. package/lib/client.settings-page.js +13 -8
  11. package/lib/client.worktree-model.js +20 -1
  12. package/lib/client.worktree-view.js +32 -9
  13. package/package.json +4 -2
  14. package/skills/deep-work/SKILL.md +7 -3
  15. package/src/blackboard/durable.js +199 -0
  16. package/src/blackboard/index.js +62 -42
  17. package/src/blackboard/kernel.js +20 -1
  18. package/src/blackboard/remote.js +36 -28
  19. package/src/blackboard/storage.js +238 -0
  20. package/src/capabilities/capability-remote.js +122 -69
  21. package/src/capabilities/preset-library.js +113 -87
  22. package/src/capabilities/read-payloads.js +21 -4
  23. package/src/capabilities/skill-selection-plugin.js +188 -168
  24. package/src/capabilities/skill-selection-provider.js +13 -0
  25. package/src/capabilities/store/enumerate.js +46 -0
  26. package/src/capabilities/store/store.js +86 -1
  27. package/src/context-guard/index.js +222 -117
  28. package/src/context-guard/pressure.js +12 -3
  29. package/src/context-guard/recovery.js +25 -0
  30. package/src/core/doctrine.js +3 -3
  31. package/src/delegate/group-coordinator.js +2 -1
  32. package/src/delegate/supervision-mount.js +41 -2
  33. package/src/delegate/tool.js +11 -3
  34. package/src/edit-lock/admin-ledger.js +42 -6
  35. package/src/edit-lock/admin-recovery.js +4 -2
  36. package/src/edit-lock/binding-attestation.js +29 -0
  37. package/src/edit-lock/binding-guidance.js +43 -0
  38. package/src/edit-lock/domain.js +20 -5
  39. package/src/edit-lock/host.js +3 -3
  40. package/src/edit-lock/index.js +268 -57
  41. package/src/edit-lock/lifecycle.js +106 -47
  42. package/src/edit-lock/maintenance.js +12 -3
  43. package/src/edit-lock/manager.js +8 -3
  44. package/src/edit-lock/peer.js +2 -0
  45. package/src/edit-lock/publisher.js +57 -1
  46. package/src/edit-lock/remote.js +120 -52
  47. package/src/edit-lock/runtime.js +8 -0
  48. package/src/edit-lock/view.js +27 -12
  49. package/src/settings/sections.js +30 -6
  50. package/src/shared/audit.js +6 -0
  51. package/src/shared/delegate-edit-lifecycle.js +36 -0
  52. package/src/todo-driver/index.js +16 -1
  53. package/src/worktree/cards.js +47 -3
  54. package/src/worktree/command.js +29 -5
  55. package/src/worktree/git.js +39 -5
  56. package/src/worktree/lanes.js +83 -25
  57. package/src/worktree/prompts.js +2 -1
package/cordis.patch.yml CHANGED
@@ -40,7 +40,7 @@
40
40
  worktreeAutoSetup: true
41
41
  worktreeWatchTimeoutMinutes: 360
42
42
  worktreeAutoApprove: auto-clean
43
- editLockEnabled: false
43
+ editLockEnabled: true
44
44
  # Message-driven auto-resume (edit-lock-auto-resume-on-message): a genuine
45
45
  # user message resumes a stopped session and confirms its retained files.
46
46
  # Read live per message, so toggling applies without a restart.
@@ -70,13 +70,12 @@
70
70
  - id: weir-notify
71
71
  name: 'weir-harness/notify'
72
72
  # Capability read channel (silent-capability-reads): the plugin-owned
73
- # typert remote serving the Badge/manager READ verbs (receipt / list /
74
- # conditions) so reads never touch the session log. Host layer by
75
- # necessity: the typert gateway resolves the service from the host root
76
- # (S27); the preset-layer skill-selection feeds its faces through the
77
- # module-level bridge. Inert with a warning when typert is absent.
73
+ # typert remote serving all five Badge/manager reads without session-log
74
+ # writes. The host gateway resolves this service from the root (S27);
75
+ # preset feeds are labeled and routed by the sessionQuery projection.
78
76
  - id: weir-capability-remote
79
77
  name: 'weir-harness/capability-remote'
78
+ inject: [typert, sessionQuery]
80
79
  # Session blackboard panel data channel (design D8): plugin-owned typert
81
80
  # remote serving list/read/apply/write/remove so panel reads never touch
82
81
  # the session log. Host layer for the same S27 reason as the capability
@@ -146,6 +145,7 @@
146
145
  - id: weir-skill-selection
147
146
  name: 'weir-harness/skill-selection'
148
147
  config:
148
+ readPresetId: weir
149
149
  # preset-skill-applicability (D2): profile-supplemental roots
150
150
  # used ONLY to classify apply-missing names as known-but-
151
151
  # inapplicable (skip with report) vs genuinely unknown — the
@@ -179,33 +179,13 @@
179
179
  Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
180
180
 
181
181
  When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
182
- - id: compaction
182
+ - id: delegation
183
183
  name: cordis:group
184
184
  group: true
185
185
  isolate:
186
186
  compaction: true
187
187
  toolResultPruner: true
188
- config:
189
- - id: compaction-basic
190
- name: '@deepseek-ai/dsh-compaction-basic'
191
- - id: command-compact
192
- name: '@deepseek-ai/dsh-command-compact'
193
- - id: tool-result-pruner
194
- name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
195
- config:
196
- thresholdChars: 8192
197
- headChars: 4096
198
- tailChars: 1024
199
- # F6 自动压缩:上下文压力阈值 + 模型择时压缩 + 边界强制 + 压缩后续推。
200
- # `compaction` 服务被本组 isolate:消费者必须与提供者同 realm(否则
201
- # inject 永久等待,预设加载失败)。
202
- - id: context-guard
203
- name: 'weir-harness/context-guard'
204
- - id: delegation
205
- name: cordis:group
206
- group: true
207
- isolate:
208
- workflowEngine: true
188
+ weirCompactionCoordinator: true
209
189
  # 编辑锁服务:提供者 edit-lock 与消费者 todo-driver / hashline-edit /
210
190
  # lsp 同在本组。预设服务必须 isolate,否则预设加载以
211
191
  # "Preset services require isolate realms" 失败。
@@ -217,29 +197,29 @@
217
197
  # delegate write-contract consumer (slice 2) share this realm.
218
198
  weirBlackboard: true
219
199
  config:
200
+ - id: compaction
201
+ name: cordis:group
202
+ group: true
203
+ config:
204
+ - id: compaction-basic
205
+ name: '@deepseek-ai/dsh-compaction-basic'
206
+ - id: command-compact
207
+ name: '@deepseek-ai/dsh-command-compact'
208
+ - id: tool-result-pruner
209
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
210
+ config:
211
+ thresholdChars: 8192
212
+ headChars: 4096
213
+ tailChars: 1024
214
+ # F6 自动压缩:上下文压力阈值 + 模型择时压缩 + 边界强制 + 压缩后续推。
215
+ # `compaction` 服务被本组 isolate:消费者必须与提供者同 realm(否则
216
+ # inject 永久等待,预设加载失败)。
217
+ - id: context-guard
218
+ name: 'weir-harness/context-guard'
220
219
  - id: tool-subagent-control
221
220
  name: '@deepseek-ai/dsh-tool-subagent-control'
222
221
  - id: tool-subagent-list-agents
223
222
  name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
224
- - id: tool-subagent
225
- name: '@deepseek-ai/dsh-tool-subagent'
226
- config:
227
- provider: spawn
228
- toolName: subagent
229
- modelSelectionSettings: true
230
- backgroundMode: continuable
231
- - id: tool-subagent-fork
232
- name: '@deepseek-ai/dsh-tool-subagent'
233
- config:
234
- provider: fork
235
- toolName: subagent_fork
236
- backgroundMode: continuable
237
- - id: workflow-ptc
238
- name: '@deepseek-ai/dsh-workflow-ptc'
239
- config:
240
- provider: spawn
241
- - id: tool-workflow
242
- name: '@deepseek-ai/dsh-tool-workflow'
243
223
  - id: tool-ralph
244
224
  name: '@deepseek-ai/dsh-tool-ralph'
245
225
  disabled: true
@@ -262,8 +242,8 @@
262
242
  # F4 todo 续推:空转续推 + 用户打断/供应商错误语义 + 逃生舱
263
243
  - id: todo-driver
264
244
  name: 'weir-harness/todo-driver'
265
- # 编辑锁(实验,默认关):设置 editLockEnabled 开启并重启后生效。
266
- # 必须排在 hashline-edit / lsp 之前,二者在挂载时捕获锁服务。
245
+ # 编辑锁(默认开):显式保存 false 后重启才关闭;须在 hashline-edit / lsp 前挂载。
246
+ # 两个消费者挂载时捕获隔离组内的锁服务。
267
247
  - id: edit-lock
268
248
  name: 'weir-harness/edit-lock'
269
249
  # F7 锚点编辑:read 锚点增强 + fail-closed hash_edit 工具
@@ -356,6 +336,7 @@
356
336
  - id: weir-skill-selection
357
337
  name: 'weir-harness/skill-selection'
358
338
  config:
339
+ readPresetId: weir-creative
359
340
  supplementalSkillDirs:
360
341
  - !!js (() => { try { const p = process.getBuiltinModule('node:path'); return p.join(p.dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@deepseek-ai/dsh-agent-preset/package.json')), 'skills') } catch { return process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:os').tmpdir(), 'dsh-agent-preset-skills-unavailable') } })()
361
342
  customSkillDirs:
@@ -392,57 +373,37 @@
392
373
  Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
393
374
 
394
375
  When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
395
- - id: compaction
376
+ - id: delegation
396
377
  name: cordis:group
397
378
  group: true
398
379
  isolate:
399
380
  compaction: true
400
381
  toolResultPruner: true
401
- config:
402
- - id: compaction-basic
403
- name: '@deepseek-ai/dsh-compaction-basic'
404
- - id: command-compact
405
- name: '@deepseek-ai/dsh-command-compact'
406
- - id: tool-result-pruner
407
- name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
408
- config:
409
- thresholdChars: 8192
410
- headChars: 4096
411
- tailChars: 1024
412
- - id: context-guard
413
- name: 'weir-harness/context-guard'
414
- - id: delegation
415
- name: cordis:group
416
- group: true
417
- isolate:
418
- workflowEngine: true
382
+ weirCompactionCoordinator: true
419
383
  weirEditLock: true
420
384
  weirWorktreeLanes: true
421
385
  weirBlackboard: true
422
386
  config:
387
+ - id: compaction
388
+ name: cordis:group
389
+ group: true
390
+ config:
391
+ - id: compaction-basic
392
+ name: '@deepseek-ai/dsh-compaction-basic'
393
+ - id: command-compact
394
+ name: '@deepseek-ai/dsh-command-compact'
395
+ - id: tool-result-pruner
396
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
397
+ config:
398
+ thresholdChars: 8192
399
+ headChars: 4096
400
+ tailChars: 1024
401
+ - id: context-guard
402
+ name: 'weir-harness/context-guard'
423
403
  - id: tool-subagent-control
424
404
  name: '@deepseek-ai/dsh-tool-subagent-control'
425
405
  - id: tool-subagent-list-agents
426
406
  name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
427
- - id: tool-subagent
428
- name: '@deepseek-ai/dsh-tool-subagent'
429
- config:
430
- provider: spawn
431
- toolName: subagent
432
- modelSelectionSettings: true
433
- backgroundMode: continuable
434
- - id: tool-subagent-fork
435
- name: '@deepseek-ai/dsh-tool-subagent'
436
- config:
437
- provider: fork
438
- toolName: subagent_fork
439
- backgroundMode: continuable
440
- - id: workflow-ptc
441
- name: '@deepseek-ai/dsh-workflow-ptc'
442
- config:
443
- provider: spawn
444
- - id: tool-workflow
445
- name: '@deepseek-ai/dsh-tool-workflow'
446
407
  - id: tool-ralph
447
408
  name: '@deepseek-ai/dsh-tool-ralph'
448
409
  disabled: true
package/icon.svg ADDED
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="36" fill="#0A0A0B"/><g transform="translate(80 80) scale(.72) translate(-80 -84)"><polygon points="16,48 30,48 56,126 80,56 104,126 130,48 144,48 144,144 16,144" fill="#ECECE6"/><rect x="16" y="24" width="128" height="10" fill="#CCFF00"/></g></svg>
@@ -120,32 +120,35 @@ window.__ModuleLoader__.load({
120
120
  const [panelOpen, setPanelOpen] = react.useState(false);
121
121
  const [convergence, setConvergence] = react.useState(null);
122
122
  const aliveRef = react.useRef(false);
123
+ const requestRef = react.useRef(0);
123
124
  const refresh = react.useCallback(() => {
124
125
  if (!sessionId || typeof props.fetchReceipt !== "function") return Promise.resolve();
126
+ const request = ++requestRef.current;
125
127
  return props.fetchReceipt(sessionId).then((receipt) => {
126
- if (aliveRef.current) { setState(model.badgeStateOf(receipt, null)); setSettled(true); }
127
- }).catch(() => { if (aliveRef.current) setSettled(true); });
128
+ if (aliveRef.current && request === requestRef.current) { setState(model.badgeStateOf(receipt, null)); setSettled(true); }
129
+ }).catch(() => {
130
+ if (aliveRef.current && request === requestRef.current) { setState(null); setSettled(true); }
131
+ });
128
132
  }, [sessionId]);
129
133
  react.useEffect(() => {
134
+ ++requestRef.current;
135
+ setState(null);
130
136
  if (!sessionId || typeof props.fetchReceipt !== "function") {
131
- setState(null);
132
137
  setSettled(false);
133
138
  return undefined;
134
139
  }
135
140
  aliveRef.current = true;
136
141
  setSettled(false);
137
142
  refresh();
138
- // 12.5: a same-session frame refreshes this window's Badge; a
139
- // missing subscription degrades to an explicit refresh hint.
140
143
  if (typeof props.subscribeFrames === "function") {
141
144
  setConvergence(model.convergenceHintOf({ subscribed: true }));
142
145
  const stop = props.subscribeFrames((frame) => {
143
146
  if (model.frameRefreshesSession(frame, sessionId)) refresh();
144
147
  });
145
- return () => { aliveRef.current = false; if (typeof stop === "function") stop(); };
148
+ return () => { aliveRef.current = false; ++requestRef.current; if (typeof stop === "function") stop(); };
146
149
  }
147
150
  setConvergence(model.convergenceHintOf({ subscribed: false }));
148
- return () => { aliveRef.current = false; };
151
+ return () => { aliveRef.current = false; ++requestRef.current; };
149
152
  }, [sessionId, refresh]);
150
153
  // Close on outside click (edit-lock panel pattern): any pointerdown
151
154
  // outside this entry's wrapper dismisses the panel. Clicks INSIDE
@@ -168,15 +171,12 @@ window.__ModuleLoader__.load({
168
171
  const known = Boolean(state && state.known);
169
172
  const label = known
170
173
  ? t("capability.badge", "{skills} skills · {mcp} MCP").replace("{skills}", String(state.applied.skills)).replace("{mcp}", String(state.applied.mcpServers))
171
- : settled
172
- // The receipt fetch settled without a receipt: this session's
173
- // composition has no capability command (a non-Weir preset)
174
- // or it failed — an explicit n/a, never a perpetual spinner.
175
- ? t("capability.unavailable.short", "Capabilities n/a")
176
- : t("capability.loading", "Capabilities…");
177
- const compactLabel = known ? `${state.applied.skills} · ${state.applied.mcpServers}` : label;
178
- const hasWarning = Boolean(state && state.unavailable && state.unavailable.length > 0);
179
- const title = hasWarning ? state.unavailable.join("; ") : (convergence ?? (known ? label : undefined));
174
+ : settled && state?.degraded
175
+ ? t("capability.effectiveUnknown", "Applied count unknown — refresh and check Skill sources")
176
+ : settled ? t("capability.unavailable.short", "Capabilities n/a") : t("capability.loading", "Capabilities…");
177
+ const compactLabel = known ? `${state.applied.skills} · ${state.applied.mcpServers}` : state?.degraded ? t("capability.effectiveUnknownShort", "Skills ? · MCP ?") : label;
178
+ const hasWarning = Boolean(state?.degraded);
179
+ const title = hasWarning ? `${label}: ${state.warnings.join("; ")}` : (convergence ?? (known ? label : undefined));
180
180
  // D4: with the composition root's openPanel verb (a shell with a right
181
181
  // sidebar) activation opens the dedicated panel and closes any open
182
182
  // popover; without it the existing popover path runs unchanged.
@@ -539,6 +539,9 @@ window.__ModuleLoader__.load({
539
539
  // recompute dirty, Apply goes through /capabilities apply with the
540
540
  // draft's CAS revision and a fresh request ID.
541
541
  const [draft, setDraft] = react.useState(null);
542
+ const [receiptState, setReceiptState] = react.useState(null);
543
+ const receiptRequest = react.useRef(0);
544
+ const applying = react.useRef(false);
542
545
  const [commit, setCommit] = react.useState(null);
543
546
  // Managed MCP add form (8.1/8.2): per-field errors are derived by the
544
547
  // pure model (mcpAddErrorsOf) and shown after the first submit
@@ -546,13 +549,25 @@ window.__ModuleLoader__.load({
546
549
  // return crashes with React #310.
547
550
  const [addForm, setAddForm] = react.useState({ open: false, identity: "", label: "", command: "", args: "", showErrors: false, error: null, busy: false });
548
551
 
552
+ const refreshReceipt = () => {
553
+ if (typeof props.fetchReceipt !== "function") return;
554
+ const request = ++receiptRequest.current;
555
+ props.fetchReceipt(props.sessionId).then((receipt) => {
556
+ if (request !== receiptRequest.current) return;
557
+ setReceiptState(model.badgeStateOf(receipt, null));
558
+ setDraft(current => model.draftAfterRefresh(current, receipt));
559
+ }).catch(() => {
560
+ if (request === receiptRequest.current) {
561
+ setReceiptState(null);
562
+ setDraft(current => model.draftAfterRefresh(current, null));
563
+ }
564
+ });
565
+ };
549
566
  const refreshAll = () => {
550
567
  if (typeof props.fetchListing === "function") {
551
568
  props.fetchListing(props.sessionId).then((data) => setListing(data ?? { error: true })).catch(() => setListing({ error: true }));
552
569
  }
553
- if (typeof props.fetchReceipt === "function") {
554
- props.fetchReceipt(props.sessionId).then((receipt) => setDraft(receipt ? model.draftFromReceipt(receipt) : null)).catch(() => {});
555
- }
570
+ refreshReceipt();
556
571
  };
557
572
  react.useEffect(() => {
558
573
  let alive = true;
@@ -564,16 +579,16 @@ window.__ModuleLoader__.load({
564
579
  if (typeof props.fetchConditions === "function") {
565
580
  props.fetchConditions(props.sessionId).then((data) => { if (alive) setConditions(data); }).catch(() => {});
566
581
  }
567
- if (typeof props.fetchReceipt === "function") {
568
- props.fetchReceipt(props.sessionId).then((receipt) => { if (alive) setDraft(receipt ? model.draftFromReceipt(receipt) : null); }).catch(() => {});
569
- }
570
- return () => { alive = false; };
582
+ refreshReceipt();
583
+ return () => { alive = false; ++receiptRequest.current; };
571
584
  }, [props.sessionId]);
572
585
  const toggle = (kind, name) => {
573
- if (draft && commit?.phase !== "submitting") setDraft(model.draftToggle(draft, kind, name));
586
+ if (draft && commit?.phase !== "submitting") { setDraft(model.draftToggle(draft, kind, name)); setCommit(null); }
574
587
  };
575
588
  const applyDraft = () => {
576
- if (!draft || !draft.dirty || typeof props.applySelection !== "function") return;
589
+ const check = model.skillDraftConflicts(draft, listing);
590
+ if (applying.current || !draft || !draft.dirty || (draft.unknownBaseline && !draft.overwriteConfirmed) || check.selected.length || check.unresolved.some(item => item.state === "ambiguous") || typeof props.applySelection !== "function") return;
591
+ applying.current = true;
577
592
  const requestId = crypto.randomUUID();
578
593
  setCommit({ phase: "submitting", requestId });
579
594
  props.applySelection(props.sessionId, {
@@ -582,18 +597,18 @@ window.__ModuleLoader__.load({
582
597
  skills: draft.skills,
583
598
  mcpServers: draft.mcpServers,
584
599
  }).then((response) => {
600
+ applying.current = false;
585
601
  const outcome = model.commitOutcomeOf(response);
586
602
  setCommit(outcome);
587
603
  if (outcome.phase === "applied") {
604
+ setDraft(current => current ? { ...current, applied: { skills: [...current.skills], mcpServers: [...current.mcpServers], unresolvedRefs: [...current.unresolvedRefs] }, revision: outcome.revision, dirty: false, appliedDigest: undefined, appliedSourceIssue: "receipt" } : null);
588
605
  refreshAll();
589
- // The Badge holds its own receipt state — tell it to re-pull
590
- // so the composer counts follow the Apply immediately.
591
606
  if (typeof props.onApplied === "function") props.onApplied();
592
607
  }
593
- }).catch(() => setCommit({ phase: "failed", error: "apply request failed", draftKept: true }));
608
+ }).catch(() => { applying.current = false; setCommit({ phase: "failed", error: "apply request failed", draftKept: true }); });
594
609
  };
595
610
  const discardDraft = () => {
596
- if (draft) setDraft({ ...draft, skills: [...draft.applied.skills], mcpServers: [...draft.applied.mcpServers], dirty: false });
611
+ if (draft) setDraft({ ...draft, skills: [...draft.applied.skills], mcpServers: [...draft.applied.mcpServers], unresolvedRefs: [], dirty: false });
597
612
  setCommit(null);
598
613
  };
599
614
  // Load-into-draft staging (preset view): only the preset's resolved
@@ -601,7 +616,11 @@ window.__ModuleLoader__.load({
601
616
  // the session's applied set.
602
617
  const stagePreset = (document) => {
603
618
  if (commit?.phase === "submitting") return;
604
- setDraft(model.draftFromPreset(document, draft));
619
+ const resolveName = (name) => {
620
+ const matches = (listing?.skills ?? []).filter(row => row.status === "parsed" && row.name === name && row.identity);
621
+ return matches.length === 1 ? matches[0].identity : undefined;
622
+ };
623
+ setDraft(model.draftFromPreset(document, draft, resolveName));
605
624
  setCommit(null);
606
625
  };
607
626
  // Managed MCP add form (8.1/8.2): registers a stdio server and mounts
@@ -670,7 +689,9 @@ window.__ModuleLoader__.load({
670
689
  icon: "warn",
671
690
  color: DANGER,
672
691
  title: t("capability.error", "Capabilities unavailable for this session."),
673
- description: t("capability.state.errorDesc", "The capability listing could not be loaded.")
692
+ description: (draft?.skills ?? []).some(entry => typeof entry === "string")
693
+ ? t("capability.inventoryUnavailableName", "The listing is unavailable; legacy names remain unresolved in the draft. Retry later; no source was selected.")
694
+ : t("capability.state.errorDesc", "The capability listing could not be loaded.")
674
695
  })
675
696
  });
676
697
  }
@@ -697,6 +718,22 @@ window.__ModuleLoader__.load({
697
718
  }
698
719
  const partition = model.partitionManagerListing(listing);
699
720
  const skillRows = model.groupSkillRows(model.filterSkillRows(partition.skills, query));
721
+ const skillKeys = new Set((draft?.skills ?? []).map(model.skillEntryKeyOf));
722
+ const check = model.skillDraftConflicts(draft, listing);
723
+ const sourceLabels = model.skillSourceLabels(partition.skills);
724
+ const sourceOf = identity => (sourceLabels[model.skillIdentityKeyOf(identity)] ?? t("capability.sourceUnknown", "unknown source"))
725
+ .replace(" · source ", t("capability.sourceNumber", " · source "));
726
+ const selectedKeys = new Set(check.selected.flatMap(item => item.identities.map(model.skillEntryKeyOf)));
727
+ const ambiguous = check.unresolved.filter(item => item.state === "ambiguous");
728
+ const unknown = check.unresolved.filter(item => item.state === "unknown");
729
+ const replaceName = (name, identity) => { if (commit?.phase !== "submitting") { setDraft(model.resolveSkillName(draft, name, identity)); setCommit(null); } };
730
+ const warningText = [
731
+ ...check.selected.map(item => t("capability.sameNameWarning", "{name}: only one source can be enabled in this session. Uncheck one: {sources}.")
732
+ .replace("{name}", item.name).replace("{sources}", item.identities.map(sourceOf).join(", "))),
733
+ ...ambiguous.map(item => t("capability.unresolvedNameWarning", "{name}: legacy name has no chosen source. Choose an exact source or remove the name: {sources}.")
734
+ .replace("{name}", item.name).replace("{sources}", item.candidates.map(sourceOf).join(", "))),
735
+ ...unknown.map(item => t("capability.unknownLegacyName", "{name}: no matching Skill in the current listing. Refresh the listing or remove this legacy name.").replace("{name}", item.name))
736
+ ];
700
737
  const draftHas = (kind, name) => draft !== null && draft[kind].includes(name);
701
738
  const selectionDisabled = draft === null || commit?.phase === "submitting";
702
739
  const fieldError = (code) => code === "invalid"
@@ -752,8 +789,10 @@ window.__ModuleLoader__.load({
752
789
  ] })
753
790
  ] })
754
791
  ] }) : null;
755
- const skillRow = (skill) => {
756
- const selected = draftHas("skills", skill.name);
792
+ const skillRow = (skill, index) => {
793
+ const selected = skillKeys.has(model.skillEntryKeyOf(skill.identity ?? skill.name));
794
+ const selectedConflict = selectedKeys.has(model.skillEntryKeyOf(skill.identity));
795
+ const source = skill.identity ? sourceOf(skill.identity) : skill.source;
757
796
  const rest = selected ? tint(ACCENT, 8) : "transparent";
758
797
  return react_jsx_runtime.jsxs("label", {
759
798
  style: { ...S.row.base, background: rest },
@@ -765,20 +804,21 @@ window.__ModuleLoader__.load({
765
804
  style: S.row.checkbox,
766
805
  checked: selected,
767
806
  disabled: selectionDisabled,
768
- "aria-label": skill.name,
769
- onChange: () => toggle("skills", skill.name)
807
+ "aria-label": `${skill.name} — ${source}`,
808
+ "aria-invalid": selectedConflict || undefined,
809
+ onChange: () => toggle("skills", skill.identity ?? skill.name)
770
810
  }),
771
811
  react_jsx_runtime.jsxs("span", { style: S.row.text, children: [
772
812
  react_jsx_runtime.jsxs("span", { style: S.row.nameLine, children: [
773
813
  react_jsx_runtime.jsx("span", { style: S.row.name, title: skill.name, children: skill.name }),
774
- react_jsx_runtime.jsx("span", { style: { ...S.tag.base, ...S.tag.scope(skill.scopeKey) }, children: skill.source }),
775
- skill.conflict ? react_jsx_runtime.jsx("span", { style: { ...S.tag.base, ...S.tag.warn }, children: t("capability.conflict", "conflict") }) : null,
814
+ react_jsx_runtime.jsx("span", { style: { ...S.tag.base, ...S.tag.scope(skill.scopeKey) }, children: source }),
815
+ selectedConflict || skill.conflict ? react_jsx_runtime.jsx("span", { style: { ...S.tag.base, ...S.tag.warn }, children: t("capability.conflict", "conflict") }) : null,
776
816
  skill.missing ? react_jsx_runtime.jsx("span", { style: { ...S.tag.base, ...S.tag.danger }, title: skill.status, children: t("capability.missing", "missing") }) : null
777
817
  ] }),
778
818
  skill.description ? react_jsx_runtime.jsx("span", { style: S.row.desc, title: skill.description, children: skill.description }) : null
779
819
  ] })
780
820
  ]
781
- }, skill.name);
821
+ }, `${model.skillIdentityKeyOf(skill.identity) ?? `${skill.scopeKey}:${skill.name}`}:${index}`);
782
822
  };
783
823
  const mcpRow = (server) => {
784
824
  const name = server.identity ?? server.serverName ?? "unknown";
@@ -819,11 +859,16 @@ window.__ModuleLoader__.load({
819
859
  ]
820
860
  }, name);
821
861
  };
862
+ const legacyActions = [...ambiguous, ...unknown].map(item => react_jsx_runtime.jsxs("div", { style: S.list.card, children: [
863
+ react_jsx_runtime.jsx("div", { style: S.form.title, children: t("capability.unresolvedNameTitle", "Choose a source for legacy name {name}").replace("{name}", item.name) }),
864
+ react_jsx_runtime.jsxs("div", { style: S.form.actions, children: [
865
+ ...item.candidates.map(identity => react_jsx_runtime.jsx("button", { type: "button", style: S.form.ghost, ...ring, disabled: selectionDisabled, onClick: () => replaceName(item.name, identity), children: t("capability.replaceName", "Replace {name} with {source}").replace("{name}", item.name).replace("{source}", sourceOf(identity)) }, model.skillIdentityKeyOf(identity))),
866
+ react_jsx_runtime.jsx("button", { type: "button", style: S.form.ghost, ...ring, disabled: selectionDisabled, onClick: () => replaceName(item.name, null), children: t("capability.removeName", "Remove legacy name {name}").replace("{name}", item.name) })
867
+ ] })
868
+ ] }, item.name));
822
869
  const body = tab === "skills"
823
- ? (skillRows.length === 0
824
- ? react_jsx_runtime.jsx(StatePanel, { icon: "search", title: t("capability.noSkills", "No skills match.") })
825
- : react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
826
- children: skillRows.map((group) => react_jsx_runtime.jsxs("div", {
870
+ ? react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
871
+ children: [...(skillRows.length === 0 ? [react_jsx_runtime.jsx(StatePanel, { icon: "search", title: t("capability.noSkills", "No skills match.") })] : skillRows.map((group) => react_jsx_runtime.jsxs("div", {
827
872
  style: S.list.group,
828
873
  children: [
829
874
  react_jsx_runtime.jsxs("div", {
@@ -835,8 +880,8 @@ window.__ModuleLoader__.load({
835
880
  }),
836
881
  ...group.rows.map(skillRow)
837
882
  ]
838
- }, group.key))
839
- }))
883
+ }, group.key))), ...legacyActions]
884
+ })
840
885
  : tab === "mcp"
841
886
  ? react_jsx_runtime.jsxs(react_jsx_runtime.Fragment, {
842
887
  children: [
@@ -895,7 +940,7 @@ window.__ModuleLoader__.load({
895
940
  // Footer commit bar (12.3): the dirty diff summary (+n/−m) ahead of
896
941
  // the same Apply/Discard/commit-phase semantics as before.
897
942
  const diff = draft ? model.draftDiffOf(draft) : null;
898
- const diffParts = diff && diff.any
943
+ const diffParts = diff && diff.any && !draft?.unknownBaseline
899
944
  ? [
900
945
  diff.skillsAdded > 0 ? { added: true, text: t("capability.diff.addSkills", "+{n} skills").replace("{n}", String(diff.skillsAdded)) } : null,
901
946
  diff.skillsRemoved > 0 ? { added: false, text: t("capability.diff.removeSkills", "−{n} skills").replace("{n}", String(diff.skillsRemoved)) } : null,
@@ -903,19 +948,19 @@ window.__ModuleLoader__.load({
903
948
  diff.mcpRemoved > 0 ? { added: false, text: t("capability.diff.removeMcp", "−{n} MCP").replace("{n}", String(diff.mcpRemoved)) } : null
904
949
  ].filter(Boolean)
905
950
  : null;
906
- const applyDisabled = !draft || !draft.dirty || commit?.phase === "submitting";
951
+ const applyDisabled = !draft || !draft.dirty || (draft.unknownBaseline && !draft.overwriteConfirmed) || commit?.phase === "submitting" || check.selected.length > 0 || ambiguous.length > 0;
907
952
  // Session state summary (sidebar header only): the APPLIED counts,
908
953
  // same template the composer Badge renders.
909
- const appliedSummary = draft !== null
910
- ? t("capability.badge", "{skills} skills · {mcp} MCP").replace("{skills}", String(draft.applied.skills.length)).replace("{mcp}", String(draft.applied.mcpServers.length))
911
- : null;
954
+ const appliedSummary = receiptState?.known
955
+ ? t("capability.badge", "{skills} skills · {mcp} MCP").replace("{skills}", String(receiptState.applied.skills)).replace("{mcp}", String(receiptState.applied.mcpServers))
956
+ : receiptState?.degraded ? t("capability.effectiveUnknownShort", "Skills ? · MCP ?") : null;
912
957
  return react_jsx_runtime.jsxs("div", { style: frameStyle, children: [
913
958
  react_jsx_runtime.jsxs("div", { style: S.header.row, children: [
914
959
  react_jsx_runtime.jsx("button", { type: "button", style: S.tabs.segment(tab === "skills", "first"), ...ring, onClick: () => setTab("skills"), "aria-pressed": tab === "skills", children: t("capability.tab.skills", "Skills") }),
915
960
  react_jsx_runtime.jsx("button", { type: "button", style: S.tabs.segment(tab === "mcp", "middle"), ...ring, onClick: () => setTab("mcp"), "aria-pressed": tab === "mcp", children: t("capability.tab.mcp", "MCP") }),
916
961
  react_jsx_runtime.jsx("button", { type: "button", style: S.tabs.segment(tab === "presets", "last"), ...ring, onClick: () => setTab("presets"), "aria-pressed": tab === "presets", children: t("capability.tab.presets", "Presets") }),
917
962
  props.shell === "sidebar" && appliedSummary
918
- ? react_jsx_runtime.jsx("span", { style: S.header.summary, children: appliedSummary })
963
+ ? react_jsx_runtime.jsx("span", { style: S.header.summary, title: receiptState?.degraded ? t("capability.effectiveUnknown", "Applied count unknown — refresh and check Skill sources") : undefined, children: appliedSummary })
919
964
  : null,
920
965
  react_jsx_runtime.jsxs("span", {
921
966
  style: S.search.wrap,
@@ -944,8 +989,12 @@ window.__ModuleLoader__.load({
944
989
  ...(props.shell === "sidebar"
945
990
  ? [react_jsx_runtime.jsxs("div", { style: S.list.scroll, children: [addBlock, body] })]
946
991
  : [addBlock, body]),
992
+ receiptState?.degraded ? react_jsx_runtime.jsx("div", { role: "status", "aria-live": "polite", style: { color: WARN_TEXT, lineHeight: "16px", padding: "6px" }, children: t("capability.effectiveUnknownDetail", "Accepted revision; effective capabilities cannot be verified. Refresh and check Skill sources. Draft kept; only Apply changes the session.") }) : null,
947
993
  draft !== null ? react_jsx_runtime.jsxs("div", { style: S.footer.bar, children: [
948
994
  diffParts ? react_jsx_runtime.jsx("span", { style: S.footer.diff, "data-weir-capability-diff": "", children: diffParts.map((part, index) => react_jsx_runtime.jsx("span", { style: S.footer.diffBadge(part.added), children: part.text }, index)) }) : null,
995
+ warningText.length ? react_jsx_runtime.jsx("div", { role: "alert", "aria-live": "assertive", style: { width: "100%", color: WARN_TEXT, lineHeight: "16px" }, children: warningText.join(" ") }) : null,
996
+ draft.unknownBaseline ? react_jsx_runtime.jsx("div", { role: "alert", style: { width: "100%", color: WARN_TEXT, lineHeight: "16px" }, children: t("capability.unknownOverwriteRisk", "Previous selection is unknown. Applying this draft may replace other accepted Skills and MCP servers. Review the full selection before confirming.") }) : null,
997
+ draft.unknownBaseline && draft.dirty && !draft.overwriteConfirmed ? react_jsx_runtime.jsx("button", { type: "button", style: S.footer.discard, ...ring, disabled: commit?.phase === "submitting", onClick: () => setDraft(current => current ? { ...current, overwriteConfirmed: true } : null), children: t("capability.confirmUnknownOverwrite", "Confirm replacing unknown selection") }) : null,
949
998
  react_jsx_runtime.jsx("button", {
950
999
  type: "button",
951
1000
  style: applyDisabled ? { ...S.footer.apply, ...S.footer.disabled } : S.footer.apply,
@@ -956,11 +1005,13 @@ window.__ModuleLoader__.load({
956
1005
  children: commit?.phase === "submitting" ? t("capability.applying", "Applying…") : t("capability.apply", "Apply")
957
1006
  }),
958
1007
  draft.dirty ? react_jsx_runtime.jsx("button", { type: "button", style: S.footer.discard, ...ring, ...hover(HOVER_BG, "none"), onClick: discardDraft, children: t("capability.discard", "Discard") }) : null,
959
- commit?.phase === "applied" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(SUCCESS), children: t("capability.applied", "Applied") }) : null,
1008
+ commit?.phase === "applied" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(receiptState?.degraded ? WARN : SUCCESS), children: receiptState?.degraded ? t("capability.acceptedUnknown", "Accepted · effective count unknown") : t("capability.applied", "Applied") }) : null,
960
1009
  commit?.phase === "applied" && Array.isArray(commit.skipped) && commit.skipped.length > 0
961
1010
  ? react_jsx_runtime.jsx("span", { style: S.footer.chip(WARN), title: commit.skipped.map(entry => `${entry.name} — ${entry.reason}`).join("\n"), children: t("capability.appliedSkipped", "skipped {n} (not applicable here)").replace("{n}", String(commit.skipped.length)) })
962
1011
  : null,
963
- commit?.phase === "failed" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(DANGER), title: commit.error, children: t("capability.failed", "Failed — draft kept") }) : null,
1012
+ commit?.phase === "failed" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(DANGER), role: "alert", children: t("capability.failed", "Failed — draft kept") }) : null,
1013
+ commit?.phase === "same-name-conflict" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(DANGER), role: "alert", children: t("capability.serverSameName", "Server rejected same-name Skill {names}. Uncheck one source, or refresh the listing; draft kept.").replace("{names}", commit.names.join(", ") || t("capability.unknownName", "unknown name")) }) : null,
1014
+ commit?.phase === "missing" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(WARN), role: "alert", children: t("capability.serverMissing", "Server could not resolve {names}. Refresh the listing and choose an exact source or remove the name; draft kept.").replace("{names}", commit.names.join(", ") || t("capability.unknownName", "unknown name")) }) : null,
964
1015
  commit?.phase === "revision-conflict" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(WARN), children: t("capability.conflictState", "Changed elsewhere — review current state") }) : null,
965
1016
  commit?.phase === "install-or-configure" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(ACCENT), title: (commit.missing ?? []).join(", "), children: t("capability.missingAction", "Missing items need install/configure") }) : null,
966
1017
  commit?.phase === "indeterminate" ? react_jsx_runtime.jsx("span", { style: S.footer.chip(MUTED), children: t("capability.pending", "Result pending — query the receipt") }) : null