weir-harness 1.2.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/cordis.patch.yml +7 -18
  2. package/lib/client.capability-badge.js +2 -0
  3. package/lib/client.capability-manager.js +46 -179
  4. package/lib/client.capability-model.js +428 -3
  5. package/lib/client.file-lease-badge.js +307 -0
  6. package/lib/client.file-lease-tool-model.js +189 -0
  7. package/lib/client.file-lease-tool-view.js +434 -0
  8. package/lib/client.js +124 -36
  9. package/lib/client.mcp-manager.js +563 -0
  10. package/lib/client.settings-page.js +10 -34
  11. package/package.json +2 -2
  12. package/skills/mcp-server-setup/SKILL.md +124 -0
  13. package/src/capabilities/apply-engine.js +18 -4
  14. package/src/capabilities/capability-remote.js +296 -45
  15. package/src/capabilities/initial-selection.js +3 -2
  16. package/src/capabilities/lifecycle-preload.js +69 -11
  17. package/src/capabilities/lifecycle-snapshot.js +127 -39
  18. package/src/capabilities/mcp-admission.js +2 -2
  19. package/src/capabilities/mcp-config.js +344 -0
  20. package/src/capabilities/mcp-credentials.js +180 -0
  21. package/src/capabilities/mcp-diagnostics.js +169 -0
  22. package/src/capabilities/mcp-drain.js +18 -4
  23. package/src/capabilities/mcp-facade.js +3 -3
  24. package/src/capabilities/mcp-gate.js +51 -52
  25. package/src/capabilities/mcp-install-tool.js +276 -0
  26. package/src/capabilities/mcp-manager.js +368 -142
  27. package/src/capabilities/mcp-mount.js +219 -79
  28. package/src/capabilities/mcp-operations.js +359 -0
  29. package/src/capabilities/mcp-process.js +43 -0
  30. package/src/capabilities/mcp-registry.js +74 -60
  31. package/src/capabilities/mcp-runtime.js +103 -0
  32. package/src/capabilities/read-payloads.js +8 -3
  33. package/src/capabilities/skill-applicability.js +185 -0
  34. package/src/capabilities/skill-builtin-migration.js +2 -1
  35. package/src/capabilities/skill-selection-plugin.js +161 -56
  36. package/src/capabilities/store/checked-contract.d.ts +2 -2
  37. package/src/capabilities/store/fs-adapter.js +2 -3
  38. package/src/context-guard/index.js +2 -2
  39. package/src/delegate/supervision-mount.js +5 -5
  40. package/src/delegate/tool.js +2 -10
  41. package/src/file-lease/client.js +1299 -0
  42. package/src/file-lease/index.js +34 -0
  43. package/src/file-lease/lineage.js +207 -0
  44. package/src/file-lease/node-contracts.d.ts +42 -0
  45. package/src/file-lease/plugin.js +1013 -0
  46. package/src/file-lease/singleton.js +155 -0
  47. package/src/file-lease/spawn.js +62 -0
  48. package/src/file-lease/tools.js +847 -0
  49. package/src/file-lease/view.js +144 -0
  50. package/src/file-lease/watch.js +473 -0
  51. package/src/hashline-edit/index.js +76 -42
  52. package/src/hashline-edit/write-tool.js +90 -0
  53. package/src/intent-gate/index.js +29 -12
  54. package/src/intent-gate/matcher.js +36 -2
  55. package/src/lease-broker/entry.js +98 -0
  56. package/src/lease-broker/node-contracts.d.ts +63 -0
  57. package/src/lease-broker/protocol.js +237 -0
  58. package/src/lease-broker/server.js +402 -0
  59. package/src/lease-broker/state.js +482 -0
  60. package/src/lsp/tools.js +137 -27
  61. package/src/process/index.js +4 -2
  62. package/src/settings/index.js +2 -44
  63. package/src/settings/sections.js +2 -24
  64. package/src/shared/audit.js +9 -1
  65. package/src/shared/child-scope.js +2 -0
  66. package/src/todo-driver/index.js +1 -6
  67. package/src/worktree/cards.js +6 -21
  68. package/src/worktree/lane-core.js +1 -19
  69. package/src/worktree/lane-workflows.js +11 -32
  70. package/src/worktree/rules.js +1 -1
  71. package/lib/client.edit-lock-maintenance.js +0 -592
  72. package/lib/client.edit-lock-panel.js +0 -405
  73. package/src/edit-lock/adapter.js +0 -57
  74. package/src/edit-lock/admin-ledger.js +0 -149
  75. package/src/edit-lock/admin-recovery.js +0 -151
  76. package/src/edit-lock/admission.js +0 -82
  77. package/src/edit-lock/binding-attestation.js +0 -29
  78. package/src/edit-lock/binding-guidance.js +0 -72
  79. package/src/edit-lock/binding-machine.js +0 -312
  80. package/src/edit-lock/call-context.js +0 -65
  81. package/src/edit-lock/derived-facts.js +0 -67
  82. package/src/edit-lock/domain.js +0 -165
  83. package/src/edit-lock/domains.js +0 -170
  84. package/src/edit-lock/host.js +0 -149
  85. package/src/edit-lock/incarnations.js +0 -150
  86. package/src/edit-lock/index.js +0 -1303
  87. package/src/edit-lock/inspect.js +0 -219
  88. package/src/edit-lock/lifecycle.js +0 -579
  89. package/src/edit-lock/limits.js +0 -61
  90. package/src/edit-lock/maintenance.js +0 -310
  91. package/src/edit-lock/manager.js +0 -875
  92. package/src/edit-lock/negotiation.js +0 -132
  93. package/src/edit-lock/operation-history.js +0 -214
  94. package/src/edit-lock/peer-client.js +0 -86
  95. package/src/edit-lock/peer-transport.js +0 -76
  96. package/src/edit-lock/peer.js +0 -87
  97. package/src/edit-lock/publisher.js +0 -191
  98. package/src/edit-lock/read-authority.js +0 -101
  99. package/src/edit-lock/recovery.js +0 -139
  100. package/src/edit-lock/remote.js +0 -362
  101. package/src/edit-lock/request-binding.js +0 -42
  102. package/src/edit-lock/request-data.js +0 -40
  103. package/src/edit-lock/reservation.js +0 -301
  104. package/src/edit-lock/reserved-runtime.js +0 -45
  105. package/src/edit-lock/resource-identity.js +0 -130
  106. package/src/edit-lock/runtime.js +0 -145
  107. package/src/edit-lock/settle.js +0 -144
  108. package/src/edit-lock/snapshot.js +0 -175
  109. package/src/edit-lock/stale-sweep.js +0 -120
  110. package/src/edit-lock/state.js +0 -462
  111. package/src/edit-lock/store.js +0 -327
  112. package/src/edit-lock/tool-scope.js +0 -29
  113. package/src/edit-lock/view.js +0 -170
  114. package/src/edit-lock/write-scopes.js +0 -48
  115. package/src/edit-lock/write-tool.js +0 -45
  116. package/src/shared/delegate-edit-lifecycle.js +0 -36
  117. package/src/worktree/authority.js +0 -56
