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.
- package/cordis.patch.yml +7 -18
- package/lib/client.capability-badge.js +2 -0
- package/lib/client.capability-manager.js +46 -179
- package/lib/client.capability-model.js +428 -3
- package/lib/client.file-lease-badge.js +307 -0
- package/lib/client.file-lease-tool-model.js +189 -0
- package/lib/client.file-lease-tool-view.js +434 -0
- package/lib/client.js +124 -36
- package/lib/client.mcp-manager.js +563 -0
- package/lib/client.settings-page.js +10 -34
- package/package.json +2 -2
- package/skills/mcp-server-setup/SKILL.md +124 -0
- package/src/capabilities/apply-engine.js +18 -4
- package/src/capabilities/capability-remote.js +296 -45
- package/src/capabilities/initial-selection.js +3 -2
- package/src/capabilities/lifecycle-preload.js +69 -11
- package/src/capabilities/lifecycle-snapshot.js +127 -39
- package/src/capabilities/mcp-admission.js +2 -2
- package/src/capabilities/mcp-config.js +344 -0
- package/src/capabilities/mcp-credentials.js +180 -0
- package/src/capabilities/mcp-diagnostics.js +169 -0
- package/src/capabilities/mcp-drain.js +18 -4
- package/src/capabilities/mcp-facade.js +3 -3
- package/src/capabilities/mcp-gate.js +51 -52
- package/src/capabilities/mcp-install-tool.js +276 -0
- package/src/capabilities/mcp-manager.js +368 -142
- package/src/capabilities/mcp-mount.js +219 -79
- package/src/capabilities/mcp-operations.js +359 -0
- package/src/capabilities/mcp-process.js +43 -0
- package/src/capabilities/mcp-registry.js +74 -60
- package/src/capabilities/mcp-runtime.js +103 -0
- package/src/capabilities/read-payloads.js +8 -3
- package/src/capabilities/skill-applicability.js +185 -0
- package/src/capabilities/skill-builtin-migration.js +2 -1
- package/src/capabilities/skill-selection-plugin.js +161 -56
- package/src/capabilities/store/checked-contract.d.ts +2 -2
- package/src/capabilities/store/fs-adapter.js +2 -3
- package/src/context-guard/index.js +2 -2
- package/src/delegate/supervision-mount.js +5 -5
- package/src/delegate/tool.js +2 -10
- package/src/file-lease/client.js +1299 -0
- package/src/file-lease/index.js +34 -0
- package/src/file-lease/lineage.js +207 -0
- package/src/file-lease/node-contracts.d.ts +42 -0
- package/src/file-lease/plugin.js +1013 -0
- package/src/file-lease/singleton.js +155 -0
- package/src/file-lease/spawn.js +62 -0
- package/src/file-lease/tools.js +847 -0
- package/src/file-lease/view.js +144 -0
- package/src/file-lease/watch.js +473 -0
- package/src/hashline-edit/index.js +76 -42
- package/src/hashline-edit/write-tool.js +90 -0
- package/src/intent-gate/index.js +29 -12
- package/src/intent-gate/matcher.js +36 -2
- package/src/lease-broker/entry.js +98 -0
- package/src/lease-broker/node-contracts.d.ts +63 -0
- package/src/lease-broker/protocol.js +237 -0
- package/src/lease-broker/server.js +402 -0
- package/src/lease-broker/state.js +482 -0
- package/src/lsp/tools.js +137 -27
- package/src/process/index.js +4 -2
- package/src/settings/index.js +2 -44
- package/src/settings/sections.js +2 -24
- package/src/shared/audit.js +9 -1
- package/src/shared/child-scope.js +2 -0
- package/src/todo-driver/index.js +1 -6
- package/src/worktree/cards.js +6 -21
- package/src/worktree/lane-core.js +1 -19
- package/src/worktree/lane-workflows.js +11 -32
- package/src/worktree/rules.js +1 -1
- package/lib/client.edit-lock-maintenance.js +0 -592
- package/lib/client.edit-lock-panel.js +0 -405
- package/src/edit-lock/adapter.js +0 -57
- package/src/edit-lock/admin-ledger.js +0 -149
- package/src/edit-lock/admin-recovery.js +0 -151
- package/src/edit-lock/admission.js +0 -82
- package/src/edit-lock/binding-attestation.js +0 -29
- package/src/edit-lock/binding-guidance.js +0 -72
- package/src/edit-lock/binding-machine.js +0 -312
- package/src/edit-lock/call-context.js +0 -65
- package/src/edit-lock/derived-facts.js +0 -67
- package/src/edit-lock/domain.js +0 -165
- package/src/edit-lock/domains.js +0 -170
- package/src/edit-lock/host.js +0 -149
- package/src/edit-lock/incarnations.js +0 -150
- package/src/edit-lock/index.js +0 -1303
- package/src/edit-lock/inspect.js +0 -219
- package/src/edit-lock/lifecycle.js +0 -579
- package/src/edit-lock/limits.js +0 -61
- package/src/edit-lock/maintenance.js +0 -310
- package/src/edit-lock/manager.js +0 -875
- package/src/edit-lock/negotiation.js +0 -132
- package/src/edit-lock/operation-history.js +0 -214
- package/src/edit-lock/peer-client.js +0 -86
- package/src/edit-lock/peer-transport.js +0 -76
- package/src/edit-lock/peer.js +0 -87
- package/src/edit-lock/publisher.js +0 -191
- package/src/edit-lock/read-authority.js +0 -101
- package/src/edit-lock/recovery.js +0 -139
- package/src/edit-lock/remote.js +0 -362
- package/src/edit-lock/request-binding.js +0 -42
- package/src/edit-lock/request-data.js +0 -40
- package/src/edit-lock/reservation.js +0 -301
- package/src/edit-lock/reserved-runtime.js +0 -45
- package/src/edit-lock/resource-identity.js +0 -130
- package/src/edit-lock/runtime.js +0 -145
- package/src/edit-lock/settle.js +0 -144
- package/src/edit-lock/snapshot.js +0 -175
- package/src/edit-lock/stale-sweep.js +0 -120
- package/src/edit-lock/state.js +0 -462
- package/src/edit-lock/store.js +0 -327
- package/src/edit-lock/tool-scope.js +0 -29
- package/src/edit-lock/view.js +0 -170
- package/src/edit-lock/write-scopes.js +0 -48
- package/src/edit-lock/write-tool.js +0 -45
- package/src/shared/delegate-edit-lifecycle.js +0 -36
- package/src/worktree/authority.js +0 -56
package/src/process/index.js
CHANGED
|
@@ -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
|
-
|
|
480
|
-
`Install the server as a plain .exe, or 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]
|
package/src/settings/index.js
CHANGED
|
@@ -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 () => {
|
|
91
|
+
return () => { offLspAdmin(); offEnvAdmin(); listeners.clear() }
|
|
134
92
|
}
|
|
135
93
|
}
|
|
136
94
|
const apply = createSettingsPlugin()
|
package/src/settings/sections.js
CHANGED
|
@@ -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,
|
|
102
|
-
*
|
|
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)
|
package/src/shared/audit.js
CHANGED
|
@@ -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
|
/**
|
package/src/todo-driver/index.js
CHANGED
|
@@ -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)
|
|
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
|
})
|
package/src/worktree/cards.js
CHANGED
|
@@ -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]
|
|
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,
|
|
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]
|
|
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,
|
|
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]
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
456
|
-
//
|
|
457
|
-
//
|
|
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,
|
|
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)
|
|
543
|
-
*
|
|
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}` : ''}
|
|
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,
|
|
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}
|
|
683
|
+
return result(final, `abandoned; removed ${lane.path}${branchNote}`)
|
|
705
684
|
}
|
|
706
685
|
|
|
707
686
|
// ─── watches (lane state subscriptions) ────────────────────────────────
|
package/src/worktree/rules.js
CHANGED
|
@@ -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 (
|
|
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
|
*/
|