@@ -475,9 +475,11 @@ export function spawnArgv(command, args = [], platform = process.platform, optio
475
475
  // Unknown shim, but nothing here can be re-parsed: let cmd run it.
476
476
  return ['cmd.exe', '/d', '/c', command, ...args]
477
477
  }
478
+ const subject = options.subject ?? 'lsp'
479
+ const setting = options.setting ?? 'lspServers'
478
480
  throw new Error(
479
- `lsp: cannot start '${command}' on Windows — its .cmd shim could not be unwrapped and the path or an argument contains characters cmd.exe would re-parse. ` +
480
- `Install the server as a plain .exe, or point lspServers at its node entry point.`,
481
+ `${subject}: cannot start '${command}' on Windows — its .cmd shim could not be unwrapped and the path or an argument contains characters cmd.exe would re-parse. ` +
482
+ `Install the server as a plain .exe, or point ${setting} at its node entry point.`,
481
483
  )
482
484
  }
483
485
  return [command, ...args]
@@ -14,32 +14,14 @@ import { configValue } from './volatile.js'
14
14
  import { computeSections } from './sections.js'
15
15
  import { wireLspAdmin } from '../lsp/admin.js'
16
16
  import { wireEnvAdmin } from './env-admin.js'
17
- import { createEditLockEvidence, wireEditLockMaintenance } from '../edit-lock/maintenance.js'
18
- import { managementRootFor } from '../edit-lock/domains.js'
19
- import { createAudit, AUDIT_TYPES } from '../shared/audit.js'
20
-
21
- // Host lifetime, not settings-row lifetime: remounting settings must not forget
22
- // still-live preset rows. Weak keys isolate hosts and release stopped hosts.
23
- const evidenceByHost = new WeakMap()
24
17
 
25
18
  export { Config } from './sections.js'
26
19
 
27
20
  const name = 'weir-settings'
28
21
  const inject = []
29
22
 
30
- // Explicit resolver seam keeps root-discovery tests away from ancestor authorities.
31
- export function maintenanceRoots(agents, resolveRoot = managementRootFor) {
32
- const roots = new Set()
33
- for (const agent of agents?.roots?.() ?? []) {
34
- const cwd = agent.session?.header?.cwd
35
- if (typeof cwd !== 'string') continue
36
- try { roots.add(resolveRoot(cwd)) } catch { /* unavailable root */ }
37
- }
38
- return [...roots]
39
- }
40
- /** @param {{ resolveRoot?: typeof managementRootFor }} [dependencies] */
41
- export function createSettingsPlugin({ resolveRoot = managementRootFor } = {}) {
42
23
  // Arrow apply: cordis must retain the returned endpoint disposer, not construct it.
24
+ export function createSettingsPlugin() {
43
25
  return (ctx, config = {}) => {
44
26
  // The whitelist defaults cache: the defaults file is read once per process,
45
27
  // and an explicit reload entry (robashDefaultsReload) clears it. Nothing here
@@ -55,21 +37,9 @@ return (ctx, config = {}) => {
55
37
  // Compute eagerly: malformed categoryChains/lspServers/robash lists fail
56
38
  // activation loud.
57
39
  compute()
58
- const host = ctx.root ?? ctx
59
- let editLockEvidence = evidenceByHost.get(host)
60
- if (!editLockEvidence) {
61
- editLockEvidence = createEditLockEvidence()
62
- evidenceByHost.set(host, editLockEvidence)
63
- }
64
- const candidateRoots = () => maintenanceRoots(ctx.get?.('agents'), resolveRoot)
65
- const savedEnabled = () => compute().editLock?.enabled === true
66
- const audit = createAudit(ctx)
67
- let lastEnabled = savedEnabled()
68
-
69
40
  const listeners = new Set()
70
41
  // Host-plane service: preset modules read their override section on demand.
71
42
  ctx.reflect.provide('weirSettings', {
72
- editLockEvidence,
73
43
  get(key) {
74
44
  return compute()[key]
75
45
  },
@@ -97,17 +67,6 @@ return (ctx, config = {}) => {
97
67
  observeReloadEntry()
98
68
  ctx.on('loader/volatile-update', () => {
99
69
  observeReloadEntry()
100
- const enabled = savedEnabled()
101
- if (enabled !== lastEnabled) {
102
- lastEnabled = enabled
103
- // Profile intent has no session log. Mirror only when a server-derived
104
- // workspace exists; no cwd fallback to the developer's current directory.
105
- const data = { kind: enabled ? 'enable-requested' : 'disable-requested', scope: 'profile', appliesAfterRestart: true }
106
- let roots = []
107
- try { roots = candidateRoots() } catch { /* emit remains available */ }
108
- if (roots.length) for (const root of roots) audit(null, AUDIT_TYPES.editLockMaintenance, data, { root })
109
- else audit(null, AUDIT_TYPES.editLockMaintenance, data)
110
- }
111
70
  for (const callback of listeners) callback()
112
71
  })
113
72
 
@@ -126,11 +85,10 @@ return (ctx, config = {}) => {
126
85
  // built-in catalog.
127
86
  const offLspAdmin = wireLspAdmin(ctx, () => compute().lsp?.servers)
128
87
 
129
- const offMaintenance = wireEditLockMaintenance(ctx, { savedEnabled, evidence: editLockEvidence, candidateRoots })
130
88
  // Environment facts endpoint (settings-page `when: { env }` rows): same
131
89
  // settings-row wiring as the LSP endpoints above (S19).
132
90
  const offEnvAdmin = wireEnvAdmin(ctx)
133
- return () => { offMaintenance(); offLspAdmin(); offEnvAdmin(); listeners.clear() }
91
+ return () => { offLspAdmin(); offEnvAdmin(); listeners.clear() }
134
92
  }
135
93
  }
136
94
  const apply = createSettingsPlugin()
@@ -43,17 +43,6 @@ const FIELDS = [
43
43
  { key: 'guardSoftThreshold', section: 'contextGuard', field: 'softThreshold', type: 'number', description: 'Soft pressure threshold (live advisory; 0 < soft < hard < 1)' },
44
44
  { key: 'guardHardThreshold', section: 'contextGuard', field: 'hardThreshold', type: 'number', description: 'Hard pressure threshold (live maintenance request; not a completion guarantee)' },
45
45
  { key: 'hashlineHideStockEdit', section: 'hashlineEdit', field: 'hideStockEdit', type: 'boolean', description: 'Hide the stock edit tool (hash_edit only)', restart: true },
46
- { key: 'editLockEnabled', section: 'editLock', field: 'enabled', type: 'boolean', description: 'Edit Lock cross-session file ownership (default on; explicit off applies after restart)', restart: true },
47
- // Retention (design D1/D2): one lock batch shares one cumulative allowance, so a
48
- // session reads one expiry instead of N countdowns. These keys are policy, not
49
- // mechanism: the kernel never sees them, the lifecycle resolves them per call.
50
- { key: 'editLockHoldDefaultMinutes', section: 'editLock', field: 'holdDefaultMinutes', type: 'number', description: 'Edit Lock: retention minutes used when a session asks to keep its locks without giving a period (default 30)', page: false },
51
- { key: 'editLockHoldSingleMaxMinutes', section: 'editLock', field: 'holdSingleMaxMinutes', type: 'number', description: 'Edit Lock: most minutes one retention request may ask for (default 30)', page: false },
52
- { key: 'editLockHoldCumulativeMaxMinutes', section: 'editLock', field: 'holdCumulativeMaxMinutes', type: 'number', description: 'Edit Lock: cumulative retention minutes one batch of locks may use; asks beyond it are refused and only release remains (default 120)', page: false },
53
- { key: 'editLockNudgeAttempts', section: 'editLock', field: 'nudgeAttempts', type: 'number', description: 'Edit Lock: how many times a finished turn is continued to ask for still-held locks to be released or retained (default 2)', page: false },
54
- { key: 'editLockNudgeFallback', section: 'editLock', field: 'nudgeFallback', type: { union: ['release', 'abnormal'] }, description: 'Edit Lock: disposition of locks still held after those notices are used up; release frees the files for other sessions, abnormal keeps them for the user (default release)', page: false },
55
- { key: 'editLockAutoResume', section: 'editLock', field: 'autoResume', type: 'boolean', description: 'Edit Lock: a genuine user message automatically resumes a stopped or administratively revoked session and retries a duplicate-binding failure (default on)' },
56
- { key: 'editLockStaleSweep', section: 'editLock', field: 'staleSweep', type: 'boolean', description: 'Edit Lock: a genuine user message silently releases locks whose target file no longer exists (default on; read per message, applies immediately)' },
57
46
  { key: 'robashEnabled', section: 'robash', field: 'enabled', type: 'boolean', description: 'Guarded read-only bash for curated agents (master switch)' },
58
47
  { key: 'robashAllow', section: 'robash', field: 'allow', type: 'string', list: true, description: 'JSON array of command names to APPEND to the product-default bash allow list (empty adds nothing; the defaults are always in effect)' },
59
48
  { key: 'robashGitAllow', section: 'robash', field: 'gitAllow', type: 'string', list: true, description: 'JSON array of git subcommands to APPEND to the product-default git allow list (empty adds nothing; the defaults are always in effect)' },
@@ -98,8 +87,8 @@ export { FIELDS }
98
87
 
99
88
  /** Product defaults of the flat settings keys, mirrored from the consuming
100
89
  * modules' code defaults (intent-gate classifier, delegate supervision,
101
- * todo driver, context guard, edit-lock retention, robash, lsp, worktree,
102
- * notify). This is the canonical declaration: the cordis.patch.yml
90
+ * todo driver, context guard, robash, lsp, worktree, notify). This is the
91
+ * canonical declaration: the cordis.patch.yml
103
92
  * weir-settings row carries the same values so saved profiles resolve
104
93
  * them, and the settings page mirrors this table client-side to SHOW the
105
94
  * effective default of an unset field (both directions pinned by test).
@@ -121,14 +110,6 @@ export const FIELD_DEFAULTS = Object.freeze({
121
110
  guardSoftThreshold: 0.72,
122
111
  guardHardThreshold: 0.88,
123
112
  hashlineHideStockEdit: true,
124
- editLockEnabled: true,
125
- editLockHoldDefaultMinutes: 30,
126
- editLockHoldSingleMaxMinutes: 30,
127
- editLockHoldCumulativeMaxMinutes: 120,
128
- editLockNudgeAttempts: 2,
129
- editLockNudgeFallback: 'release',
130
- editLockAutoResume: true,
131
- editLockStaleSweep: true,
132
113
  robashEnabled: true,
133
114
  lspEnabled: false,
134
115
  lspIdleMs: 600000,
@@ -360,9 +341,6 @@ export function computeSections(config, { readDefaults } = {}) {
360
341
  }
361
342
  }
362
343
  }
363
- // A saved profile row replaces the bundle row wholesale. An omitted key
364
- // must still activate protection; a saved false remains an explicit opt-out.
365
- if (key === 'editLock' && out.enabled === undefined) out.enabled = FIELD_DEFAULTS.editLockEnabled
366
344
  if (key === 'contextGuard') guardOptions(out)
367
345
  if (key === 'lsp' && typeof out.servers === 'string' && out.servers.trim().length > 0) {
368
346
  out.servers = parseJsonField(validateLspServers, out.servers)
@@ -23,7 +23,6 @@ export const AUDIT_TYPES = Object.freeze({
23
23
  compaction: 'compaction',
24
24
  supervision: 'supervision',
25
25
  worktree: 'worktree',
26
- editLockMaintenance: 'edit-lock-maintenance',
27
26
  // Session capability manager (tasks 4.2+): one record per durable Apply
28
27
  // acceptance or receipt-query recovery; never commit evidence by itself.
29
28
  capabilityApply: 'capability-apply',
@@ -35,6 +34,9 @@ export const AUDIT_TYPES = Object.freeze({
35
34
  // identity, feed census and provider status, never skill names; the receipt
36
35
  // itself stays the only effective-state authority.
37
36
  capabilityRead: 'capability-read',
37
+ // MCP management mutations: operation/status/identity/generation/incarnation
38
+ // only. Never config, credential refs or values, argv, URLs, or raw errors.
39
+ capabilityMcp: 'capability-mcp',
38
40
  // The pending model-facing capability notice (durable-pending-notice D7):
39
41
  // queue / degrade / deliver / clear outcomes of the durable notice unit.
40
42
  // Counts only, never names — the same rule as capability-read above; the
@@ -48,6 +50,12 @@ export const AUDIT_TYPES = Object.freeze({
48
50
  // spawn carrying the composition the child's creation window applies, so a
49
51
  // host restart can replay it — the shim itself is runtime-only state.
50
52
  delegate: 'delegate',
53
+ // File-lease degraded managed writes (edit-lock-rebuild D7): one record per
54
+ // degradation, carrying the kind, the owner (session and lineage), the path
55
+ // COUNT and the failure reason — never path contents. The lease client owns
56
+ // the per-process rate limit (one record per window); the emit site adds no
57
+ // throttle of its own and a failed emit never blocks a write.
58
+ fileLease: 'file-lease',
51
59
  })
52
60
  /**
53
61
  * Known dynamic sub-event kinds: `<type>/<kind>` events emitted beside a base
@@ -44,6 +44,7 @@ export const CHILD_DENY_TOOLS = [
44
44
  'worktree_check',
45
45
  'exit_plan_mode',
46
46
  'ask_user_question',
47
+ 'mcp_install',
47
48
  'present',
48
49
  'edit_lock_acquire',
49
50
  'edit_lock_hold',
@@ -51,6 +52,7 @@ export const CHILD_DENY_TOOLS = [
51
52
  'edit_lock_release',
52
53
  'edit_lock_status',
53
54
  'edit_lock_try_steal',
55
+ 'edit_lock_watch',
54
56
  ]
55
57
 
56
58
  /**
@@ -68,9 +68,6 @@ function apply(ctx, config = {}) {
68
68
  }
69
69
  }
70
70
 
71
- // Edit Lock (when enabled) owns session interruption: only its trusted
72
- // resume re-arms editing work, never an ordinary user message or retry.
73
- const editLockBlocks = (agent) => ctx.get?.('weirEditLock')?.blocksContinuation?.(agent) === true
74
71
  const compactionBlocks = (agent) => agent && ctx.get?.('weirCompactionCoordinator')?.blocksContinuation(agent) === true
75
72
  /** Delayed followup continuation for the provider-error path. */
76
73
  function scheduleRetry(session, delayMs) {
@@ -82,7 +79,6 @@ function apply(ctx, config = {}) {
82
79
  if (remaining.length === 0) return
83
80
  if (!stateOf(session.id).armed) return
84
81
  if (jobsRunningOf(agent.session)) return
85
- if (editLockBlocks(agent)) return
86
82
  if (compactionBlocks(agent)) return
87
83
  injectOrWarn(ctx, `todo-driver: could not queue continuation for "${session.id}"`, () => {
88
84
  agent.followup(userTextMessage(renderContinuation(remaining), 'weir-todo-driver'))
@@ -95,7 +91,7 @@ function apply(ctx, config = {}) {
95
91
 
96
92
  // The sanctioned continuation boundary: steer and the turn runs on.
97
93
  function continueAtBoundary({ agent, signal }) {
98
- if (compactionBlocks(agent) || editLockBlocks(agent)) return
94
+ if (compactionBlocks(agent)) return
99
95
  if (!opts.enabled) return
100
96
  const state = stateOf(agent.id)
101
97
  const remaining = remainingTodos(agent.session)
@@ -106,7 +102,6 @@ function apply(ctx, config = {}) {
106
102
  error: providerErrorPending.get(agent.id),
107
103
  })
108
104
  if (decision.kind !== 'continue') return
109
- if (editLockBlocks(agent)) return
110
105
  injectOrWarn(ctx, `todo-driver: could not steer continuation for "${agent.id}"`, () => {
111
106
  agent.steer(userTextMessage(renderContinuation(remaining), 'weir-todo-driver'))
112
107
  })
@@ -72,12 +72,6 @@ const COPY = {
72
72
  abandonLine: (/** @type {string} */ id, /** @type {string} */ title, /** @type {string} */ state) => `Abandon lane \`${id}\` — ${title} (state ${state}).`,
73
73
  unmergedLost: (/** @type {number} */ count, /** @type {string} */ branch) => `**${count} unmerged commit(s)** on \`${branch}\` will be lost if the branch is removed.`,
74
74
  noUnmerged: (/** @type {string} */ branch) => `\`${branch}\` has no unmerged commits.`,
75
- residueWarning: (/** @type {{ operations: number, locks: number }} */ residue) => {
76
- const parts = []
77
- if (residue.operations > 0) parts.push(`${residue.operations} unresolved Edit Lock operation(s)`)
78
- if (residue.locks > 0) parts.push(`${residue.locks} Edit Lock lock(s)`)
79
- return `⚠️ **Edit Lock residue**: this lane's session left ${parts.join(' and ')} in the Edit Lock authority. Locks are reclaimed automatically by the stale-lock sweep; unresolved publications settle via crash self-heal (dead process) or one-click recovery in the Edit Lock maintenance panel (Settings → Editing). This is a warning only — cleanup still proceeds.`
80
- },
81
75
  dirtyWarning: (/** @type {{ modified: number, untracked: number }} */ dirty) => {
82
76
  const parts = []
83
77
  if (dirty.modified > 0) parts.push(`${dirty.modified} modified`)
@@ -137,12 +131,6 @@ const COPY = {
137
131
  abandonLine: (/** @type {string} */ id, /** @type {string} */ title, /** @type {string} */ state) => `放弃车道 \`${id}\` —— ${title}(状态 ${state})。`,
138
132
  unmergedLost: (/** @type {number} */ count, /** @type {string} */ branch) => `删除分支将**永久丢失** \`${branch}\` 上的 **${count} 个未合并提交**。`,
139
133
  noUnmerged: (/** @type {string} */ branch) => `\`${branch}\` 没有未合并的提交。`,
140
- residueWarning: (/** @type {{ operations: number, locks: number }} */ residue) => {
141
- const parts = []
142
- if (residue.operations > 0) parts.push(`${residue.operations} 个未决 Edit Lock 操作`)
143
- if (residue.locks > 0) parts.push(`${residue.locks} 把 Edit Lock 锁`)
144
- return `⚠️ **Edit Lock 残留**:该车道的会话在 Edit Lock 权威中留有 ${parts.join(' 和 ')}。锁会由 stale-lock 自动清扫回收;未决发布会由崩溃自愈(进程死亡时)或 Edit Lock 维护面板(设置 → 编辑)的一键恢复结清。此仅为警示——清理仍会进行。`
145
- },
146
134
  dirtyWarning: (/** @type {{ modified: number, untracked: number }} */ dirty) => {
147
135
  const parts = []
148
136
  if (dirty.modified > 0) parts.push(`${dirty.modified} 个已修改`)
@@ -193,15 +181,14 @@ export function cardCopy(locale) {
193
181
  ].join('\n')
194
182
  },
195
183
  /**
196
- * @param {any} lane @param {{ files: number, added: number, removed: number }} stat @param {string} [root] @param {{ operations: number, locks: number } | null} [residue]
184
+ * @param {any} lane @param {{ files: number, added: number, removed: number }} stat @param {string} [root]
197
185
  * @param {{ modified: number, untracked: number } | null} [dirty] - dirty-content probe; disclosed as its own warning paragraph when non-zero
198
186
  */
199
- cleanupDetail(lane, stat, root, residue = null, dirty = null) {
187
+ cleanupDetail(lane, stat, root, dirty = null) {
200
188
  return [
201
189
  `- ${copy.merged(lane.branch, lane.base.branch)} (${stat.files} ${copy.files}, +${stat.added} −${stat.removed}).`,
202
190
  `- ${copy.worktree}: \`${displayPath(lane.path, root)}\``,
203
191
  `- ${copy.scratch}`,
204
- ...(residue ? ['', copy.residueWarning(residue)] : []),
205
192
  ...(hasDirtyContent(dirty) ? ['', copy.dirtyWarning(/** @type {any} */ (dirty))] : []),
206
193
  ].join('\n')
207
194
  },
@@ -216,11 +203,11 @@ export function cardCopy(locale) {
216
203
  return copy.disputedReasons.noEvidence
217
204
  },
218
205
  /**
219
- * @param {any} lane @param {number} unmerged @param {string} [root] @param {{ operations: number, locks: number } | null} [residue]
206
+ * @param {any} lane @param {number} unmerged @param {string} [root]
220
207
  * @param {{ child: string, forced: boolean } | null} [reclaimed] - a binding this abandon already settled (design D1/D4), disclosed on the card
221
208
  * @param {{ modified: number, untracked: number } | null} [dirty] - dirty-content probe; disclosed as its own warning paragraph when non-zero
222
209
  */
223
- abandonDetail(lane, unmerged, root, residue = null, reclaimed = null, dirty = null) {
210
+ abandonDetail(lane, unmerged, root, reclaimed = null, dirty = null) {
224
211
  return [
225
212
  `- ${copy.abandonLine(lane.id, lane.title, lane.state)}`,
226
213
  `- ${copy.worktree}: \`${displayPath(lane.path, root)}\``,
@@ -228,22 +215,20 @@ export function cardCopy(locale) {
228
215
  // The unmerged-commit warning stays its own paragraph for emphasis.
229
216
  unmerged > 0 ? copy.unmergedLost(unmerged, lane.branch) : copy.noUnmerged(lane.branch),
230
217
  ...(reclaimed ? ['', copy.reconciledNote(reclaimed.child, reclaimed.forced)] : []),
231
- ...(residue ? ['', copy.residueWarning(residue)] : []),
232
218
  ...(hasDirtyContent(dirty) ? ['', copy.dirtyWarning(/** @type {any} */ (dirty))] : []),
233
219
  ].join('\n')
234
220
  },
235
221
  /**
236
222
  * The disputed-binding card body (design D4): lane facts, then the
237
223
  * reconciliation outcome as the warning paragraph.
238
- * @param {any} lane @param {string} child @param {{ childAlive?: boolean, ownerAlive?: boolean, terminalEvidence?: any } | null} verdict @param {string} [root] @param {{ operations: number, locks: number } | null} [residue]
224
+ * @param {any} lane @param {string} child @param {{ childAlive?: boolean, ownerAlive?: boolean, terminalEvidence?: any } | null} verdict @param {string} [root]
239
225
  */
240
- abandonDisputedDetail(lane, child, verdict, root, residue = null) {
226
+ abandonDisputedDetail(lane, child, verdict, root) {
241
227
  return [
242
228
  `- ${copy.abandonLine(lane.id, lane.title, lane.state)}`,
243
229
  `- ${copy.worktree}: \`${displayPath(lane.path, root)}\``,
244
230
  '',
245
231
  copy.disputedWarning(child, this.disputedReason(verdict)),
246
- ...(residue ? ['', copy.residueWarning(residue)] : []),
247
232
  ].join('\n')
248
233
  },
249
234
  }
@@ -16,7 +16,6 @@ import { normalizeRoot, parseRepoConfig } from './rules.js'
16
16
  import { reconcile } from './reconcile.js'
17
17
  import { createLedger } from './ledger.js'
18
18
  import { renderNotice, renderWatchExpired, renderWatchHit } from './prompts.js'
19
- import { authorityResidue } from './authority.js'
20
19
 
21
20
  export const CONFIG_FILE = '.config.json'
22
21
  const LOG_TAIL_LINES = 40
@@ -74,7 +73,6 @@ export function pathKey(path, platform = process.platform) {
74
73
  * @property {(handle: any) => void} [clearTimer]
75
74
  * @property {number} [pid] - this process id (tests)
76
75
  * @property {{ warn?: (message: string) => void }} [logger]
77
- * @property {(root: string, sessionId: string | null | undefined) => ({ operations: number, locks: number } | null)} [authorityResidue] - read-only Edit Lock residue probe feeding the cleanup/abandon warning (tests); defaults to the bounded snapshot read in ./authority.js
78
76
  * @property {(boundChild: string, ownerSession: string | null, context: { root: string }) => Promise<{ childAlive: boolean, ownerAlive: boolean, terminalEvidence: { source: string, detail: string } | null }>} [bindingLiveness] - read-only zombie-binding probe (design D2); absent = LANE_BUSY refusals stand unreconciled
79
77
  */
80
78
 
@@ -347,22 +345,6 @@ export function createLaneCore(deps) {
347
345
  }
348
346
  }
349
347
 
350
- /**
351
- * Best-effort Edit Lock authority-residue probe (design D5): does the
352
- * lane's owner session still own unresolved operations or locks in the
353
- * management domain? Warning input only — read-only, and a failed check
354
- * is silent (never blocks the cleanup/abandon transition).
355
- * @param {any} repo @param {any} lane @returns {{ operations: number, locks: number } | null}
356
- */
357
- function residueOf(repo, lane) {
358
- try {
359
- return (deps.authorityResidue ?? authorityResidue)(repo.mainRoot, lane?.ownerSession) ?? null
360
- } catch (error) {
361
- deps.logger?.warn?.(`worktree: authority-residue check failed: ${/** @type {any} */ (error)?.message ?? error}`)
362
- return null
363
- }
364
- }
365
-
366
348
  /** @param {any} lane */
367
349
  function result(lane, summary, extra = {}) {
368
350
  return {
@@ -635,7 +617,7 @@ export function createLaneCore(deps) {
635
617
  approveModeSafe, approveModeSourceSafe,
636
618
  cwdOf, config, repoFor, repoCached,
637
619
  armWatch, disarmWatch,
638
- laneOf, apply, patchLane, commitBinding, notifyOwner, residueOf, result, readConfig, logFile,
620
+ laneOf, apply, patchLane, commitBinding, notifyOwner, result, readConfig, logFile,
639
621
  refresh, reconcileNote, reconcileBinding, childSettled, check,
640
622
  }
641
623
  }
@@ -21,21 +21,9 @@ import { CONFIG_FILE, pathKey } from './lane-core.js'
21
21
 
22
22
  const SETUP_TIMEOUT_MS = 600_000
23
23
 
24
- /**
25
- * Model-facing English residue warning appended to cleanup/abandon tool
26
- * results (the human-facing card copy localizes its own in cards.js).
27
- * @param {{ operations: number, locks: number }} residue
28
- */
29
- function residueNote(residue) {
30
- const parts = []
31
- if (residue.operations > 0) parts.push(`${residue.operations} unresolved Edit Lock operation(s)`)
32
- if (residue.locks > 0) parts.push(`${residue.locks} Edit Lock lock(s)`)
33
- return `warning: the lane's session left ${parts.join(' and ')} in the management authority; locks are reclaimed by the stale-lock sweep, unresolved publications settle via crash self-heal (dead process) or the Edit Lock maintenance panel's one-click recovery`
34
- }
35
-
36
24
  /** @param {ReturnType<typeof import('./lane-core.js').createLaneCore>} core */
37
25
  export function createLaneWorkflows(core) {
38
- const { deps, git, now, pid, asking, approveModeSafe, cwdOf, config, repoFor, laneOf, apply, patchLane, commitBinding, notifyOwner, residueOf, result, readConfig, logFile, refresh, reconcileNote, reconcileBinding, armWatch, disarmWatch } = core
26
+ const { deps, git, now, pid, asking, approveModeSafe, cwdOf, config, repoFor, laneOf, apply, patchLane, commitBinding, notifyOwner, result, readConfig, logFile, refresh, reconcileNote, reconcileBinding, armWatch, disarmWatch } = core
39
27
 
40
28
  // ─── open / setup ─────────────────────────────────────────────────────
41
29
 
@@ -263,7 +251,7 @@ export function createLaneWorkflows(core) {
263
251
  id: 'abandon',
264
252
  header: copy.abandonHeader,
265
253
  question: copy.abandonQuestion(lane.title),
266
- detail: copy.abandonDisputedDetail(lane, lane.boundChild, reconciliation?.verdict ?? null, repo.mainRoot, residueOf(repo, lane)),
254
+ detail: copy.abandonDisputedDetail(lane, lane.boundChild, reconciliation?.verdict ?? null, repo.mainRoot),
267
255
  options: [
268
256
  { label: copy.forceReclaim, description: copy.forceReclaimDescription },
269
257
  { label: copy.cancel, description: copy.abandonDescriptions.cancel },
@@ -452,10 +440,9 @@ export function createLaneWorkflows(core) {
452
440
  }
453
441
  // Auto cleanup after the merge (design D3): auto-keep → mode worktree,
454
442
  // auto-clean → mode all. The existing cleanup() body runs unchanged —
455
- // scratch sync first, never --force on an auto path, the Edit Lock residue
456
- // probe feeding the summary. A blocked removal stays `landed` and is
457
- // reported concisely with the typed --force retry instead of a false
458
- // success (the user can clean up later).
443
+ // scratch sync first, never --force on an auto path. A blocked removal
444
+ // stays `landed` and is reported concisely with the typed --force retry
445
+ // instead of a false success (the user can clean up later).
459
446
  const cleanupMode = approveMode === 'auto-keep' ? 'worktree' : 'all'
460
447
  try {
461
448
  const cleaned = await cleanup(session, laneId, /** @type {'worktree' | 'all'} */ (cleanupMode), { by: 'host', auto: true, approveMode })
@@ -488,7 +475,7 @@ export function createLaneWorkflows(core) {
488
475
  id: 'cleanup',
489
476
  header: copy.cleanupHeader,
490
477
  question: copy.cleanupQuestion(lane.title),
491
- detail: copy.cleanupDetail(lane, lane.land?.stat ?? { files: 0, added: 0, removed: 0 }, repo.mainRoot, residueOf(repo, lane), dirtyCard ? dirty : null),
478
+ detail: copy.cleanupDetail(lane, lane.land?.stat ?? { files: 0, added: 0, removed: 0 }, repo.mainRoot, dirtyCard ? dirty : null),
492
479
  options: [
493
480
  { label: copy.choices.keep, description: copy.cleanupDescriptions.keep },
494
481
  { label: copy.choices.worktree, description: dirtyCard ? copy.cleanupDescriptionsDirty.worktree : copy.cleanupDescriptions.worktree },
@@ -539,9 +526,8 @@ export function createLaneWorkflows(core) {
539
526
  * Auto paths (design D3) reuse this exact body with `{ by: 'host', auto: true,
540
527
  * approveMode }`: the scratch sync, the removal (forced ONLY behind the
541
528
  * user's informed explicit choice — a disclosed card choice or a typed
542
- * `--force`, never an auto/model path), the residue warning and the
543
- * blocked-removal semantics are identical; only the audit payload gains the
544
- * auto marker.
529
+ * `--force`, never an auto/model path) and the blocked-removal semantics are
530
+ * identical; only the audit payload gains the auto marker.
545
531
  * @param {any} session @param {string} laneId @param {'keep' | 'worktree' | 'all'} mode @param {{ by?: string, auto?: boolean, approveMode?: string, force?: boolean }} [options]
546
532
  */
547
533
  async function cleanup(session, laneId, mode, options = {}) {
@@ -557,9 +543,6 @@ export function createLaneWorkflows(core) {
557
543
  const kept = await apply(repo, laneId, { type: 'keep', by, patch: { cleanup: { mode: 'keep', at: now(), by } } })
558
544
  return result(kept, `kept ${kept.path}`)
559
545
  }
560
- // Read-only residue probe BEFORE anything is removed (design D5): a
561
- // warning only, never a gate, and it never mutates the authority.
562
- const residue = residueOf(repo, lane)
563
546
  const scratch = existsSync(lane.path) ? syncScratch(repo, lane) : null
564
547
  // `force` reaches git only from an informed explicit user choice (a
565
548
  // disclosed card choice via askCleanup, or the typed `--force`); auto and
@@ -591,7 +574,7 @@ export function createLaneWorkflows(core) {
591
574
  ? await patchLane(repo, laneId, () => ({ cleanup: record }))
592
575
  : await apply(repo, laneId, { type: 'clean', by, patch: { cleanup: record } })
593
576
  deps.audit('cleanup', { lane: laneId, mode, by, scratch, ...(force ? { forced: true } : {}), ...(options.auto === true ? { auto: true, approveMode: options.approveMode ?? null } : {}) }, repo.mainRoot, lane.ownerSession)
594
- return result(next, `removed ${lane.path}${branchNote}${scratch ? `; scratch copied to ${scratch}` : ''}${residue ? `; ${residueNote(residue)}` : ''}`)
577
+ return result(next, `removed ${lane.path}${branchNote}${scratch ? `; scratch copied to ${scratch}` : ''}`)
595
578
  }
596
579
 
597
580
  /**
@@ -651,7 +634,7 @@ export function createLaneWorkflows(core) {
651
634
  id: 'abandon',
652
635
  header: copy.abandonHeader,
653
636
  question: copy.abandonQuestion(lane.title),
654
- detail: copy.abandonDetail(lane, unmerged, repo.mainRoot, residueOf(repo, lane), reconciliation?.settled ? { child: reconciliation.childId, forced: reconciliation.forced } : null, dirtyCard ? dirty : null),
637
+ detail: copy.abandonDetail(lane, unmerged, repo.mainRoot, reconciliation?.settled ? { child: reconciliation.childId, forced: reconciliation.forced } : null, dirtyCard ? dirty : null),
655
638
  options: [
656
639
  { label: copy.choices.keep, description: copy.abandonDescriptions.keep },
657
640
  { label: copy.choices.worktree, description: dirtyCard ? copy.abandonDescriptionsDirty.worktree : copy.abandonDescriptions.worktree },
@@ -678,10 +661,6 @@ export function createLaneWorkflows(core) {
678
661
  const auditExtra = { ...(auto ? { auto: true, approveMode } : {}), ...(force ? { forced: true } : {}) }
679
662
  const abandoned = await apply(repo, laneId, { type: 'abandon', by: auto ? 'host' : 'user', reason: auto ? `abandoned automatically (${approveMode} mode)` : 'abandoned by the user', audit: Object.keys(auditExtra).length > 0 ? auditExtra : undefined, patch: { cleanup: { mode, at: now(), by: auto ? 'host' : 'user' } } })
680
663
  if (mode === 'keep') return result(abandoned, `abandoned; ${abandoned.path} and ${abandoned.branch} are kept`)
681
- // Read-only residue probe BEFORE anything is removed (design D5): a
682
- // warning only, never a gate, and it never mutates the authority. The
683
- // confirmation card already carried the warning when there was one.
684
- const residue = residueOf(repo, lane)
685
664
  const scratch = existsSync(lane.path) ? syncScratch(repo, lane) : null
686
665
  if (existsSync(lane.path)) {
687
666
  try {
@@ -701,7 +680,7 @@ export function createLaneWorkflows(core) {
701
680
  branchNote = `; deleted ${lane.branch}${unmerged > 0 ? ` (${unmerged} unmerged commit(s) discarded)` : ''}`
702
681
  }
703
682
  const final = await patchLane(repo, laneId, (current) => ({ cleanup: { ...current.cleanup, scratch } }))
704
- return result(final, `abandoned; removed ${lane.path}${branchNote}${residue ? `; ${residueNote(residue)}` : ''}`)
683
+ return result(final, `abandoned; removed ${lane.path}${branchNote}`)
705
684
  }
706
685
 
707
686
  // ─── watches (lane state subscriptions) ────────────────────────────────
@@ -91,7 +91,7 @@ import { bareSetupCommand } from './pkgmgr.js'
91
91
  * Lockfile-derived package MANAGER, or null when the repository has none.
92
92
  * Only the manager is decided here: how it is invoked (system tool, DSH
93
93
  * bundled runtime, or nothing available) is resolved separately so a bare
94
- * `pnpm` is never assumed to exist on PATH (AGENTS.md §2, S21).
94
+ * `pnpm` is never assumed to exist on PATH (docs/contributing/development.md §1, S21).
95
95
  * @param {Iterable<string>} fileNames - names present at the lane root
96
96
  * @returns {'pnpm' | 'bun' | 'yarn' | 'npm' | null}
97
97
  */